mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Various minor fixes. WARNING: Adding of books is currently broken.
This commit is contained in:
parent
2e0ad5d1e0
commit
1a117fd070
@ -231,7 +231,7 @@ class DevicePlugin(Plugin):
|
||||
def settings(cls):
|
||||
'''
|
||||
Should return an opts object. The opts object should have one attribute
|
||||
`formats` whihc is an ordered list of formats for the device.
|
||||
`format_map` which is an ordered list of formats for the device.
|
||||
'''
|
||||
raise NotImplementedError()
|
||||
|
||||
|
@ -53,7 +53,7 @@ class KINDLE(USBMS):
|
||||
|
||||
@classmethod
|
||||
def metadata_from_path(cls, path):
|
||||
from calibre.devices.usbms.driver import metadata_from_formats
|
||||
from calibre.ebooks.metadata.meta import metadata_from_formats
|
||||
mi = metadata_from_formats([path])
|
||||
if mi.title == _('Unknown') or ('-asin' in mi.title and '-type' in mi.title):
|
||||
match = cls.WIRELESS_FILE_NAME_PATTERN.match(os.path.basename(path))
|
||||
|
@ -116,7 +116,7 @@ class Book(object):
|
||||
self.authors.encode('utf-8') + " at " + self.path.encode('utf-8')
|
||||
|
||||
|
||||
def fix_ids(media, cache):
|
||||
def fix_ids(media, cache, *args):
|
||||
'''
|
||||
Adjust ids in cache to correspond with media.
|
||||
'''
|
||||
|
@ -47,6 +47,7 @@ from calibre.devices.prs500.prstypes import *
|
||||
from calibre.devices.errors import *
|
||||
from calibre.devices.prs500.books import BookList, fix_ids
|
||||
from calibre import __author__, __appname__
|
||||
from calibre.devices.usbms.deviceconfig import DeviceConfig
|
||||
|
||||
# Protocol versions this driver has been tested with
|
||||
KNOWN_USB_PROTOCOL_VERSIONS = [0x3030303030303130L]
|
||||
@ -76,7 +77,7 @@ class File(object):
|
||||
return self.name
|
||||
|
||||
|
||||
class PRS500(DevicePlugin):
|
||||
class PRS500(DeviceConfig, DevicePlugin):
|
||||
|
||||
"""
|
||||
Implements the backend for communication with the SONY Reader.
|
||||
@ -624,6 +625,8 @@ class PRS500(DevicePlugin):
|
||||
data_type=FreeSpaceAnswer, \
|
||||
command_number=FreeSpaceQuery.NUMBER)[0]
|
||||
data.append( pkt.free )
|
||||
data = [x for x in data if x != 0]
|
||||
data.append(0)
|
||||
return data
|
||||
|
||||
def _exists(self, path):
|
||||
|
@ -147,7 +147,7 @@ class BookList(_BookList):
|
||||
nodes = self.root_element.childNodes
|
||||
for i, book in enumerate(nodes):
|
||||
if report_progress:
|
||||
self.report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...'))
|
||||
report_progress((i+1) / float(len(nodes)), _('Getting list of books on device...'))
|
||||
if hasattr(book, 'tagName') and book.tagName.endswith('text'):
|
||||
tags = [i.getAttribute('title') for i in self.get_playlists(book.getAttribute('id'))]
|
||||
self.append(Book(book, mountpath, tags, prefix=self.prefix))
|
||||
|
@ -12,7 +12,8 @@ class DeviceConfig(object):
|
||||
|
||||
@classmethod
|
||||
def _config(cls):
|
||||
c = Config('device_drivers_%s' % cls.__class__.__name__, _('settings for device drivers'))
|
||||
klass = cls if isinstance(cls, type) else cls.__class__
|
||||
c = Config('device_drivers_%s' % klass.__name__, _('settings for device drivers'))
|
||||
c.add_opt('format_map', default=cls.FORMATS, help=cls.HELP_MESSAGE)
|
||||
return c
|
||||
|
||||
|
96
src/calibre/ebooks/conversion/config.py
Normal file
96
src/calibre/ebooks/conversion/config.py
Normal file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.lock import ExclusiveFile
|
||||
from calibre import sanitize_file_name
|
||||
from calibre.customize.conversion import OptionRecommendation
|
||||
|
||||
|
||||
config_dir = os.path.join(config_dir, 'conversion')
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
|
||||
def name_to_path(name):
|
||||
return os.path.join(config_dir, sanitize_file_name(name)+'.py')
|
||||
|
||||
def save_defaults(name, recs):
|
||||
path = name_to_path(name)
|
||||
raw = str(recs)
|
||||
with open(path, 'wb'):
|
||||
pass
|
||||
with ExclusiveFile(path) as f:
|
||||
f.write(raw)
|
||||
|
||||
def load_defaults(name):
|
||||
path = name_to_path(name)
|
||||
if not os.path.exists(path):
|
||||
open(path, 'wb').close()
|
||||
with ExclusiveFile(path) as f:
|
||||
raw = f.read()
|
||||
r = GuiRecommendations()
|
||||
if raw:
|
||||
r.from_string(raw)
|
||||
return r
|
||||
|
||||
def save_specifics(db, book_id, recs):
|
||||
raw = str(recs)
|
||||
db.set_conversion_options(book_id, 'PIPE', raw)
|
||||
|
||||
def load_specifics(db, book_id):
|
||||
raw = db.conversion_options(book_id, 'PIPE')
|
||||
r = GuiRecommendations()
|
||||
if raw:
|
||||
r.from_string(raw)
|
||||
return r
|
||||
|
||||
class GuiRecommendations(dict):
|
||||
|
||||
def __new__(cls, *args):
|
||||
dict.__new__(cls)
|
||||
obj = super(GuiRecommendations, cls).__new__(cls, *args)
|
||||
obj.disabled_options = set([])
|
||||
return obj
|
||||
|
||||
def to_recommendations(self, level=OptionRecommendation.LOW):
|
||||
ans = []
|
||||
for key, val in self.items():
|
||||
ans.append((key, val, level))
|
||||
return ans
|
||||
|
||||
def __str__(self):
|
||||
ans = ['{']
|
||||
for key, val in self.items():
|
||||
ans.append('\t'+repr(key)+' : '+repr(val)+',')
|
||||
ans.append('}')
|
||||
return '\n'.join(ans)
|
||||
|
||||
def from_string(self, raw):
|
||||
try:
|
||||
d = eval(raw)
|
||||
except SyntaxError:
|
||||
d = None
|
||||
if d:
|
||||
self.update(d)
|
||||
|
||||
def merge_recommendations(self, get_option, level, options,
|
||||
only_existing=False):
|
||||
for name in options:
|
||||
if only_existing and name not in self:
|
||||
continue
|
||||
opt = get_option(name)
|
||||
if opt is None: continue
|
||||
if opt.level == OptionRecommendation.HIGH:
|
||||
self[name] = opt.recommended_value
|
||||
self.disabled_options.add(name)
|
||||
elif opt.level > level or name not in self:
|
||||
self[name] = opt.recommended_value
|
||||
|
||||
|
@ -241,7 +241,12 @@ class MetaInformation(object):
|
||||
self.tags += mi.tags
|
||||
self.tags = list(set(self.tags))
|
||||
|
||||
if getattr(mi, 'cover_data', None) and mi.cover_data[0] is not None:
|
||||
if getattr(mi, 'cover_data', False):
|
||||
other_cover = mi.cover_data[-1]
|
||||
self_cover = self.cover_data[-1] if self.cover_data else ''
|
||||
if not self_cover: self_cover = ''
|
||||
if not other_cover: other_cover = ''
|
||||
if len(other_cover) > len(self_cover):
|
||||
self.cover_data = mi.cover_data
|
||||
|
||||
my_comments = getattr(self, 'comments', '')
|
||||
|
@ -9,12 +9,9 @@ __copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net and ' \
|
||||
'Marshall T. Vandegrift <llasram@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import sys
|
||||
import os
|
||||
from struct import pack, unpack
|
||||
from cStringIO import StringIO
|
||||
from calibre.ebooks.mobi import MobiError
|
||||
from calibre.ebooks.mobi.reader import get_metadata
|
||||
from calibre.ebooks.mobi.writer import rescale_image, MAX_THUMB_DIMEN
|
||||
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||
|
||||
@ -116,8 +113,13 @@ class MetadataUpdater(object):
|
||||
|
||||
def update(self, mi):
|
||||
recs = []
|
||||
from calibre.ebooks.mobi.from_any import config
|
||||
if mi.author_sort and config().parse().prefer_author_sort:
|
||||
try:
|
||||
from calibre.ebooks.conversion.config import load_defaults
|
||||
prefs = load_defaults('mobi_output')
|
||||
pas = prefs.get('prefer_author_sort', False)
|
||||
except:
|
||||
pas = False
|
||||
if mi.author_sort and pas:
|
||||
authors = mi.author_sort
|
||||
recs.append((100, authors.encode(self.codec, 'replace')))
|
||||
elif mi.authors:
|
||||
|
111
src/calibre/ebooks/metadata/worker.py
Normal file
111
src/calibre/ebooks/metadata/worker.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
from __future__ import with_statement
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from threading import Thread
|
||||
from Queue import Empty
|
||||
import os, time
|
||||
|
||||
from calibre.utils.ipc.job import ParallelJob
|
||||
from calibre.utils.ipc.server import Server
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre import prints
|
||||
|
||||
def read_metadata_(task, tdir, notification=lambda x,y:x):
|
||||
from calibre.ebooks.metadata.meta import metadata_from_formats
|
||||
from calibre.ebooks.metadata.opf2 import OPFCreator
|
||||
for x in task:
|
||||
id, formats = x
|
||||
if isinstance(formats, basestring): formats = [formats]
|
||||
mi = metadata_from_formats(formats)
|
||||
mi.cover = None
|
||||
cdata = None
|
||||
if mi.cover_data:
|
||||
cdata = mi.cover_data[-1]
|
||||
mi.cover_data = None
|
||||
opf = OPFCreator(tdir, mi)
|
||||
with open(os.path.join(tdir, '%s.opf'%id), 'wb') as f:
|
||||
opf.render(f)
|
||||
if cdata:
|
||||
with open(os.path.join(tdir, str(id)), 'wb') as f:
|
||||
f.write(cdata)
|
||||
notification(0.5, id)
|
||||
|
||||
class Progress(object):
|
||||
|
||||
def __init__(self, result_queue, tdir):
|
||||
self.result_queue = result_queue
|
||||
self.tdir = tdir
|
||||
|
||||
def __call__(self, id):
|
||||
cover = os.path.join(self.tdir, str(id))
|
||||
if not os.path.exists(cover): cover = None
|
||||
self.result_queue.put((id, os.path.join(self.tdir, id+'.opf'), cover))
|
||||
|
||||
class ReadMetadata(Thread):
|
||||
|
||||
def __init__(self, tasks, result_queue):
|
||||
self.tasks, self.result_queue = tasks, result_queue
|
||||
self.canceled = False
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self.tdir = PersistentTemporaryDirectory('_rm_worker')
|
||||
|
||||
|
||||
def run(self):
|
||||
jobs, ids = set([]), set([id for id, p in self.tasks])
|
||||
progress = Progress(self.result_queue, self.tdir)
|
||||
server = Server()
|
||||
for i, task in enumerate(self.tasks):
|
||||
job = ParallelJob('read_metadata',
|
||||
'Read metadata (%d of %d)'%(i, len(self.tasks)),
|
||||
lambda x,y:x, args=[task, self.tdir])
|
||||
jobs.add(job)
|
||||
server.add_job(job)
|
||||
|
||||
while not self.canceled:
|
||||
time.sleep(0.2)
|
||||
running = False
|
||||
for job in jobs:
|
||||
while True:
|
||||
try:
|
||||
id = job.notifications.get_nowait()[-1]
|
||||
progress(id)
|
||||
ids.remove(id)
|
||||
except Empty:
|
||||
break
|
||||
job.update()
|
||||
if not job.is_finished:
|
||||
running = True
|
||||
if not running:
|
||||
break
|
||||
|
||||
if self.canceled:
|
||||
server.close()
|
||||
time.sleep(1)
|
||||
return
|
||||
|
||||
for id in ids:
|
||||
progress(id)
|
||||
|
||||
for job in jobs:
|
||||
if job.failed:
|
||||
prints(job.details)
|
||||
if os.path.exists(job.log_path):
|
||||
os.remove(job.log_path)
|
||||
|
||||
|
||||
def read_metadata(paths, result_queue):
|
||||
tasks = []
|
||||
chunk = 50
|
||||
pos = 0
|
||||
while pos < len(paths):
|
||||
tasks.append(paths[pos:pos+chunk])
|
||||
pos += chunk
|
||||
t = ReadMetadata(tasks, result_queue)
|
||||
t.start()
|
||||
return t
|
@ -2,238 +2,152 @@
|
||||
UI for adding books to the database
|
||||
'''
|
||||
import os
|
||||
from threading import Queue, Empty
|
||||
|
||||
from PyQt4.Qt import QThread, SIGNAL, QMutex, QWaitCondition, Qt
|
||||
from PyQt4.Qt import QThread, SIGNAL, QObject, QTimer
|
||||
|
||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||
from calibre.constants import preferred_encoding
|
||||
from calibre.gui2 import warning_dialog
|
||||
from calibre.ebooks.metadata.opf2 import OPF
|
||||
from calibre.ebooks.metadata import MetaInformation
|
||||
from calibre.constants import preferred_encoding
|
||||
|
||||
class Add(QThread):
|
||||
class RecursiveFind(QThread):
|
||||
|
||||
def __init__(self):
|
||||
QThread.__init__(self)
|
||||
self._lock = QMutex()
|
||||
self._waiting = QWaitCondition()
|
||||
|
||||
def is_canceled(self):
|
||||
if self.pd.canceled:
|
||||
self.canceled = True
|
||||
return self.canceled
|
||||
|
||||
def wait_for_condition(self):
|
||||
self._lock.lock()
|
||||
self._waiting.wait(self._lock)
|
||||
self._lock.unlock()
|
||||
|
||||
def wake_up(self):
|
||||
self._waiting.wakeAll()
|
||||
|
||||
class AddFiles(Add):
|
||||
|
||||
def __init__(self, paths, default_thumbnail, get_metadata, db=None):
|
||||
Add.__init__(self)
|
||||
self.paths = paths
|
||||
self.get_metadata = get_metadata
|
||||
self.default_thumbnail = default_thumbnail
|
||||
def __init__(self, parent, db, root, single):
|
||||
QThread.__init__(self, parent)
|
||||
self.db = db
|
||||
self.formats, self.metadata, self.names, self.infos = [], [], [], []
|
||||
self.duplicates = []
|
||||
self.number_of_books_added = 0
|
||||
self.connect(self.get_metadata,
|
||||
SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.metadata_delivered)
|
||||
|
||||
def metadata_delivered(self, id, mi):
|
||||
if self.is_canceled():
|
||||
self.wake_up()
|
||||
return
|
||||
if not mi.title:
|
||||
mi.title = os.path.splitext(self.names[id])[0]
|
||||
mi.title = mi.title if isinstance(mi.title, unicode) else \
|
||||
mi.title.decode(preferred_encoding, 'replace')
|
||||
self.metadata.append(mi)
|
||||
self.infos.append({'title':mi.title,
|
||||
'authors':', '.join(mi.authors),
|
||||
'cover':self.default_thumbnail, 'tags':[]})
|
||||
if self.db is not None:
|
||||
duplicates, num = self.db.add_books(self.paths[id:id+1],
|
||||
self.formats[id:id+1], [mi],
|
||||
add_duplicates=False)
|
||||
self.number_of_books_added += num
|
||||
if duplicates:
|
||||
if not self.duplicates:
|
||||
self.duplicates = [[], [], [], []]
|
||||
for i in range(4):
|
||||
self.duplicates[i] += duplicates[i]
|
||||
self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
|
||||
mi.title, id)
|
||||
self.wake_up()
|
||||
|
||||
def create_progress_dialog(self, title, msg, parent):
|
||||
self._parent = parent
|
||||
self.pd = ProgressDialog(title, msg, -1, len(self.paths)-1, parent)
|
||||
self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
|
||||
self.update_progress_dialog)
|
||||
self.pd.setModal(True)
|
||||
self.pd.show()
|
||||
self.connect(self, SIGNAL('finished()'), self.pd.hide)
|
||||
return self.pd
|
||||
|
||||
|
||||
def update_progress_dialog(self, title, count):
|
||||
self.pd.set_value(count)
|
||||
if self.db is not None:
|
||||
self.pd.set_msg(_('Added %s to library')%title)
|
||||
else:
|
||||
self.pd.set_msg(_('Read metadata from ')+title)
|
||||
|
||||
self.path = root, self.single_book_per_directory = single
|
||||
self.canceled = False
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.canceled = False
|
||||
for c, book in enumerate(self.paths):
|
||||
if self.pd.canceled:
|
||||
self.canceled = True
|
||||
break
|
||||
format = os.path.splitext(book)[1]
|
||||
format = format[1:] if format else None
|
||||
stream = open(book, 'rb')
|
||||
self.formats.append(format)
|
||||
self.names.append(os.path.basename(book))
|
||||
self.get_metadata(c, stream, stream_type=format,
|
||||
use_libprs_metadata=True)
|
||||
self.wait_for_condition()
|
||||
finally:
|
||||
self.disconnect(self.get_metadata,
|
||||
SIGNAL('metadata(PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.metadata_delivered)
|
||||
self.get_metadata = None
|
||||
|
||||
|
||||
def process_duplicates(self):
|
||||
if self.duplicates:
|
||||
files = ''
|
||||
for mi in self.duplicates[2]:
|
||||
files += mi.title+'\n'
|
||||
d = warning_dialog(_('Duplicates found!'),
|
||||
_('Books with the same title as the following already '
|
||||
'exist in the database. Add them anyway?'),
|
||||
files, parent=self._parent)
|
||||
if d.exec_() == d.Accepted:
|
||||
num = self.db.add_books(*self.duplicates,
|
||||
**dict(add_duplicates=True))[1]
|
||||
self.number_of_books_added += num
|
||||
|
||||
|
||||
class AddRecursive(Add):
|
||||
|
||||
def __init__(self, path, db, get_metadata, single_book_per_directory, parent):
|
||||
self.path = path
|
||||
self.db = db
|
||||
self.get_metadata = get_metadata
|
||||
self.single_book_per_directory = single_book_per_directory
|
||||
self.duplicates, self.books, self.metadata = [], [], []
|
||||
self.number_of_books_added = 0
|
||||
self.canceled = False
|
||||
Add.__init__(self)
|
||||
self.connect(self.get_metadata,
|
||||
SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.metadata_delivered, Qt.QueuedConnection)
|
||||
self.connect(self, SIGNAL('searching_done()'), self.searching_done,
|
||||
Qt.QueuedConnection)
|
||||
self._parent = parent
|
||||
self.pd = ProgressDialog(_('Adding books recursively...'),
|
||||
_('Searching for books in all sub-directories...'),
|
||||
0, 0, parent)
|
||||
self.connect(self, SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
|
||||
self.update_progress_dialog)
|
||||
self.connect(self, SIGNAL('update(PyQt_PyObject)'), self.pd.set_msg,
|
||||
Qt.QueuedConnection)
|
||||
self.connect(self, SIGNAL('pupdate(PyQt_PyObject)'), self.pd.set_value,
|
||||
Qt.QueuedConnection)
|
||||
self.pd.setModal(True)
|
||||
self.pd.show()
|
||||
self.connect(self, SIGNAL('finished()'), self.pd.hide)
|
||||
|
||||
def update_progress_dialog(self, title, count):
|
||||
self.pd.set_value(count)
|
||||
if title:
|
||||
self.pd.set_msg(_('Read metadata from ')+title)
|
||||
|
||||
def metadata_delivered(self, id, mi):
|
||||
if self.is_canceled():
|
||||
self.wake_up()
|
||||
return
|
||||
self.emit(SIGNAL('processed(PyQt_PyObject,PyQt_PyObject)'),
|
||||
mi.title, id)
|
||||
self.metadata.append((mi if mi.title else None, self.books[id]))
|
||||
if len(self.metadata) >= len(self.books):
|
||||
self.metadata = [x for x in self.metadata if x[0] is not None]
|
||||
self.pd.set_min(-1)
|
||||
self.pd.set_max(len(self.metadata)-1)
|
||||
self.pd.set_value(-1)
|
||||
self.pd.set_msg(_('Adding books to database...'))
|
||||
self.wake_up()
|
||||
|
||||
def searching_done(self):
|
||||
self.pd.set_min(-1)
|
||||
self.pd.set_max(len(self.books)-1)
|
||||
self.pd.set_value(-1)
|
||||
self.pd.set_msg(_('Reading metadata...'))
|
||||
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
root = os.path.abspath(self.path)
|
||||
for dirpath in os.walk(root):
|
||||
if self.is_canceled():
|
||||
if self.canceled:
|
||||
return
|
||||
self.emit(SIGNAL('update(PyQt_PyObject)'),
|
||||
_('Searching in')+' '+dirpath[0])
|
||||
self.books += list(self.db.find_books_in_directory(dirpath[0],
|
||||
self.single_book_per_directory))
|
||||
self.books = [formats for formats in self.books if formats]
|
||||
# Reset progress bar
|
||||
self.emit(SIGNAL('searching_done()'))
|
||||
|
||||
for c, formats in enumerate(self.books):
|
||||
self.get_metadata.from_formats(c, formats)
|
||||
self.wait_for_condition()
|
||||
if not self.canceled:
|
||||
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
|
||||
|
||||
# Add books to database
|
||||
for c, x in enumerate(self.metadata):
|
||||
mi, formats = x
|
||||
if self.is_canceled():
|
||||
break
|
||||
if self.db.has_book(mi):
|
||||
self.duplicates.append((mi, formats))
|
||||
else:
|
||||
self.db.import_book(mi, formats, notify=False)
|
||||
|
||||
class Adder(QObject):
|
||||
|
||||
def __init__(self, parent, db, callback):
|
||||
QObject.__init__(self, parent)
|
||||
self.pd = ProgressDialog(_('Add books'), parent=parent)
|
||||
self.db = db
|
||||
self.pd.setModal(True)
|
||||
self.pd.show()
|
||||
self._parent = parent
|
||||
self.number_of_books_added = 0
|
||||
self.rfind = self.worker = self.timer = None
|
||||
self.callback = callback
|
||||
self.infos, self.paths, self.names = [], [], []
|
||||
self.connect(self.pd, SIGNAL('canceled()'), self.canceled)
|
||||
|
||||
def add_recursive(self, root, single=True):
|
||||
self.path = root
|
||||
self.pd.set_msg(_('Searching for books in all sub-directories...'))
|
||||
self.pd.set_min(0)
|
||||
self.pd.set_max(0)
|
||||
self.pd.value = 0
|
||||
self.rfind = RecursiveFind(self, self.db, root, single)
|
||||
self.connect(self.rfind, SIGNAL('update(PyQt_PyObject)'),
|
||||
self.pd.set_msg)
|
||||
self.connect(self.rfind, SIGNAL('found(PyQt_PyObject)'),
|
||||
self.add)
|
||||
|
||||
def add(self, books):
|
||||
books = [[b] if isinstance(b, basestring) else b for b in books]
|
||||
self.rfind = None
|
||||
from calibre.ebooks.metadata.worker import read_metadata
|
||||
self.rq = Queue()
|
||||
tasks = []
|
||||
self.ids = {}
|
||||
self.nmap = {}
|
||||
self.duplicates = []
|
||||
for i, b in books:
|
||||
tasks.append((i, b))
|
||||
self.ids[i] = b
|
||||
self.nmap = os.path.basename(b[0])
|
||||
self.worker = read_metadata(tasks, self.rq)
|
||||
self.pd.set_min(0)
|
||||
self.pd.set_max(len(self.ids))
|
||||
self.pd.value = 0
|
||||
self.timer = QTimer(self)
|
||||
self.connect(self.timer, SIGNAL('timeout()'), self.update)
|
||||
self.timer.start(200)
|
||||
|
||||
def add_formats(self, id, formats):
|
||||
for path in formats:
|
||||
fmt = os.path.splitext(path)[-1].replace('.', '').upper()
|
||||
self.db.add_format(id, fmt, open(path, 'rb'), index_is_id=True,
|
||||
notify=False)
|
||||
|
||||
def canceled(self):
|
||||
if self.rfind is not None:
|
||||
self.rfind.cenceled = True
|
||||
if self.timer is not None:
|
||||
self.timer.stop()
|
||||
if self.worker is not None:
|
||||
self.worker.canceled = True
|
||||
self.pd.hide()
|
||||
|
||||
|
||||
def update(self):
|
||||
if not self.ids:
|
||||
self.timer.stop()
|
||||
self.process_duplicates()
|
||||
self.pd.hide()
|
||||
self.callback(self.paths, self.names, self.infos)
|
||||
return
|
||||
|
||||
try:
|
||||
id, opf, cover = self.rq.get_nowait()
|
||||
except Empty:
|
||||
return
|
||||
self.pd.value += 1
|
||||
formats = self.ids.pop(id)
|
||||
mi = MetaInformation(OPF(opf))
|
||||
name = self.nmap.pop(id)
|
||||
if not mi.title:
|
||||
mi.title = os.path.splitext(name)[0]
|
||||
mi.title = mi.title if isinstance(mi.title, unicode) else \
|
||||
mi.title.decode(preferred_encoding, 'replace')
|
||||
self.pd.set_msg(_('Added')+' '+mi.title)
|
||||
|
||||
if self.db is not None:
|
||||
if cover:
|
||||
cover = open(cover, 'rb').read()
|
||||
id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
|
||||
self.number_of_books_added += 1
|
||||
self.emit(SIGNAL('pupdate(PyQt_PyObject)'), c)
|
||||
finally:
|
||||
self.disconnect(self.get_metadata,
|
||||
SIGNAL('metadataf(PyQt_PyObject, PyQt_PyObject)'),
|
||||
self.metadata_delivered)
|
||||
self.get_metadata = None
|
||||
|
||||
if id is None:
|
||||
self.duplicates.append((mi, cover, formats))
|
||||
else:
|
||||
self.add_formats(id, formats)
|
||||
else:
|
||||
self.names.append(name)
|
||||
self.paths.append(formats[0])
|
||||
self.infos.append({'title':mi.title,
|
||||
'authors':', '.join(mi.authors),
|
||||
'cover':None,
|
||||
'tags':mi.tags if mi.tags else []})
|
||||
|
||||
def process_duplicates(self):
|
||||
if self.duplicates:
|
||||
files = ''
|
||||
for mi in self.duplicates:
|
||||
title = mi[0].title
|
||||
if not isinstance(title, unicode):
|
||||
title = title.decode(preferred_encoding, 'replace')
|
||||
files += title+'\n'
|
||||
files = [x[0].title for x in self.duplicates]
|
||||
d = warning_dialog(_('Duplicates found!'),
|
||||
_('Books with the same title as the following already '
|
||||
'exist in the database. Add them anyway?'),
|
||||
files, parent=self._parent)
|
||||
'\n'.join(files), parent=self._parent)
|
||||
if d.exec_() == d.Accepted:
|
||||
for mi, formats in self.duplicates:
|
||||
self.db.import_book(mi, formats, notify=False)
|
||||
for mi, cover, formats in self.duplicates:
|
||||
id = self.db.create_book_entry(mi, cover=cover,
|
||||
add_duplicates=False)
|
||||
self.add_formats(id, formats)
|
||||
self.number_of_books_added += 1
|
||||
|
||||
|
||||
|
@ -6,96 +6,13 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os
|
||||
|
||||
from PyQt4.Qt import QWidget, QSpinBox, QDoubleSpinBox, QLineEdit, QTextEdit, \
|
||||
QCheckBox, QComboBox, Qt, QIcon, SIGNAL
|
||||
|
||||
from calibre.customize.conversion import OptionRecommendation
|
||||
from calibre.utils.config import config_dir
|
||||
from calibre.utils.lock import ExclusiveFile
|
||||
from calibre import sanitize_file_name
|
||||
|
||||
config_dir = os.path.join(config_dir, 'conversion')
|
||||
if not os.path.exists(config_dir):
|
||||
os.makedirs(config_dir)
|
||||
|
||||
def name_to_path(name):
|
||||
return os.path.join(config_dir, sanitize_file_name(name)+'.py')
|
||||
|
||||
def save_defaults(name, recs):
|
||||
path = name_to_path(name)
|
||||
raw = str(recs)
|
||||
with open(path, 'wb'):
|
||||
pass
|
||||
with ExclusiveFile(path) as f:
|
||||
f.write(raw)
|
||||
save_defaults_ = save_defaults
|
||||
|
||||
def load_defaults(name):
|
||||
path = name_to_path(name)
|
||||
if not os.path.exists(path):
|
||||
open(path, 'wb').close()
|
||||
with ExclusiveFile(path) as f:
|
||||
raw = f.read()
|
||||
r = GuiRecommendations()
|
||||
if raw:
|
||||
r.from_string(raw)
|
||||
return r
|
||||
|
||||
def save_specifics(db, book_id, recs):
|
||||
raw = str(recs)
|
||||
db.set_conversion_options(book_id, 'PIPE', raw)
|
||||
|
||||
def load_specifics(db, book_id):
|
||||
raw = db.conversion_options(book_id, 'PIPE')
|
||||
r = GuiRecommendations()
|
||||
if raw:
|
||||
r.from_string(raw)
|
||||
return r
|
||||
|
||||
class GuiRecommendations(dict):
|
||||
|
||||
def __new__(cls, *args):
|
||||
dict.__new__(cls)
|
||||
obj = super(GuiRecommendations, cls).__new__(cls, *args)
|
||||
obj.disabled_options = set([])
|
||||
return obj
|
||||
|
||||
def to_recommendations(self, level=OptionRecommendation.LOW):
|
||||
ans = []
|
||||
for key, val in self.items():
|
||||
ans.append((key, val, level))
|
||||
return ans
|
||||
|
||||
def __str__(self):
|
||||
ans = ['{']
|
||||
for key, val in self.items():
|
||||
ans.append('\t'+repr(key)+' : '+repr(val)+',')
|
||||
ans.append('}')
|
||||
return '\n'.join(ans)
|
||||
|
||||
def from_string(self, raw):
|
||||
try:
|
||||
d = eval(raw)
|
||||
except SyntaxError:
|
||||
d = None
|
||||
if d:
|
||||
self.update(d)
|
||||
|
||||
def merge_recommendations(self, get_option, level, options,
|
||||
only_existing=False):
|
||||
for name in options:
|
||||
if only_existing and name not in self:
|
||||
continue
|
||||
opt = get_option(name)
|
||||
if opt is None: continue
|
||||
if opt.level == OptionRecommendation.HIGH:
|
||||
self[name] = opt.recommended_value
|
||||
self.disabled_options.add(name)
|
||||
elif opt.level > level or name not in self:
|
||||
self[name] = opt.recommended_value
|
||||
|
||||
from calibre.ebooks.conversion.config import load_defaults, \
|
||||
save_defaults as save_defaults_, \
|
||||
load_specifics, GuiRecommendations
|
||||
|
||||
class Widget(QWidget):
|
||||
|
||||
|
@ -11,7 +11,7 @@ import sys, cPickle
|
||||
from PyQt4.Qt import QString, SIGNAL, QAbstractListModel, Qt, QVariant, QFont
|
||||
|
||||
from calibre.gui2 import ResizableDialog, NONE
|
||||
from calibre.gui2.convert import GuiRecommendations, save_specifics, \
|
||||
from calibre.ebooks.conversion.config import GuiRecommendations, save_specifics, \
|
||||
load_specifics
|
||||
from calibre.gui2.convert.single_ui import Ui_Dialog
|
||||
from calibre.gui2.convert.metadata import MetadataWidget
|
||||
|
@ -34,18 +34,18 @@ class DeviceJob(BaseJob):
|
||||
BaseJob.__init__(self, description, done=done)
|
||||
self.func = func
|
||||
self.args, self.kwargs = args, kwargs
|
||||
self.exception = None
|
||||
self.job_manager = job_manager
|
||||
self.job_manager.add_job(self)
|
||||
self.details = _('No details available.')
|
||||
self._details = _('No details available.')
|
||||
|
||||
def start_work(self):
|
||||
self.start_time = time.time()
|
||||
self.job_manager.changed_queue.put(self)
|
||||
|
||||
def job_done(self):
|
||||
self.duration = time.time() - self.start_time()
|
||||
self.duration = time.time() - self.start_time
|
||||
self.percent = 1
|
||||
self.job_manager.changed_queue.put(self)
|
||||
self.job_manager.job_done(self)
|
||||
|
||||
def report_progress(self, percent, msg=''):
|
||||
self.notifications.put((percent, msg))
|
||||
@ -57,7 +57,7 @@ class DeviceJob(BaseJob):
|
||||
self.result = self.func(*self.args, **self.kwargs)
|
||||
except (Exception, SystemExit), err:
|
||||
self.failed = True
|
||||
self.details = unicode(err) + '\n\n' + \
|
||||
self._details = unicode(err) + '\n\n' + \
|
||||
traceback.format_exc()
|
||||
self.exception = err
|
||||
finally:
|
||||
@ -65,7 +65,7 @@ class DeviceJob(BaseJob):
|
||||
|
||||
@property
|
||||
def log_file(self):
|
||||
return cStringIO.StringIO(self.details.encode('utf-8'))
|
||||
return cStringIO.StringIO(self._details.encode('utf-8'))
|
||||
|
||||
|
||||
class DeviceManager(Thread):
|
||||
@ -230,7 +230,6 @@ class DeviceManager(Thread):
|
||||
|
||||
def _view_book(self, path, target):
|
||||
f = open(target, 'wb')
|
||||
print self.device
|
||||
self.device.get_file(path, f)
|
||||
f.close()
|
||||
return target
|
||||
@ -379,12 +378,12 @@ class DeviceMenu(QMenu):
|
||||
if action.dest == 'main:':
|
||||
action.setEnabled(True)
|
||||
elif action.dest == 'carda:0':
|
||||
if card_prefix[0] != None:
|
||||
if card_prefix and card_prefix[0] != None:
|
||||
action.setEnabled(True)
|
||||
else:
|
||||
action.setEnabled(False)
|
||||
elif action.dest == 'cardb:0':
|
||||
if card_prefix[1] != None:
|
||||
if card_prefix and card_prefix[1] != None:
|
||||
action.setEnabled(True)
|
||||
else:
|
||||
action.setEnabled(False)
|
||||
@ -737,7 +736,7 @@ class DeviceGUI(object):
|
||||
'''
|
||||
Called once metadata has been uploaded.
|
||||
'''
|
||||
if job.exception is not None:
|
||||
if job.failed:
|
||||
self.device_job_exception(job)
|
||||
return
|
||||
cp, fs = job.result
|
||||
|
@ -31,6 +31,16 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
||||
def set_value(self, val):
|
||||
self.bar.setValue(val)
|
||||
|
||||
@dynamic_property
|
||||
def value(self):
|
||||
def fset(self, val):
|
||||
return self.bar.setValue(val)
|
||||
def fget(self):
|
||||
return self.bar.value()
|
||||
return property(fget=fget, fset=fset)
|
||||
|
||||
|
||||
|
||||
def set_min(self, min):
|
||||
self.bar.setMinimum(min)
|
||||
|
||||
@ -41,6 +51,7 @@ class ProgressDialog(QDialog, Ui_Dialog):
|
||||
self.canceled = True
|
||||
self.button_box.setDisabled(True)
|
||||
self.title.setText(_('Aborting...'))
|
||||
self.emit(SIGNAL('canceled()'))
|
||||
|
||||
def keyPressEvent(self, ev):
|
||||
if ev.key() == Qt.Key_Escape:
|
||||
|
@ -31,8 +31,7 @@ class JobManager(QAbstractTableModel):
|
||||
|
||||
self.jobs = []
|
||||
self.add_job = Dispatcher(self._add_job)
|
||||
self.job_done = Dispatcher(self._job_done)
|
||||
self.server = Server(self.job_done)
|
||||
self.server = Server()
|
||||
self.changed_queue = Queue()
|
||||
|
||||
self.timer = QTimer(self)
|
||||
@ -98,7 +97,8 @@ class JobManager(QAbstractTableModel):
|
||||
try:
|
||||
self._update()
|
||||
except BaseException:
|
||||
pass
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
def _update(self):
|
||||
# Update running time
|
||||
@ -132,6 +132,8 @@ class JobManager(QAbstractTableModel):
|
||||
if needs_reset:
|
||||
self.jobs.sort()
|
||||
self.reset()
|
||||
if job.is_finished:
|
||||
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
|
||||
else:
|
||||
for job in jobs:
|
||||
idx = self.jobs.index(job)
|
||||
@ -155,12 +157,6 @@ class JobManager(QAbstractTableModel):
|
||||
def row_to_job(self, row):
|
||||
return self.jobs[row]
|
||||
|
||||
def _job_done(self, job):
|
||||
self.emit(SIGNAL('layoutAboutToBeChanged()'))
|
||||
self.jobs.sort()
|
||||
self.emit(SIGNAL('job_done(int)'), len(self.unfinished_jobs()))
|
||||
self.emit(SIGNAL('layoutChanged()'))
|
||||
|
||||
def has_device_jobs(self):
|
||||
for job in self.jobs:
|
||||
if job.is_running and isinstance(job, DeviceJob):
|
||||
|
@ -676,21 +676,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
'Select root folder')
|
||||
if not root:
|
||||
return
|
||||
from calibre.gui2.add import AddRecursive
|
||||
self._add_recursive_thread = AddRecursive(root,
|
||||
self.library_view.model().db, self.get_metadata,
|
||||
single, self)
|
||||
self.connect(self._add_recursive_thread, SIGNAL('finished()'),
|
||||
self._recursive_files_added)
|
||||
self._add_recursive_thread.start()
|
||||
|
||||
def _recursive_files_added(self):
|
||||
self._add_recursive_thread.process_duplicates()
|
||||
if self._add_recursive_thread.number_of_books_added > 0:
|
||||
self.library_view.model().resort(reset=False)
|
||||
self.library_view.model().research()
|
||||
self.library_view.model().count_changed()
|
||||
self._add_recursive_thread = None
|
||||
from calibre.gui2.add import Adder
|
||||
self._adder = Adder(self,
|
||||
self.library_view.model().db,
|
||||
Dispatcher(self._files_added))
|
||||
self._adder.add_recursive(root, single)
|
||||
|
||||
def add_recursive_single(self, checked):
|
||||
'''
|
||||
@ -731,10 +721,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
(_('LRF Books'), ['lrf']),
|
||||
(_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']),
|
||||
(_('LIT Books'), ['lit']),
|
||||
(_('MOBI Books'), ['mobi', 'prc']),
|
||||
(_('MOBI Books'), ['mobi', 'prc', 'azw']),
|
||||
(_('Text books'), ['txt', 'rtf']),
|
||||
(_('PDF Books'), ['pdf']),
|
||||
(_('Comics'), ['cbz', 'cbr']),
|
||||
(_('Comics'), ['cbz', 'cbr', 'cbc']),
|
||||
(_('Archives'), ['zip', 'rar']),
|
||||
])
|
||||
if not books:
|
||||
@ -745,40 +735,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
|
||||
def _add_books(self, paths, to_device, on_card=None):
|
||||
if on_card is None:
|
||||
on_card = self.stack.currentIndex() == 2
|
||||
on_card = self.stack.currentIndex() >= 2
|
||||
if not paths:
|
||||
return
|
||||
from calibre.gui2.add import AddFiles
|
||||
self._add_files_thread = AddFiles(paths, self.default_thumbnail,
|
||||
self.get_metadata,
|
||||
None if to_device else \
|
||||
self.library_view.model().db
|
||||
)
|
||||
self._add_files_thread.send_to_device = to_device
|
||||
self._add_files_thread.on_card = on_card
|
||||
self._add_files_thread.create_progress_dialog(_('Adding books...'),
|
||||
_('Reading metadata...'), self)
|
||||
self.connect(self._add_files_thread, SIGNAL('finished()'),
|
||||
self._files_added)
|
||||
self._add_files_thread.start()
|
||||
from calibre.gui2.add import Adder
|
||||
self._adder = Adder(self,
|
||||
None if to_device else self.library_view.model().db,
|
||||
Dispatcher(partial(self._files_added, on_card=on_card)))
|
||||
self._adder.add(paths)
|
||||
|
||||
def _files_added(self):
|
||||
t = self._add_files_thread
|
||||
self._add_files_thread = None
|
||||
if not t.canceled:
|
||||
if t.send_to_device:
|
||||
self.upload_books(t.paths,
|
||||
list(map(sanitize_file_name, t.names)),
|
||||
t.infos, on_card=t.on_card)
|
||||
def _files_added(self, paths=[], names=[], infos=[], on_card=False):
|
||||
if paths:
|
||||
self.upload_books(paths,
|
||||
list(map(sanitize_file_name, names)),
|
||||
infos, on_card=on_card)
|
||||
self.status_bar.showMessage(
|
||||
_('Uploading books to device.'), 2000)
|
||||
else:
|
||||
t.process_duplicates()
|
||||
if t.number_of_books_added > 0:
|
||||
self.library_view.model().books_added(t.number_of_books_added)
|
||||
if self._adder.number_of_books_added > 0:
|
||||
self.library_view.model().books_added(self._adder.number_of_books_added)
|
||||
if hasattr(self, 'db_images'):
|
||||
self.db_images.reset()
|
||||
|
||||
self._adder = None
|
||||
|
||||
|
||||
############################################################################
|
||||
|
||||
@ -1401,7 +1380,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
except:
|
||||
pass
|
||||
if not self.device_error_dialog.isVisible():
|
||||
self.device_error_dialog.set_message(job.details)
|
||||
self.device_error_dialog.setDetailedText(job.details)
|
||||
self.device_error_dialog.show()
|
||||
|
||||
def job_exception(self, job):
|
||||
@ -1525,8 +1504,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
||||
if self.job_manager.has_device_jobs():
|
||||
msg = '<p>'+__appname__ + \
|
||||
_(''' is communicating with the device!<br>
|
||||
'Quitting may cause corruption on the device.<br>
|
||||
'Are you sure you want to quit?''')+'</p>'
|
||||
Quitting may cause corruption on the device.<br>
|
||||
Are you sure you want to quit?''')+'</p>'
|
||||
|
||||
d = QMessageBox(QMessageBox.Warning, _('WARNING: Active jobs'), msg,
|
||||
QMessageBox.Yes|QMessageBox.No, self)
|
||||
|
@ -8,6 +8,7 @@ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
|
||||
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
|
||||
from calibre.utils.config import OptionParser
|
||||
from calibre.gui2 import error_dialog
|
||||
from calibre import prints
|
||||
|
||||
def option_parser(usage='''\
|
||||
Usage: %prog [options]
|
||||
@ -79,8 +80,8 @@ class MainWindow(QMainWindow):
|
||||
sio = StringIO.StringIO()
|
||||
traceback.print_exception(type, value, tb, file=sio)
|
||||
fe = sio.getvalue()
|
||||
print >>sys.stderr, fe
|
||||
msg = unicode(str(value), 'utf8', 'replace')
|
||||
prints(fe, file=sys.stderr)
|
||||
msg = '<b>%s</b>:'%type.__name__ + unicode(str(value), 'utf8', 'replace')
|
||||
error_dialog(self, _('ERROR: Unhandled exception'), msg, det_msg=fe,
|
||||
show=True)
|
||||
except:
|
||||
|
@ -285,6 +285,7 @@ class JobsView(TableView):
|
||||
job = self.model().row_to_job(row)
|
||||
d = DetailView(self, job)
|
||||
d.exec_()
|
||||
d.timer.stop()
|
||||
|
||||
|
||||
class FontFamilyModel(QAbstractListModel):
|
||||
|
@ -1183,6 +1183,28 @@ class LibraryDatabase2(LibraryDatabase):
|
||||
path = path_or_stream
|
||||
return run_plugins_on_import(path, format)
|
||||
|
||||
def create_book_entry(self, mi, cover=None, add_duplicates=True):
|
||||
if not add_duplicates and self.has_book(mi):
|
||||
return None
|
||||
series_index = 1 if mi.series_index is None else mi.series_index
|
||||
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
||||
title = mi.title
|
||||
if isinstance(aus, str):
|
||||
aus = aus.decode(preferred_encoding, 'replace')
|
||||
if isinstance(title, str):
|
||||
title = title.decode(preferred_encoding)
|
||||
obj = self.conn.execute('INSERT INTO books(title, series_index, author_sort) VALUES (?, ?, ?)',
|
||||
(title, series_index, aus))
|
||||
id = obj.lastrowid
|
||||
self.data.books_added([id], self.conn)
|
||||
self.set_path(id, True)
|
||||
self.conn.commit()
|
||||
self.set_metadata(id, mi)
|
||||
if cover:
|
||||
self.set_cover(id, cover)
|
||||
return id
|
||||
|
||||
|
||||
def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True):
|
||||
'''
|
||||
Add a book to the database. The result cache is not updated.
|
||||
|
@ -4,9 +4,10 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
import sys, os, inspect, re
|
||||
from sphinx.builder import StandaloneHTMLBuilder, bold
|
||||
from sphinx.builder import StandaloneHTMLBuilder
|
||||
from sphinx.util import rpartition
|
||||
from sphinx.ext.autodoc import get_module_charset, prepare_docstring
|
||||
from sphinx.util.console import bold
|
||||
from sphinx.ext.autodoc import prepare_docstring
|
||||
from docutils.statemachine import ViewList
|
||||
from docutils import nodes
|
||||
|
||||
@ -181,7 +182,7 @@ def auto_member(dirname, arguments, options, content, lineno,
|
||||
docstring = '\n'.join(comment_lines)
|
||||
|
||||
if module is not None and docstring is not None:
|
||||
docstring = docstring.decode(get_module_charset(mod))
|
||||
docstring = docstring.decode('utf-8')
|
||||
|
||||
result = ViewList()
|
||||
result.append('.. attribute:: %s.%s'%(cls.__name__, obj), '<autodoc>')
|
||||
|
@ -17,39 +17,11 @@ E-book Format Conversion
|
||||
|
||||
What formats does |app| support conversion to/from?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|app| supports the conversion of the following formats:
|
||||
|app| supports the conversion of many input formats to many output formats.
|
||||
It can convert every input format in the following list, to every output format.
|
||||
|
||||
+----------------------------+------------------------------------------------------------------+
|
||||
| | **Output formats** |
|
||||
| +------------------+-----------------------+-----------------------+
|
||||
| | EPUB | LRF | MOBI |
|
||||
+===================+========+==================+=======================+=======================+
|
||||
| | MOBI | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | LIT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | PRC** | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | EPUB | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | ODT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | FB2 | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | HTML | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| **Input formats** | CBR | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | CBZ | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | RTF | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | TXT | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | PDF | ✔ | ✔ | ✔ |
|
||||
| | | | | |
|
||||
| | LRS | | ✔ | |
|
||||
+-------------------+--------+------------------+-----------------------+-----------------------+
|
||||
*Input Formats:* CBZ, CBR, CBC, EPUB, FB2, HTML, LIT, MOBI, ODT, PDF, PRC**, RTF, TXT
|
||||
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, PDB, PDF, TXT
|
||||
|
||||
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
|
||||
|
||||
@ -64,7 +36,7 @@ The PDF conversion tries to extract the text and images from the PDF file and co
|
||||
are also represented as vector diagrams, thus they cannot be extracted.
|
||||
|
||||
How do I convert a collection of HTML files in a specific order?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
In order to convert a collection of HTML files in a specific oder, you have to create a table of contents file. That is, another HTML file that contains links to all the other files in the desired order. Such a file looks like::
|
||||
|
||||
<html>
|
||||
|
@ -257,38 +257,26 @@ The final new feature is the :meth:`calibre.web.feeds.news.BasicNewsRecipe.prepr
|
||||
Tips for developing new recipes
|
||||
---------------------------------
|
||||
|
||||
The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.py`. You can download content using this recipe with the command::
|
||||
The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.recipe`. The `.recipe` extension is required. You can download content using this recipe with the command::
|
||||
|
||||
feeds2disk --debug --test myrecipe.py
|
||||
ebook-convert myrecipe.recipe output_dir --test -vv
|
||||
|
||||
The :command:`feeds2disk` will download all the webpages and save them to the current directory. The :option:`--debug` makes feeds2disk spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds.
|
||||
The :command:`ebook-convert` will download all the webpages and save them to the directory :file:`output_dir`, creating it if necessary. The :option:`-vv` makes ebook-convert spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds.
|
||||
|
||||
Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate an LRF ebook with the command::
|
||||
Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate ebooks in different formats as shown below::
|
||||
|
||||
html2lrf --use-spine --page-break-before "$" index.html
|
||||
|
||||
If the generated :term:`LRF` looks good, you can finally, run::
|
||||
|
||||
feeds2lrf myrecipe.py
|
||||
|
||||
to see the final :term:`LRF` format e-book generated from your recipe. If you're satisfied with your recipe, consider attaching it to `the wiki <http://calibre.kovidgoyal.net/wiki/UserRecipes>`_, so that others can use it as well. If you feel there is enough demand to justify its inclusion into the set of built-in recipes, add a comment to the ticket http://calibre.kovidgoyal.net/ticket/405
|
||||
ebook-convert myrecipe.recipe myrecipe.epub
|
||||
ebook-convert myrecipe.recipe myrecipe.mobi
|
||||
...
|
||||
|
||||
|
||||
If you just want to quickly test a couple of feeds, you can use the :option:`--feeds` option::
|
||||
|
||||
feeds2disk --feeds "['http://feeds.newsweek.com/newsweek/TopNews', 'http://feeds.newsweek.com/headlines/politics']"
|
||||
If you're satisfied with your recipe, consider attaching it to `the wiki <http://calibre.kovidgoyal.net/wiki/UserRecipes>`_, so that others can use it as well. If you feel there is enough demand to justify its inclusion into the set of built-in recipes, add a comment to the ticket http://calibre.kovidgoyal.net/ticket/405
|
||||
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`feeds2disk`
|
||||
The command line interfce for downloading content from the internet
|
||||
|
||||
:ref:`feeds2lrf`
|
||||
The command line interface for downloading content fro the internet and converting it into a :term:`LRF` e-book.
|
||||
|
||||
:ref:`html2lrf`
|
||||
The command line interface for converting :term:`HTML` into a :term:`LRF` e-book.
|
||||
:ref:`ebook-convert`
|
||||
The command line interface for all ebook conversion.
|
||||
|
||||
|
||||
Further reading
|
||||
@ -305,16 +293,6 @@ To learn more about writing advanced recipes using some of the facilities, avail
|
||||
`Built-in recipes <http://bazaar.launchpad.net/~kovid/calibre/trunk/files/head:/src/calibre/web/feeds/recipes/>`_
|
||||
The source code for the built-in recipes that come with |app|
|
||||
|
||||
Migrating old style profiles to recipes
|
||||
----------------------------------------
|
||||
|
||||
In earlier versions of |app| there was a similar, if less powerful, framework for fetching news based on *Profiles*. If you have a profile that you would like to migrate to a recipe, the basic technique is simple, as they are very similar (on the surface). Common changes you have to make include:
|
||||
|
||||
* Replace ``DefaultProfile`` with ``BasicNewsRecipe``
|
||||
|
||||
* Remove ``max_recursions``
|
||||
|
||||
* If the server you're downloading from doesn't like multiple connects, set ``simultaneous_downloads = 1``.
|
||||
|
||||
API documentation
|
||||
--------------------
|
||||
|
@ -54,7 +54,7 @@ Customizing e-book download
|
||||
|
||||
.. automember:: BasicNewsRecipe.timefmt
|
||||
|
||||
.. automember:: basicNewsRecipe.conversion_options
|
||||
.. automember:: BasicNewsRecipe.conversion_options
|
||||
|
||||
.. automember:: BasicNewsRecipe.feeds
|
||||
|
||||
|
@ -42,7 +42,7 @@ class BaseJob(object):
|
||||
def update(self):
|
||||
if self.duration is not None:
|
||||
self._run_state = self.FINISHED
|
||||
self.percent = 1
|
||||
self.percent = 100
|
||||
if self.killed:
|
||||
self._status_text = _('Stopped')
|
||||
else:
|
||||
|
@ -25,6 +25,9 @@ PARALLEL_FUNCS = {
|
||||
|
||||
'gui_convert' :
|
||||
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
|
||||
|
||||
'read_metadata' :
|
||||
('calibre.ebooks.metadata.worker', 'read_metadata_', 'notification'),
|
||||
}
|
||||
|
||||
class Progress(Thread):
|
||||
|
Loading…
x
Reference in New Issue
Block a user