Initial implementation of new adding logic

This commit is contained in:
Kovid Goyal 2014-11-12 12:19:26 +05:30
parent 5d1adac683
commit bba5cbf11e
5 changed files with 513 additions and 264 deletions

View File

@ -6,178 +6,62 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from threading import Thread import os, shutil, errno
from Queue import Empty
import os, time, sys, shutil
from calibre.utils.ipc.job import ParallelJob from calibre.customize.ui import run_plugins_on_import
from calibre.utils.ipc.server import Server from calibre.db.utils import find_identical_books
from calibre.ptempfile import PersistentTemporaryDirectory, TemporaryDirectory from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre import prints from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.constants import filesystem_encoding from calibre.utils.filenames import samefile
def debug(*args): def serialize_metadata_for(paths, tdir, group_id):
prints(*args) mi = metadata_from_formats(paths)
sys.stdout.flush()
def serialize_metadata_for(formats, tdir, id_):
from calibre.ebooks.metadata.meta import metadata_from_formats
from calibre.ebooks.metadata.opf2 import metadata_to_opf
mi = metadata_from_formats(formats)
mi.cover = None mi.cover = None
cdata = None cdata = None
if mi.cover_data: if mi.cover_data:
cdata = mi.cover_data[-1] cdata = mi.cover_data[-1]
mi.cover_data = None mi.cover_data = (None, None)
if not mi.application_id: if not mi.application_id:
mi.application_id = '__calibre_dummy__' mi.application_id = '__calibre_dummy__'
with open(os.path.join(tdir, '%s.opf'%id_), 'wb') as f: opf = metadata_to_opf(mi, default_lang='und')
f.write(metadata_to_opf(mi, default_lang='und')) has_cover = False
if cdata: if cdata:
with open(os.path.join(tdir, str(id_)), 'wb') as f: with open(os.path.join(tdir, '%s.cdata' % group_id), 'wb') as f:
f.write(cdata) f.write(cdata)
has_cover = True
return mi, opf, has_cover
def read_metadata_(task, tdir, notification=lambda x,y:x): def run_import_plugins(paths, group_id, tdir):
with TemporaryDirectory() as mdir: final_paths = []
do_read_metadata(task, tdir, mdir, notification) for path in paths:
if not os.access(path, os.R_OK):
def do_read_metadata(task, tdir, mdir, notification):
from calibre.customize.ui import run_plugins_on_import
for x in task:
try:
id_, formats = x
except:
continue continue
try: nfp = run_plugins_on_import(path)
if isinstance(formats, basestring): if nfp and os.access(nfp, os.R_OK) and not samefile(nfp, path):
formats = [formats] # Ensure that the filename is preserved so that
import_map = {} # reading metadata from filename is not broken
fmts, metadata_fmts = [], [] name = os.path.splitext(os.path.basename(path))[0]
for format in formats: ext = os.path.splitext(nfp)[1]
mfmt = format path = os.path.join(tdir, '%s' % group_id, name + ext)
name, ext = os.path.splitext(os.path.basename(format)) try:
nfp = run_plugins_on_import(format) os.mkdir(os.path.dirname(path))
if not nfp or nfp == format or not os.access(nfp, os.R_OK): except EnvironmentError as err:
nfp = None if err.errno != errno.EEXIST:
else: raise
# Ensure that the filename is preserved so that try:
# reading metadata from filename is not broken os.rename(nfp, path)
nfp = os.path.abspath(nfp) except EnvironmentError:
nfext = os.path.splitext(nfp)[1] shutil.copyfile(nfp, path)
mfmt = os.path.join(mdir, name + nfext) final_paths.append(path)
shutil.copyfile(nfp, mfmt) return final_paths
metadata_fmts.append(mfmt)
fmts.append(nfp)
serialize_metadata_for(metadata_fmts, tdir, id_) def read_metadata(paths, group_id, tdir, common_data=None):
paths = run_import_plugins(paths, group_id, tdir)
for format, nfp in zip(formats, fmts): mi, opf, has_cover = serialize_metadata_for(paths, tdir, group_id)
if not nfp: duplicate_info = None
continue if common_data is not None:
if isinstance(nfp, unicode): if isinstance(common_data, (set, frozenset)):
nfp.encode(filesystem_encoding) duplicate_info = mi.title and icu_lower(mi.title) in common_data
x = lambda j : os.path.abspath(os.path.normpath(os.path.normcase(j))) else:
if x(nfp) != x(format) and os.access(nfp, os.R_OK|os.W_OK): duplicate_info = find_identical_books(mi, common_data)
fmt = os.path.splitext(format)[1].replace('.', '').lower() return paths, opf, has_cover, duplicate_info
nfmt = os.path.splitext(nfp)[1].replace('.', '').lower()
dest = os.path.join(tdir, '%s.%s'%(id_, nfmt))
shutil.copyfile(nfp, dest)
import_map[fmt] = dest
if import_map:
with open(os.path.join(tdir, str(id_)+'.import'), 'wb') as f:
for fmt, nfp in import_map.items():
f.write(fmt+':'+nfp+'\n')
notification(0.5, id_)
except:
import traceback
with open(os.path.join(tdir, '%s.error'%id_), 'wb') as f:
f.write(traceback.format_exc())
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
res = os.path.join(self.tdir, '%s.error'%id)
if not os.path.exists(res):
res = res.replace('.error', '.opf')
self.result_queue.put((id, res, cover))
class ReadMetadata(Thread):
def __init__(self, tasks, result_queue, spare_server=None):
self.tasks, self.result_queue = tasks, result_queue
self.spare_server = spare_server
self.canceled = False
Thread.__init__(self)
self.daemon = True
self.failure_details = {}
self.tdir = PersistentTemporaryDirectory('_rm_worker')
def run(self):
jobs, ids = set([]), set([])
for t in self.tasks:
for b in t:
ids.add(b[0])
progress = Progress(self.result_queue, self.tdir)
server = Server() if self.spare_server is None else self.spare_server
try:
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]
if id in ids:
progress(id)
ids.remove(id)
except Empty:
break
job.update(consume_notifications=False)
if not job.is_finished:
running = True
if not running:
break
finally:
server.close()
time.sleep(1)
if self.canceled:
return
for id in ids:
progress(id)
for job in jobs:
if job.failed:
prints(job.details)
if os.path.exists(job.log_path):
try:
os.remove(job.log_path)
except:
pass
def read_metadata(paths, result_queue, chunk=50, spare_server=None):
tasks = []
pos = 0
while pos < len(paths):
tasks.append(paths[pos:pos+chunk])
pos += chunk
t = ReadMetadata(tasks, result_queue, spare_server=spare_server)
t.start()
return t

