diff --git a/src/calibre/ebooks/metadata/fictionwise.py b/src/calibre/ebooks/metadata/fictionwise.py index b780f2b39d..1d6aceecdd 100644 --- a/src/calibre/ebooks/metadata/fictionwise.py +++ b/src/calibre/ebooks/metadata/fictionwise.py @@ -4,6 +4,7 @@ __copyright__ = '2010, sengian ' __docformat__ = 'restructuredtext en' import sys, textwrap, re, traceback, socket +from Queue import Queue from urllib import urlencode from lxml.html import soupparser, tostring @@ -39,6 +40,8 @@ class Fictionwise(MetadataSource): # {{{ class FictionwiseError(Exception): pass + + def report(verbose): if verbose: traceback.print_exc() diff --git a/src/calibre/ebooks/metadata/nicebooks.py b/src/calibre/ebooks/metadata/nicebooks.py index 7beececd7e..580e645320 100644 --- a/src/calibre/ebooks/metadata/nicebooks.py +++ b/src/calibre/ebooks/metadata/nicebooks.py @@ -3,7 +3,8 @@ __license__ = 'GPL 3' __copyright__ = '2010, sengian ' __docformat__ = 'restructuredtext en' -import sys, textwrap, re, traceback, socket, threading +import sys, textwrap, re, traceback, socket +from threading import Thread from Queue import Queue from urllib import urlencode from math import ceil @@ -79,16 +80,16 @@ class NiceBooksError(Exception): class ISBNNotFound(NiceBooksError): pass -class BrowserThread(threading.Thread): +class BrowserThread(Thread): def __init__(self, url, verbose=False, timeout=10., ex=Exception, name='Meta'): self.url = url self.ex = ex - self.name = name + self.plugname = name self.verbose = verbose self.timeout = timeout self.result = None - threading.Thread.__init__(self) + Thread.__init__(self) def get_result(self): return self.result @@ -102,8 +103,8 @@ class BrowserThread(threading.Thread): e.getcode() == 404: self.result = None if isinstance(getattr(e, 'args', [None])[0], socket.timeout): - raise self.ex(_('%s timed out. Try again later.') % self.name) - raise self.ex(_('%s encountered an error.') % self.name) + raise self.ex(_('%s timed out. Try again later.') % self.plugname) + raise self.ex(_('%s encountered an error.') % self.plugname) if '404 - ' in raw: report(self.verbose) self.result = None @@ -292,10 +293,11 @@ class ResultList(list): while len(self) < total_entries: thread = q.get(True) thread.join() - mi, order = thread.get_result() + mi = thread.get_result() if mi is None: self.append(None) - self.append(self.fill_MI(mi, verbose)) + else: + self.append(self.fill_MI(mi, verbose)) def populate(self, entries, verbose=False, brcall=3): if len(entries) == 1 and not isinstance(entries[0], str): @@ -306,8 +308,8 @@ class ResultList(list): else: #multiple entries q = Queue(brcall) - prod_thread = threading.Thread(target=self.producer, args=(q, entries, verbose)) - cons_thread = threading.Thread(target=self.consumer, args=(q, len(entries), verbose)) + prod_thread = Thread(target=self.producer, args=(q, entries, verbose)) + cons_thread = Thread(target=self.consumer, args=(q, len(entries), verbose)) prod_thread.start() cons_thread.start() prod_thread.join() @@ -362,7 +364,7 @@ def search(title=None, author=None, publisher=None, isbn=None, #List of entry ans = ResultList() ans.populate(entries, verbose) - return ans + return [x for x in ans if x] def check_for_cover(isbn): br = browser() diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index a541590fd1..a0f49a7e9a 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -5,13 +5,67 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QMenu +from functools import partial + +from PyQt4.Qt import QMenu, QObject, QTimer from calibre.gui2 import error_dialog from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.actions import InterfaceAction +single_shot = partial(QTimer.singleShot, 10) + +class MultiDeleter(QObject): + + def __init__(self, gui, rows, callback): + from calibre.gui2.dialogs.progress import ProgressDialog + QObject.__init__(self, gui) + self.model = gui.library_view.model() + self.ids = list(map(self.model.id, rows)) + self.gui = gui + self.failures = [] + self.deleted_ids = [] + self.callback = callback + single_shot(self.delete_one) + self.pd = ProgressDialog(_('Deleting...'), parent=gui, + cancelable=False, min=0, max=len(self.ids)) + self.pd.setModal(True) + self.pd.show() + + def delete_one(self): + if not self.ids: + self.cleanup() + return + id_ = self.ids.pop() + title = 'id:%d'%id_ + try: + title_ = self.model.db.title(id_, index_is_id=True) + if title_: + title = title_ + self.model.db.delete_book(id_, notify=False, commit=False) + self.deleted_ids.append(id_) + except: + import traceback + self.failures.append((id_, title, traceback.format_exc())) + single_shot(self.delete_one) + self.pd.value += 1 + self.pd.set_msg(_('Deleted') + ' ' + title) + + def cleanup(self): + self.pd.hide() + self.pd = None + self.model.db.commit() + self.model.db.clean() + self.model.books_deleted() + self.gui.tags_view.recount() + self.callback(self.deleted_ids) + if self.failures: + msg = ['==> '+x[1]+'\n'+x[2] for x in self.failures] + error_dialog(self.gui, _('Failed to delete'), + _('Failed to delete some books, click the Show Details button' + ' for details.'), det_msg='\n\n'.join(msg), show=True) + class DeleteAction(InterfaceAction): name = 'Remove Books' @@ -179,8 +233,13 @@ class DeleteAction(InterfaceAction): row = None if ci.isValid(): row = ci.row() - ids_deleted = view.model().delete_books(rows) - self.library_ids_deleted(ids_deleted, row) + if len(rows) < 5: + ids_deleted = view.model().delete_books(rows) + self.library_ids_deleted(ids_deleted, row) + else: + self.__md = MultiDeleter(self.gui, rows, + partial(self.library_ids_deleted, current_row=row)) + else: if not confirm('<p>'+_('The selected books will be ' '<b>permanently deleted</b> ' diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index e82e1dddd4..8478a6d263 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -223,21 +223,22 @@ class BooksModel(QAbstractTableModel): # {{{ def by_author(self): return self.sorted_on[0] == 'authors' + def books_deleted(self): + self.count_changed() + self.clear_caches() + self.reset() + def delete_books(self, indices): ids = map(self.id, indices) for id in ids: self.db.delete_book(id, notify=False) - self.count_changed() - self.clear_caches() - self.reset() + self.books_deleted() return ids def delete_books_by_id(self, ids): for id in ids: self.db.delete_book(id) - self.count_changed() - self.clear_caches() - self.reset() + self.books_deleted() def books_added(self, num): if num > 0: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 4efb5e6233..1229b60577 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -953,23 +953,24 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) return True - def delete_book(self, id, notify=True): + def delete_book(self, id, notify=True, commit=True): ''' Removes book from the result cache and the underlying database. + If you set commit to False, you must call clean() manually afterwards ''' try: path = os.path.join(self.library_path, self.path(id, index_is_id=True)) except: path = None - self.data.remove(id) if path and os.path.exists(path): self.rmtree(path) parent = os.path.dirname(path) if len(os.listdir(parent)) == 0: self.rmtree(parent) self.conn.execute('DELETE FROM books WHERE id=?', (id,)) - self.conn.commit() - self.clean() + if commit: + self.conn.commit() + self.clean() self.data.books_deleted([id]) if notify: self.notify('delete', [id]) diff --git a/src/calibre/web/feeds/__init__.py b/src/calibre/web/feeds/__init__.py index 8aef350498..478dd5015b 100644 --- a/src/calibre/web/feeds/__init__.py +++ b/src/calibre/web/feeds/__init__.py @@ -166,7 +166,7 @@ class Feed(object): self.articles.append(article) else: t = strftime(u'%a, %d %b, %Y %H:%M', article.localtime.timetuple()) - self.logger.debug('Skipping article %s (%s) from feed %s as it is too old.'% + self.logger.debug(u'Skipping article %s (%s) from feed %s as it is too old.'% (title, t, self.title)) d = item.get('date', '') article.formatted_date = d