Various minor fixes. WARNING: Adding of books is currently broken.

This commit is contained in:
Kovid Goyal 2009-05-14 11:26:37 -07:00
parent 2e0ad5d1e0
commit 1a117fd070
26 changed files with 669 additions and 657 deletions

View File

@ -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()

View File

@ -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))

View File

@ -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.
'''

View File

@ -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):

View File

@ -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))

View File

@ -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

View 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

View File

@ -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', '')

View File

@ -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:

View 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

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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):

View File

@ -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)

View File

@ -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:

View File

@ -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):

View File

@ -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.

View File

@ -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>')

View File

@ -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>

View File

@ -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
--------------------

View File

@ -54,7 +54,7 @@ Customizing e-book download
.. automember:: BasicNewsRecipe.timefmt
.. automember:: basicNewsRecipe.conversion_options
.. automember:: BasicNewsRecipe.conversion_options
.. automember:: BasicNewsRecipe.feeds

View File

@ -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:

View File

@ -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):