View File

@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from collections import defaultdict
from PyQt5.Qt import QPixmap, QTimer from PyQt5.Qt import QPixmap, QTimer
@ -20,8 +19,6 @@ from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.gui2.widgets import IMAGE_EXTENSIONS from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.filenames import ascii_filename from calibre.utils.filenames import ascii_filename
from calibre.utils.icu import sort_key
from calibre.constants import filesystem_encoding
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.gui2 import question_dialog from calibre.gui2 import question_dialog
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
@ -154,15 +151,15 @@ class AddAction(InterfaceAction):
_('Select root folder')) _('Select root folder'))
if not root: if not root:
return return
lp = os.path.normcase(os.path.abspath(self.gui.current_db.library_path))
if lp.startswith(os.path.normcase(os.path.abspath(root)) + os.pathsep):
return error_dialog(self.gui, _('Cannot add'), _(
'Cannot add books from the folder: %s as it contains the currently opened calibre library') % root, show=True)
self.do_add_recursive(root, single) self.do_add_recursive(root, single)
def do_add_recursive(self, root, single): def do_add_recursive(self, root, single):
from calibre.gui2.add import Adder from calibre.gui2.add2 import Adder
self._adder = Adder(self.gui, Adder(root, single_book_per_directory=single, db=self.gui.current_db, callback=self._files_added, parent=self.gui)
self.gui.library_view.model().db,
self.Dispatcher(self._files_added), spare_server=self.gui.spare_server)
self.gui.tags_view.disable_recounting = True
self._adder.add_recursive(root, single)
def add_recursive_single(self, *args): def add_recursive_single(self, *args):
''' '''
@ -364,75 +361,58 @@ class AddAction(InterfaceAction):
'cardb' if self.gui.stack.currentIndex() == 3 else None 'cardb' if self.gui.stack.currentIndex() == 3 else None
if not paths: if not paths:
return return
from calibre.gui2.add import Adder from calibre.gui2.add2 import Adder
self.__adder_func = partial(self._files_added, on_card=on_card) Adder(paths, db=None if to_device else self.gui.current_db, parent=self.gui, callback=partial(self._files_added, on_card=on_card))
self._adder = Adder(self.gui,
None if to_device else self.gui.library_view.model().db,
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
self.gui.tags_view.disable_recounting = True
self._adder.add(paths)
def _files_added(self, paths=[], names=[], infos=[], on_card=None): def _files_added(self, adder, on_card=None):
self.gui.tags_view.disable_recounting = False if adder.items:
if paths: paths, infos, names = [], [], []
self.gui.upload_books(paths, for mi, cover_path, format_paths in adder.items:
list(map(ascii_filename, names)), mi.cover = cover_path
infos, on_card=on_card) paths.append(format_paths[0]), infos.append(mi)
names.append(ascii_filename(os.path.basename(paths[-1])))
self.gui.upload_books(paths, names, infos, on_card=on_card)
self.gui.status_bar.show_message( self.gui.status_bar.show_message(
_('Uploading books to device.'), 2000) _('Uploading books to device.'), 2000)
if getattr(self._adder, 'number_of_books_added', 0) > 0: return
self.gui.library_view.model().books_added(self._adder.number_of_books_added)
if adder.number_of_books_added > 0:
self.gui.library_view.model().books_added(adder.number_of_books_added)
self.gui.library_view.set_current_row(0) self.gui.library_view.set_current_row(0)
if hasattr(self.gui, 'db_images'): if hasattr(self.gui, 'db_images'):
self.gui.db_images.beginResetModel(), self.gui.db_images.endResetModel() self.gui.db_images.beginResetModel(), self.gui.db_images.endResetModel()
self.gui.tags_view.recount() self.gui.tags_view.recount()
if getattr(self._adder, 'merged_books', False): # if getattr(self._adder, 'merged_books', False):
merged = defaultdict(list) # merged = defaultdict(list)
for title, author in self._adder.merged_books: # for title, author in self._adder.merged_books:
merged[author].append(title) # merged[author].append(title)
lines = [] # lines = []
for author in sorted(merged, key=sort_key): # for author in sorted(merged, key=sort_key):
lines.append(author) # lines.append(author)
for title in sorted(merged[author], key=sort_key): # for title in sorted(merged[author], key=sort_key):
lines.append('\t' + title) # lines.append('\t' + title)
lines.append('') # lines.append('')
info_dialog(self.gui, _('Merged some books'), # info_dialog(self.gui, _('Merged some books'),
_('The following %d duplicate books were found and incoming ' # _('The following %d duplicate books were found and incoming '
'book formats were processed and merged into your ' # 'book formats were processed and merged into your '
'Calibre database according to your automerge ' # 'Calibre database according to your automerge '
'settings:')%len(self._adder.merged_books), # 'settings:')%len(self._adder.merged_books),
det_msg='\n'.join(lines), show=True) # det_msg='\n'.join(lines), show=True)
#
if getattr(self._adder, 'number_of_books_added', 0) > 0 or \ # if getattr(self._adder, 'number_of_books_added', 0) > 0 or \
getattr(self._adder, 'merged_books', False): # getattr(self._adder, 'merged_books', False):
# The formats of the current book could have changed if # # The formats of the current book could have changed if
# automerge is enabled # # automerge is enabled
current_idx = self.gui.library_view.currentIndex() # current_idx = self.gui.library_view.currentIndex()
if current_idx.isValid(): # if current_idx.isValid():
self.gui.library_view.model().current_changed(current_idx, # self.gui.library_view.model().current_changed(current_idx,
current_idx) # current_idx)
#
if getattr(self._adder, 'critical', None): def _add_from_device_adder(self, adder, on_card=None, model=None):
det_msg = [] self._files_added(adder, on_card=on_card)
for name, log in self._adder.critical.items():
if isinstance(name, str):
name = name.decode(filesystem_encoding, 'replace')
det_msg.append(name+'\n'+log)
warning_dialog(self.gui, _('Failed to read metadata'),
_('Failed to read metadata from the following')+':',
det_msg='\n\n'.join(det_msg), show=True)
if hasattr(self._adder, 'cleanup'):
self._adder.cleanup()
self._adder.setParent(None)
del self._adder
self._adder = None
def _add_from_device_adder(self, paths=[], names=[], infos=[],
on_card=None, model=None):
self._files_added(paths, names, infos, on_card=on_card)
# set the in-library flags, and as a consequence send the library's # set the in-library flags, and as a consequence send the library's
# metadata for this book to the device. This sets the uuid to the # metadata for this book to the device. This sets the uuid to the
# correct value. Note that set_books_in_library might sync_booklists # correct value. Note that set_books_in_library might sync_booklists
@ -503,12 +483,6 @@ class AddAction(InterfaceAction):
show=True) show=True)
if ok_paths: if ok_paths:
from calibre.gui2.add import Adder from calibre.gui2.add2 import Adder
self.__adder_func = partial(self._add_from_device_adder, on_card=None, callback = partial(self._add_from_device_adder, on_card=None, model=view.model())
model=view.model()) Adder(ok_paths, db=None, parent=self.gui, callback=callback)
self._adder = Adder(self.gui, self.gui.library_view.model().db,
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
self._adder.add(ok_paths)

388
src/calibre/gui2/add2.py Normal file
View File

@ -0,0 +1,388 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil, os, weakref, traceback
from threading import Thread
from collections import OrderedDict
from Queue import Empty
from io import BytesIO
from PyQt5.Qt import QObject, Qt, pyqtSignal
from calibre import prints
from calibre.customize.ui import run_plugins_on_postimport
from calibre.db.adding import find_books_in_directory
from calibre.ebooks.metadata.book.base import Metadata
from calibre.ebooks.metadata.opf2 import OPF
from calibre.gui2 import error_dialog, warning_dialog
from calibre.gui2.dialogs.duplicates import DuplicatesQuestion
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils import join_with_timeout
from calibre.utils.config import prefs
from calibre.utils.ipc.pool import Pool, Failure
def validate_source(source, parent=None): # {{{
if isinstance(source, basestring):
if not os.path.exists(source):
error_dialog(parent, _('Cannot add books'), _(
'The path %s does not exist') % source, show=True)
return False
if os.path.isdir(source):
if not os.access(source, os.X_OK|os.R_OK):
error_dialog(parent, _('Cannot add books'), _(
'You do not have permission to read %s') % source, show=True)
return False
else:
if not os.access(source, os.R_OK):
error_dialog(parent, _('Cannot add books'), _(
'You do not have permission to read %s') % source, show=True)
return False
if not source.lower().rpartition(os.extsep) in {'zip', 'rar'}:
error_dialog(parent, _('Cannot add books'), _(
'The file %s is not a recognized archive format') % source, show=True)
return False
return True
# }}}
class Adder(QObject):
do_one_signal = pyqtSignal()
def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None):
if not validate_source(source, parent):
raise ValueError('Bad source')
QObject.__init__(self, parent)
self.single_book_per_directory = single_book_per_directory
self.callback = callback
self.add_formats_to_existing = prefs['add_formats_to_existing']
self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
self.tdir = PersistentTemporaryDirectory('_add_books')
self.pool = None
self.pd = ProgressDialog(_('Adding books...'), _('Scanning for files...'), min=0, max=0, parent=parent, icon='add_book.png')
self.db = getattr(db, 'new_api', None)
if self.db is not None:
self.dbref = weakref.ref(db)
self.source = source
self.tdir = PersistentTemporaryDirectory('_add_books')
self.scan_error = None
self.file_groups = OrderedDict()
self.abort_scan = False
self.duplicates = []
self.report = []
self.items = []
self.added_book_ids = set()
self.added_duplicate_info = ({}, {}, {}) if self.add_formats_to_existing else set()
self.pd.show()
self.scan_thread = Thread(target=self.scan, name='ScanBooks')
self.scan_thread.daemon = True
self.scan_thread.start()
self.do_one = self.monitor_scan
self.do_one_signal.emit()
def break_cycles(self):
self.abort_scan = True
self.pd.close()
self.pd.deleteLater()
shutil.rmtree(self.tdir, ignore_errors=True)
if self.pool is not None:
self.pool.shutdown()
if not self.items:
shutil.rmtree(self.tdir, ignore_errors=True)
self.setParent(None)
self.added_duplicate_info = self.pool = self.items = self.duplicates = self.pd = self.db = self.dbref = self.tdir = self.file_groups = self.scan_thread = None # noqa
self.deleteLater()
def tick(self):
if self.pd.canceled:
try:
if callable(self.callback):
self.callback(self)
finally:
self.break_cycles()
return
self.do_one()
# Filesystem scan {{{
def scan(self):
try:
if isinstance(self.source, basestring):
if os.path.isdir(self.source):
root = self.source
else:
root = self.extract()
for dirpath, dirnames, filenames in os.walk(root):
for files in find_books_in_directory(dirpath, self.single_book_per_directory):
if self.abort_scan:
return
if files:
self.file_groups[len(self.file_groups)] = files
else:
unreadable_files = []
for path in self.source:
if self.abort_scan:
return
if os.access(path, os.R_OK):
self.file_groups[len(self.file_groups)] = [path]
else:
unreadable_files.append(path)
if unreadable_files:
if not self.file_groups:
self.scan_error = _('You do not have permission to read the selected file(s).') + '\n'
self.scan_error += '\n'.join(unreadable_files)
else:
a = self.report.append
for f in unreadable_files:
a(_('Could not add %s as you do not have permission to read the file' % f))
a('')
except Exception:
self.scan_error = traceback.format_exc()
def extract(self):
tdir = os.path.join(self.tdir, 'archive')
if self.source.lower().endswith('.zip'):
from calibre.utils.zipfile import ZipFile
try:
with ZipFile(self.source) as zf:
zf.extractall(tdir)
except Exception:
prints('Corrupt ZIP file, trying to use local headers')
from calibre.utils.localunzip import extractall
extractall(self.source, tdir)
elif self.path.lower().endswith('.rar'):
from calibre.utils.unrar import extract
extract(self.source, tdir)
return tdir
def monitor_scan(self):
self.scan_thread.join(0.05)
if self.scan_thread.is_alive():
self.do_one_signal.emit()
return
if self.scan_error is not None:
error_dialog(self.pd, _('Cannot add books'), _(
'Failed to add any books, click "Show details" for more information.'),
det_msg=self.scan_error, show=True)
self.break_cycles()
return
if not self.file_groups:
error_dialog(self.pd, _('Could not add'), _(
'No ebook files were found in %s') % self.source, show=True)
self.break_cycles()
return
self.pd.msg = _('Reading metadata and adding to library...')
self.pd.max = len(self.file_groups)
self.pd.value = 0
self.pool = Pool(name='AddBooks') if self.pool is None else self.pool
if self.db is not None:
data = self.db.data_for_find_identical_books() if self.add_formats_to_existing else self.db.data_for_has_book()
try:
self.pool.set_common_data(data)
except Failure as err:
error_dialog(self.pd, _('Cannot add books'), _(
'Failed to add any books, click "Show details" for more information.'),
det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
self.pd.canceled = True
self.groups_to_add = iter(self.file_groups)
self.do_one = self.do_one_group
self.do_one_signal.emit()
# }}}
def do_one_group(self):
try:
group_id = next(self.groups_to_add)
except StopIteration:
self.do_one = self.monitor_pool
self.do_one_signal.emit()
return
try:
self.pool(group_id, 'calibre.ebooks.metadata.worker', 'read_metadata',
self.file_groups[group_id], group_id, self.tdir)
except Failure as err:
error_dialog(self.pd, _('Cannot add books'), _(
'Failed to add any books, click "Show details" for more information.'),
det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
self.pd.canceled = True
self.do_one_signal.emit()
def monitor_pool(self):
try:
worker_result = self.pool.results.get(True, 0.05)
self.pool.results.task_done()
except Empty:
try:
self.pool.wait_for_tasks(timeout=0.01)
except RuntimeError:
pass # Tasks still remaining
except Failure as err:
error_dialog(self.pd, _('Cannot add books'), _(
'Failed to add some books, click "Show details" for more information.'),
det_msg=unicode(err.failure_message) + '\n' + unicode(err.details), show=True)
self.pd.canceled = True
else:
# All tasks completed
try:
join_with_timeout(self.pool.results, 0.01)
except RuntimeError:
pass # There are results remaining
else:
# No results left
self.process_duplicates()
return
else:
group_id = worker_result.id
if worker_result.is_terminal_failure:
error_dialog(self.pd, _('Critical failure'), _(
'The read metadata worker process crashed while processing'
' some files. Adding of books is aborted. Click "Show details"'
' to see which files caused the problem.'), show=True,
det_msg='\n'.join(self.file_groups[group_id]))
self.pd.canceled = True
else:
try:
self.process_result(group_id, worker_result.result)
except Exception:
self.report_metadata_failure(group_id, traceback.format_exc())
self.pd.value += 1
self.do_one_signal.emit()
def report_metadata_failure(self, group_id, details):
a = self.report.append
paths = self.file_groups[group_id]
a(''), a('-' * 70)
a(_('Failed to read metadata from the file(s):'))
[a('\t' + f) for f in paths]
a(_('With error:')), a(details)
mi = Metadata(_('Unknown'))
mi.read_metadata_failed = False
return mi
def process_result(self, group_id, result):
if result.err:
mi = self.report_metadata_failure(group_id, result.traceback)
paths = self.file_groups[group_id]
has_cover = False
duplicate_info = set() if self.add_formats_to_existing else False
else:
paths, opf, has_cover, duplicate_info = result.value
try:
mi = OPF(BytesIO(opf), basedir=self.tdir, populate_spine=False, try_to_guess_cover=False).to_book_metadata()
mi.read_metadata_failed = False
except Exception:
mi = self.report_metadata_failure(group_id, traceback.format_exc())
if mi.is_null('title'):
for path in paths:
mi.title = os.path.splitext(os.path.basename(path))[0]
break
if mi.application_id == '__calibre_dummy__':
mi.application_id = None
self.pd.msg = mi.title
cover_path = os.path.join(self.tdir, '%s.cdata' % group_id) if has_cover else None
if self.db is None:
if paths:
self.items.append((mi, cover_path, paths))
return
if self.add_formats_to_existing:
pass # TODO: Implement this
else:
if duplicate_info or icu_lower(mi.title or _('Unknown')) in self.added_duplicate_info:
self.duplicates.append((mi, cover_path, paths))
else:
self.add_book(mi, cover_path, paths)
def add_book(self, mi, cover_path, paths):
try:
cdata = None
if cover_path:
with open(cover_path, 'rb') as f:
cdata = f.read()
book_id = self.dbref().create_book_entry(mi, cover=cdata)
self.added_book_ids.add(book_id)
except Exception:
a = self.report.append
a(''), a('-' * 70)
a(_('Failed to add the book: ') + mi.title)
[a('\t' + f) for f in paths]
a(_('With error:')), a(traceback.format_exc())
return
else:
self.add_formats(book_id, paths, mi)
if self.add_formats_to_existing:
pass # TODO: Implement this
else:
self.added_duplicate_info.add(icu_lower(mi.title or _('Unknown')))
def add_formats(self, book_id, paths, mi):
fmap = {p.rpartition(os.path.extsep)[-1].lower():p for p in paths}
for fmt, path in fmap.iteritems():
# The onimport plugins have already been run by the read metadata
# worker
try:
if self.db.add_format(book_id, fmt, path, run_hooks=False):
run_plugins_on_postimport(self.dbref(), book_id, fmt)
except Exception:
a = self.report.append
a(''), a('-' * 70)
a(_('Failed to add the file {0} to the book: {1}').format(path, mi.title))
a(_('With error:')), a(traceback.format_exc())
def process_duplicates(self):
if self.duplicates:
d = DuplicatesQuestion(self.dbref(), self.duplicates, self.pd)
duplicates = tuple(d.duplicates)
d.deleteLater()
if duplicates:
self.do_one = self.process_duplicate
self.duplicates_to_process = iter(duplicates)
self.do_one_signal.emit()
return
self.finish()
def process_duplicate(self):
try:
mi, cover_path, paths = next(self.duplicates_to_process)
except StopIteration:
self.finish()
return
self.add_book(mi, cover_path, paths)
self.do_one_signal.emit()
def finish(self):
if self.report:
added_some = self.items or self.added_book_ids
d = warning_dialog if added_some else error_dialog
msg = _('There were problems adding some files, click "Show details" for more information') if added_some else _(
'Failed to add any books, click "Show details" for more information')
d(self.pd, _('Errors while adding'), msg, det_msg='\n'.join(self.report), show=True)
try:
if callable(self.callback):
self.callback(self)
finally:
self.break_cycles()
@property
def number_of_books_added(self):
return len(self.added_book_ids)
# TODO: Duplicates and auto-merge (in particular adding duplicated files as well as adding files already in the db)
# TODO: Test importing with filetype plugin (archive, de-obfuscate)
# TODO: Test recursive adding when no books are found
# TODO: Test handling of exception in metadata read function
# TODO: Report terminal erros where some books have been added better
# TODO: Test direct add of books to device
# TODO: Test adding form device to library
# TODO: Check aborting after a few books have been added

View File

@ -109,6 +109,12 @@ class MainWindow(QMainWindow):
if disable_automatic_gc: if disable_automatic_gc:
self._gc = GarbageCollector(self, debug=False) self._gc = GarbageCollector(self, debug=False)
def enable_garbage_collection(self, enabled=True):
if hasattr(self, '_gc'):
self._gc.timer.blockSignals(not enabled)
else:
gc.enable() if enabled else gc.disable()
def unhandled_exception(self, type, value, tb): def unhandled_exception(self, type, value, tb):
if type == KeyboardInterrupt: if type == KeyboardInterrupt:
self.keyboard_interrupt.emit() self.keyboard_interrupt.emit()

View File

@ -19,41 +19,38 @@ from calibre.constants import iswindows, isosx
from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc import eintr_retry_call
PARALLEL_FUNCS = { PARALLEL_FUNCS = {
'lrfviewer' : 'lrfviewer' :
('calibre.gui2.lrf_renderer.main', 'main', None), ('calibre.gui2.lrf_renderer.main', 'main', None),
'ebook-viewer' : 'ebook-viewer' :
('calibre.gui_launch', 'ebook_viewer', None), ('calibre.gui_launch', 'ebook_viewer', None),
'ebook-edit' : 'ebook-edit' :
('calibre.gui_launch', 'gui_ebook_edit', None), ('calibre.gui_launch', 'gui_ebook_edit', None),
'render_pages' : 'render_pages' :
('calibre.ebooks.comic.input', 'render_pages', 'notification'), ('calibre.ebooks.comic.input', 'render_pages', 'notification'),
'gui_convert' : 'gui_convert' :
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'), ('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
'gui_polish' : 'gui_polish' :
('calibre.ebooks.oeb.polish.main', 'gui_polish', None), ('calibre.ebooks.oeb.polish.main', 'gui_polish', None),
'gui_convert_override' : 'gui_convert_override' :
('calibre.gui2.convert.gui_conversion', 'gui_convert_override', 'notification'), ('calibre.gui2.convert.gui_conversion', 'gui_convert_override', 'notification'),
'gui_catalog' : 'gui_catalog' :
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'), ('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
'move_library' : 'move_library' :
('calibre.library.move', 'move_library', 'notification'), ('calibre.library.move', 'move_library', 'notification'),
'read_metadata' : 'arbitrary' :
('calibre.ebooks.metadata.worker', 'read_metadata_', 'notification'), ('calibre.utils.ipc.worker', 'arbitrary', None),
'arbitrary' : 'arbitrary_n' :
('calibre.utils.ipc.worker', 'arbitrary', None), ('calibre.utils.ipc.worker', 'arbitrary_n', 'notification'),
'arbitrary_n' :
('calibre.utils.ipc.worker', 'arbitrary_n', 'notification'),
} }
class Progress(Thread): class Progress(Thread):