diff --git a/resources/recipes/libero.recipe b/resources/recipes/libero.recipe index 4354940746..f2208d01a3 100644 --- a/resources/recipes/libero.recipe +++ b/resources/recipes/libero.recipe @@ -11,11 +11,11 @@ http://www.libero-news.it/ from calibre.web.feeds.news import BasicNewsRecipe class LiberoNews(BasicNewsRecipe): - __author__ = 'Marini Gabriele' - description = 'Italian daily newspaper' + __author__ = 'Marini Gabriele' + description = 'Italian daily newspaper' - cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif' - title = u'Libero' + cover_url = 'http://www.libero-news.it/images/logo.png' + title = u'Libero ' publisher = 'EDITORIALE LIBERO s.r.l 2006' category = 'News, politics, culture, economy, general interest' diff --git a/resources/recipes/the_oz.recipe b/resources/recipes/the_oz.recipe index a55f31e63e..ccdce0acb6 100644 --- a/resources/recipes/the_oz.recipe +++ b/resources/recipes/the_oz.recipe @@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe): language = 'en_AU' oldest_article = 2 - max_articles_per_feed = 10 + max_articles_per_feed = 20 remove_javascript = True no_stylesheets = True encoding = 'utf8' diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 54bd745879..0bbdf0f22c 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -30,7 +30,7 @@ class ANDROID(USBMS): 0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]}, # Samsung - 0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]}, + 0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]}, # Acer 0x502 : { 0x3203 : [0x0100]}, @@ -41,10 +41,12 @@ class ANDROID(USBMS): 'be used') EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) - VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700'] + VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', + 'GT-I5700', 'SAMSUNG'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', - '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD'] - WINDOWS_CARD_A_MEM = ['ANDROID_PHONE'] + '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', + 'PROD_GT-I9000'] + WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD'] OSX_MAIN_MEM = 'HTC Android Phone Media' diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index 3e16eccbbc..ae440a359e 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -76,7 +76,7 @@ class ITUNES(DevicePlugin): supported_platforms = ['osx','windows'] author = 'GRiker' #: The version of this plugin as a 3-tuple (major, minor, revision) - version = (0, 5, 0) + version = (0,6,0) OPEN_FEEDBACK_MESSAGE = _( 'Apple device detected, launching iTunes, please wait ...') @@ -280,7 +280,7 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count)) - self._purge_orphans(cached_books) + self._purge_orphans(library_books, cached_books) elif iswindows: try: @@ -316,7 +316,7 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(i+1/book_count, _('%d of %d') % (i+1, book_count)) - self._purge_orphans(cached_books) + self._purge_orphans(library_books, cached_books) finally: pythoncom.CoUninitialize() @@ -324,9 +324,9 @@ class ITUNES(DevicePlugin): if self.report_progress is not None: self.report_progress(1.0, _('finished')) self.cached_books = cached_books - if DEBUG: - self._dump_booklist(booklist, 'returning from books():') - self._dump_cached_books('returning from books():') +# if DEBUG: +# self._dump_booklist(booklist, 'returning from books():') +# self._dump_cached_books('returning from books():') return booklist else: return [] @@ -463,7 +463,7 @@ class ITUNES(DevicePlugin): else: # iTunes running, but not connected iPad if DEBUG: - self.log.info(' self.ejected = True') + self.log.info(' iDevice has been ejected') self.ejected = True return False @@ -782,121 +782,6 @@ class ITUNES(DevicePlugin): # self._dump_cached_books('upload_books()') self._dump_update_list('upload_books()') - ''' - if isosx: - - for (i,file) in enumerate(files): - path = self.path_template % (metadata[i].title, metadata[i].author[0]) - - if self.manual_sync_mode: - # Delete existing from Device|Books, add to self.update_list - # for deletion from booklist[0] during add_books_to_metadata - if path in self.cached_books: - self.update_list.append(self.cached_books[path]) - if DEBUG: - self.log.info(" adding '%s' by %s to self.update_list" % - (self.cached_books[path]['title'],self.cached_books[path]['author'])) - - if DEBUG: - self.log.info( " deleting existing '%s'" % (path)) - self._remove_from_iTunes(self.cached_books[path]) - if self.manual_sync_mode: - dev_book_added = self._remove_from_device(self.cached_books[path]) - - - # Add to iTunes Library|Books - fpath = file - if getattr(file, 'orig_file_path', None) is not None: - fpath = file.orig_file_path - elif getattr(file, 'name', None) is not None: - fpath = file.name - - if isinstance(file,PersistentTemporaryFile) and self.manual_sync_mode: - if DEBUG: - self.log.info(" PTF not added to Library|Books") - else: - added = self.iTunes.add(appscript.mactypes.File(fpath)) - if DEBUG: - self.log.info(" file added to Library|Books") - - dev_book_added = None - if self.manual_sync_mode: - dev_book_added = self._add_device_book(fpath) - - thumb = None - if metadata[i].cover: - try: - # Use cover data as artwork - cover_data = open(metadata[i].cover,'rb') - added.artworks[1].data_.set(cover_data.read()) - - # Resize for thumb - width = metadata[i].thumbnail[0] - height = metadata[i].thumbnail[1] - im = PILImage.open(metadata[i].cover) - im = im.resize((width, height), PILImage.ANTIALIAS) - of = cStringIO.StringIO() - im.convert('RGB').save(of, 'JPEG') - thumb = of.getvalue() - - # Refresh the thumbnail cache - if DEBUG: - self.log.info( " refreshing cached thumb for '%s'" % metadata[i].title) - archive_path = os.path.join(self.cache_dir, "thumbs.zip") - zfw = zipfile.ZipFile(archive_path, mode='a') - thumb_path = path.rpartition('.')[0] + '.jpg' - zfw.writestr(thumb_path, thumb) - zfw.close() - except: - self.problem_titles.append("'%s' by %s" % (metadata[i].title, metadata[i].author[0])) - self.log.error("ITUNES.upload_books(): error converting '%s' to thumb for '%s'" % (metadata[i].cover,metadata[i].title)) - - # Create a new Book - this_book = Book(metadata[i].title, metadata[i].author[0]) - try: - this_book.datetime = parse_date(str(added.date_added())).timetuple() - except: - pass - this_book.db_id = None - this_book.device_collections = [] - this_book.library_id = added - this_book.path = path - this_book.size = self._get_device_book_size(fpath, added.size()) - this_book.thumbnail = thumb - this_book.iTunes_id = added - - new_booklist.append(this_book) - - # Populate the iTunes metadata - if metadata[i].comments: - added.comment.set(strip_tags.sub('',metadata[i].comments)) - added.description.set("added by calibre %s" % strftime('%Y-%m-%d %H:%M:%S')) - added.enabled.set(True) - if metadata[i].rating: - added.rating.set(metadata[i].rating*10) - added.sort_artist.set(metadata[i].author_sort.title()) - added.sort_name.set(this_book.title_sorter) - - # Set genre from metadata - # iTunes grabs the first dc:subject from the opf metadata, - # But we can manually override with first tag starting with alpha - for tag in metadata[i].tags: - if self._is_alpha(tag[0]): - added.genre.set(tag) - break - - # Add new_book to self.cached_paths - self.cached_books[this_book.path] = { - 'title': this_book.title, - 'author': this_book.author, - 'lib_book': added, - 'dev_book': dev_book_added - } - - # Report progress - if self.report_progress is not None: - self.report_progress(i+1/file_count, _('%d of %d') % (i+1, file_count)) - ''' if isosx: for (i,file) in enumerate(files): path = self.path_template % (metadata[i].title, metadata[i].author[0]) @@ -1378,6 +1263,15 @@ class ITUNES(DevicePlugin): self.log.info(" %s" % file.name) self.log.info() + def _dump_library_books(self, library_books): + ''' + ''' + if DEBUG: + self.log.info("\n library_books:") + for book in library_books: + self.log.info(" %s" % book) + self.log.info() + def _dump_update_list(self,header=None): if header: msg = '\nself.update_list called from %s' % header @@ -1590,7 +1484,7 @@ class ITUNES(DevicePlugin): self.log.info(" ignoring '%s' of type '%s'" % (book.name(), book.kind())) else: if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind())) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind())) device_books.append(book) elif iswindows: @@ -1619,7 +1513,7 @@ class ITUNES(DevicePlugin): self.log.info(" ignoring '%s' of type '%s'" % (book.Name, book.KindAsString)) else: if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString)) device_books.append(book) finally: @@ -1716,11 +1610,11 @@ class ITUNES(DevicePlugin): if book.location() == appscript.k.missing_value: library_orphans[path] = book if DEBUG: - self.log.info(" found calibre orphan '%s' in Library|Books" % book.name()) + self.log.info(" found iTunes PTF '%s' in Library|Books" % book.name()) library_books[path] = book if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.name(), book.kind())) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.name(), book.artist(), book.kind())) else: if DEBUG: self.log.info(' no Library playlists') @@ -1730,9 +1624,6 @@ class ITUNES(DevicePlugin): elif iswindows: lib = None -# try: -# pythoncom.CoInitialize() -# self.iTunes = win32com.client.Dispatch("iTunes.Application") for source in self.iTunes.sources: if source.Kind == self.Sources.index('Library'): lib = source @@ -1772,16 +1663,14 @@ class ITUNES(DevicePlugin): if not book.Location: library_orphans[path] = book if DEBUG: - self.log.info(" found calibre orphan '%s' in Library|Books" % book.Name) + self.log.info(" found iTunes PTF '%s' in Library|Books" % book.Name) library_books[path] = book if DEBUG: - self.log.info(" adding %-30.30s [%s]" % (book.Name, book.KindAsString)) + self.log.info(" adding %-30.30s %-30.30s [%s]" % (book.Name, book.Artist, book.KindAsString)) except: if DEBUG: self.log.info(" no books in library") -# finally: -# pythoncom.CoUninitialize() self.library_orphans = library_orphans return library_books @@ -1905,44 +1794,36 @@ class ITUNES(DevicePlugin): self.version[0],self.version[1],self.version[2])) self.log.info(" iTunes_media: %s" % self.iTunes_media) - def _purge_orphans(self,cached_books): + def _purge_orphans(self,library_books, cached_books): ''' - Scan self.library_orphans for any paths not on device - Remove any true orphans from iTunes - This occurs when recipes are uploaded in a previous session - and the book has since been deleted on the device + Scan library_books for any paths not on device + Remove any iTunes orphans originally added by calibre + This occurs when the user deletes a book in iBooks while disconnected ''' if DEBUG: - self.log.info(" ITUNES._purge_orphans") + self.log.info("\n ITUNES._purge_orphans") + #self._dump_library_books(library_books) #self.log.info(" cached_books:\n %s" % "\n ".join(cached_books.keys())) - orphan_paths = {} - - if isosx: - for orphan in self.library_orphans: - path = self.path_template % (self.library_orphans[orphan].name(), - self.library_orphans[orphan].artist()) - orphan_paths[path] = self.library_orphans[orphan] - - # Scan orphan_paths for paths not found in cached_books - for orphan in orphan_paths.keys(): - if orphan not in cached_books: + for book in library_books: + if isosx: + if book not in cached_books and \ + str(library_books[book].description()).startswith(self.description_prefix): if DEBUG: - self.log.info(" '%s' not found on device, removing from iTunes" % orphan) - self.iTunes.delete(orphan_paths[orphan]) - - elif iswindows: - for orphan in self.library_orphans: - path = self.path_template % (self.library_orphans[orphan].Name, - self.library_orphans[orphan].Artist) - orphan_paths[path] = self.library_orphans[orphan] - - # Scan orphan_paths for paths not found in cached_books - for orphan in orphan_paths.keys(): - if orphan not in cached_books: + self.log.info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { 'title':library_books[book].name(), + 'author':library_books[book].artist(), + 'lib_book':library_books[book]} + self._remove_from_iTunes(btr) + elif iswindows: + if book not in cached_books and \ + library_books[book].Description.startswith(self.description_prefix): if DEBUG: - self.log.info(" '%s' not found on device, removing from iTunes" % orphan) - orphan_paths[orphan].Delete() + self.log.info(" '%s' not found on iDevice, removing from iTunes" % book) + btr = { 'title':library_books[book].Name, + 'author':library_books[book].Artist, + 'lib_book':library_books[book]} + self._remove_from_iTunes(btr) def _remove_existing_copies(self,path,file,metadata): ''' @@ -2040,7 +1921,7 @@ class ITUNES(DevicePlugin): except: # We get here if there was an error with .location().path - self.log.info(" removing orphan '%s' from iTunes" % cached_book['title']) + self.log.info(" removing orphan '%s' from iTunes" % cached_book['title']) self.iTunes.delete(cached_book['lib_book']) @@ -2049,33 +1930,33 @@ class ITUNES(DevicePlugin): Assume we're wrapped in a pythoncom Windows stores the book under a common author directory, so we just delete the .epub ''' - - book = self._find_library_book(cached_book) - if book: + try: + book = cached_book['lib_book'] + path = book.Location + except: + book = self._find_library_book(cached_book) path = book.Location - storage_path = os.path.split(book.Location) - if book.Location.startswith(self.iTunes_media): - if DEBUG: - self.log.info(" removing '%s' at %s" % - (cached_book['title'], path)) - try: - os.remove(path) - except: - self.log.warning(" could not find '%s' in iTunes storage" % path) - try: - os.rmdir(storage_path[0]) - self.log.info(" removed folder '%s'" % storage_path[0]) - except: - self.log.info(" folder '%s' not found or not empty" % storage_path[0]) - # Delete from iTunes database - else: - self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title']) - - book.Delete() + storage_path = os.path.split(book.Location) + if book.Location.startswith(self.iTunes_media): + if DEBUG: + self.log.info(" removing '%s' at %s" % + (cached_book['title'], path)) + try: + os.remove(path) + except: + self.log.warning(" could not find '%s' in iTunes storage" % path) + try: + os.rmdir(storage_path[0]) + self.log.info(" removed folder '%s'" % storage_path[0]) + except: + self.log.info(" folder '%s' not found or not empty" % storage_path[0]) + # Delete from iTunes database else: - self.log.warning(" could not find '%s' in iTunes database" % cached_book['title']) + self.log.info(" '%s' stored external to iTunes, no files deleted" % cached_book['title']) + + book.Delete() def _update_device(self, msg='', wait=True): ''' diff --git a/src/calibre/ebooks/oeb/transforms/cover.py b/src/calibre/ebooks/oeb/transforms/cover.py index 4d41ab14b4..83c5ec93e4 100644 --- a/src/calibre/ebooks/oeb/transforms/cover.py +++ b/src/calibre/ebooks/oeb/transforms/cover.py @@ -103,8 +103,8 @@ class CoverManager(object): 32)] img_data = create_cover_page(lines, I('library.png')) id, href = self.oeb.manifest.generate('cover_image', - 'cover_image.png') - item = self.oeb.manifest.add(id, href, guess_type('t.png')[0], + 'cover_image.jpg') + item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0], data=img_data) m.clear('cover') m.add('cover', item.id) diff --git a/src/calibre/gui2/actions.py b/src/calibre/gui2/actions.py new file mode 100644 index 0000000000..4b2e367080 --- /dev/null +++ b/src/calibre/gui2/actions.py @@ -0,0 +1,1200 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import shutil, os, datetime, sys, time +from functools import partial + +from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \ + SIGNAL, QPixmap, QTimer, QDesktopServices, QUrl, QDialog + +from calibre import strftime +from calibre.ptempfile import PersistentTemporaryFile +from calibre.utils.config import prefs, dynamic +from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \ + choose_dir, warning_dialog, info_dialog, question_dialog, config +from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString +from calibre.utils.filenames import ascii_filename +from calibre.gui2.widgets import IMAGE_EXTENSIONS +from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog +from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog +from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ + fetch_scheduled_recipe, generate_catalog +from calibre.constants import preferred_encoding, filesystem_encoding, \ + isosx, isfrozen, islinux +from calibre.gui2.dialogs.choose_format import ChooseFormatDialog +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.gui2.dialogs.confirm_delete import confirm + +class AnnotationsAction(object): # {{{ + + def fetch_annotations(self, *args): + # Generate a path_map from selected ids + def get_ids_from_selected_rows(): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) < 2: + rows = xrange(self.library_view.model().rowCount(QModelIndex())) + ids = map(self.library_view.model().id, rows) + return ids + + def get_formats(id): + formats = db.formats(id, index_is_id=True) + fmts = [] + if formats: + for format in formats.split(','): + fmts.append(format.lower()) + return fmts + + def generate_annotation_paths(ids, db, device): + # Generate path templates + # Individual storage mount points scanned/resolved in driver.get_annotations() + path_map = {} + for id in ids: + mi = db.get_metadata(id, index_is_id=True) + a_path = device.create_upload_path(os.path.abspath('/'), mi, 'x.bookmark', create_dirs=False) + path_map[id] = dict(path=a_path, fmts=get_formats(id)) + return path_map + + device = self.device_manager.device + + if self.current_view() is not self.library_view: + return error_dialog(self, _('Use library only'), + _('User annotations generated from main library only'), + show=True) + db = self.library_view.model().db + + # Get the list of ids + ids = get_ids_from_selected_rows() + if not ids: + return error_dialog(self, _('No books selected'), + _('No books selected to fetch annotations from'), + show=True) + + # Map ids to paths + path_map = generate_annotation_paths(ids, db, device) + + # Dispatch to devices.kindle.driver.get_annotations() + self.device_manager.annotations(Dispatcher(self.annotations_fetched), + path_map) + + def annotations_fetched(self, job): + from calibre.devices.usbms.device import Device + from calibre.ebooks.metadata import MetaInformation + from calibre.gui2.dialogs.progress import ProgressDialog + from calibre.library.cli import do_add_format + + class Updater(QThread): + + update_progress = pyqtSignal(int) + update_done = pyqtSignal() + FINISHED_READING_PCT_THRESHOLD = 96 + + def __init__(self, parent, db, annotation_map, done_callback): + QThread.__init__(self, parent) + self.db = db + self.pd = ProgressDialog(_('Merging user annotations into database'), '', + 0, len(job.result), parent=parent) + + self.am = annotation_map + self.done_callback = done_callback + self.connect(self.pd, SIGNAL('canceled()'), self.canceled) + self.pd.setModal(True) + self.pd.show() + self.update_progress.connect(self.pd.set_value, + type=Qt.QueuedConnection) + self.update_done.connect(self.pd.hide, type=Qt.QueuedConnection) + + def generate_annotation_html(self, bookmark): + # Returns
...
+ last_read_location = bookmark.last_read_location + timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp) + percent_read = bookmark.percent_read + + ka_soup = BeautifulSoup() + dtc = 0 + divTag = Tag(ka_soup,'div') + divTag['class'] = 'user_annotations' + + # Add the last-read location + spanTag = Tag(ka_soup, 'span') + spanTag['style'] = 'font-weight:bold' + if bookmark.book_format == 'pdf': + spanTag.insert(0,NavigableString( + _("%s
Last Page Read: %d (%d%%)") % \ + (strftime(u'%x', timestamp.timetuple()), + last_read_location, + percent_read))) + else: + spanTag.insert(0,NavigableString( + _("%s
Last Page Read: Location %d (%d%%)") % \ + (strftime(u'%x', timestamp.timetuple()), + last_read_location, + percent_read))) + + divTag.insert(dtc, spanTag) + dtc += 1 + divTag.insert(dtc, Tag(ka_soup,'br')) + dtc += 1 + + if bookmark.user_notes: + user_notes = bookmark.user_notes + annotations = [] + + # Add the annotations sorted by location + # Italicize highlighted text + for location in sorted(user_notes): + if user_notes[location]['text']: + annotations.append( + _('Location %d • %s
%s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'], + user_notes[location]['text'] if \ + user_notes[location]['type'] == 'Note' else \ + '%s' % user_notes[location]['text'])) + else: + if bookmark.book_format == 'pdf': + annotations.append( + _('Page %d • %s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'])) + else: + annotations.append( + _('Location %d • %s
') % \ + (user_notes[location]['displayed_location'], + user_notes[location]['type'])) + + for annotation in annotations: + divTag.insert(dtc, annotation) + dtc += 1 + + ka_soup.insert(0,divTag) + return ka_soup + + def mark_book_as_read(self,id): + read_tag = gprefs.get('catalog_epub_mobi_read_tag') + self.db.set_tags(id, [read_tag], append=True) + + def canceled(self): + self.pd.hide() + + def run(self): + ignore_tags = set(['Catalog','Clippings']) + for (i, id) in enumerate(self.am): + bm = Device.UserAnnotation(self.am[id][0],self.am[id][1]) + if bm.type == 'kindle_bookmark': + mi = self.db.get_metadata(id, index_is_id=True) + user_notes_soup = self.generate_annotation_html(bm.value) + if mi.comments: + a_offset = mi.comments.find('
') + ad_offset = mi.comments.find('
') + + if a_offset >= 0: + mi.comments = mi.comments[:a_offset] + if ad_offset >= 0: + mi.comments = mi.comments[:ad_offset] + if set(mi.tags).intersection(ignore_tags): + continue + if mi.comments: + hrTag = Tag(user_notes_soup,'hr') + hrTag['class'] = 'annotations_divider' + user_notes_soup.insert(0,hrTag) + + mi.comments += user_notes_soup.prettify() + else: + mi.comments = unicode(user_notes_soup.prettify()) + # Update library comments + self.db.set_comment(id, mi.comments) + + # Update 'read' tag except for Catalogs/Clippings + if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD: + if not set(mi.tags).intersection(ignore_tags): + self.mark_book_as_read(id) + + # Add bookmark file to id + self.db.add_format_with_hooks(id, bm.value.bookmark_extension, + bm.value.path, index_is_id=True) + self.update_progress.emit(i) + elif bm.type == 'kindle_clippings': + # Find 'My Clippings' author=Kindle in database, or add + last_update = 'Last modified %s' % strftime(u'%x %X',bm.value['timestamp'].timetuple()) + mc_id = list(db.data.parse('title:"My Clippings"')) + if mc_id: + do_add_format(self.db, mc_id[0], 'TXT', bm.value['path']) + mi = self.db.get_metadata(mc_id[0], index_is_id=True) + mi.comments = last_update + self.db.set_metadata(mc_id[0], mi) + else: + mi = MetaInformation('My Clippings', authors = ['Kindle']) + mi.tags = ['Clippings'] + mi.comments = last_update + self.db.add_books([bm.value['path']], ['txt'], [mi]) + + self.update_done.emit() + self.done_callback(self.am.keys()) + + if not job.result: return + + if self.current_view() is not self.library_view: + return error_dialog(self, _('Use library only'), + _('User annotations generated from main library only'), + show=True) + db = self.library_view.model().db + + self.__annotation_updater = Updater(self, db, job.result, + Dispatcher(self.library_view.model().refresh_ids)) + self.__annotation_updater.start() + + # }}} + +class AddAction(object): # {{{ + + def __init__(self): + self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) + + def add_recursive(self, single): + root = choose_dir(self, 'recursive book import root dir dialog', + 'Select root folder') + if not root: + return + from calibre.gui2.add import Adder + self._adder = Adder(self, + self.library_view.model().db, + Dispatcher(self._files_added), spare_server=self.spare_server) + self._adder.add_recursive(root, single) + + def add_recursive_single(self, *args): + ''' + Add books from the local filesystem to either the library or the device + recursively assuming one book per folder. + ''' + self.add_recursive(True) + + def add_recursive_multiple(self, *args): + ''' + Add books from the local filesystem to either the library or the device + recursively assuming multiple books per folder. + ''' + self.add_recursive(False) + + def add_empty(self, *args): + ''' + Add an empty book item to the library. This does not import any formats + from a book file. + ''' + num, ok = QInputDialog.getInt(self, _('How many empty books?'), + _('How many empty books should be added?'), 1, 1, 100) + if ok: + from calibre.ebooks.metadata import MetaInformation + for x in xrange(num): + self.library_view.model().db.import_book(MetaInformation(None), []) + self.library_view.model().books_added(num) + + def files_dropped(self, paths): + to_device = self.stack.currentIndex() != 0 + self._add_books(paths, to_device) + + def files_dropped_on_book(self, event, paths): + accept = False + if self.current_view() is not self.library_view: + return + db = self.library_view.model().db + current_idx = self.library_view.currentIndex() + if not current_idx.isValid(): return + cid = db.id(current_idx.row()) + for path in paths: + ext = os.path.splitext(path)[1].lower() + if ext: + ext = ext[1:] + if ext in IMAGE_EXTENSIONS: + pmap = QPixmap() + pmap.load(path) + if not pmap.isNull(): + accept = True + db.set_cover(cid, pmap) + elif ext in BOOK_EXTENSIONS: + db.add_format_with_hooks(cid, ext, path, index_is_id=True) + accept = True + if accept: + event.accept() + self.cover_cache.refresh([cid]) + self.library_view.model().current_changed(current_idx, current_idx) + + def __add_filesystem_book(self, paths, allow_device=True): + if isinstance(paths, basestring): + paths = [paths] + books = [path for path in map(os.path.abspath, paths) if os.access(path, + os.R_OK)] + + if books: + to_device = allow_device and self.stack.currentIndex() != 0 + self._add_books(books, to_device) + if to_device: + self.status_bar.show_message(\ + _('Uploading books to device.'), 2000) + + + def add_filesystem_book(self, paths, allow_device=True): + self._add_filesystem_book(paths, allow_device=allow_device) + + def add_books(self, *args): + ''' + Add books from the local filesystem to either the library or the device. + ''' + filters = [ + (_('Books'), BOOK_EXTENSIONS), + (_('EPUB Books'), ['epub']), + (_('LRF Books'), ['lrf']), + (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']), + (_('LIT Books'), ['lit']), + (_('MOBI Books'), ['mobi', 'prc', 'azw']), + (_('Topaz books'), ['tpz','azw1']), + (_('Text books'), ['txt', 'rtf']), + (_('PDF Books'), ['pdf']), + (_('Comics'), ['cbz', 'cbr', 'cbc']), + (_('Archives'), ['zip', 'rar']), + ] + to_device = self.stack.currentIndex() != 0 + if to_device: + filters = [(_('Supported books'), self.device_manager.device.FORMATS)] + + books = choose_files(self, 'add books dialog dir', 'Select books', + filters=filters) + if not books: + return + self._add_books(books, to_device) + + def _add_books(self, paths, to_device, on_card=None): + if on_card is None: + on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None + if not paths: + return + from calibre.gui2.add import Adder + self.__adder_func = partial(self._files_added, on_card=on_card) + self._adder = Adder(self, + None if to_device else self.library_view.model().db, + Dispatcher(self.__adder_func), spare_server=self.spare_server) + self._adder.add(paths) + + def _files_added(self, paths=[], names=[], infos=[], on_card=None): + if paths: + self.upload_books(paths, + list(map(ascii_filename, names)), + infos, on_card=on_card) + self.status_bar.show_message( + _('Uploading books to device.'), 2000) + if getattr(self._adder, 'number_of_books_added', 0) > 0: + self.library_view.model().books_added(self._adder.number_of_books_added) + if hasattr(self, 'db_images'): + self.db_images.reset() + if getattr(self._adder, 'merged_books', False): + books = u'\n'.join([x if isinstance(x, unicode) else + x.decode(preferred_encoding, 'replace') for x in + self._adder.merged_books]) + info_dialog(self, _('Merged some books'), + _('Some duplicates were found and merged into the ' + 'following existing books:'), det_msg=books, show=True) + if getattr(self._adder, 'critical', None): + det_msg = [] + 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, _('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 = None + # }}} + +class DeleteAction(object): # {{{ + + def _get_selected_formats(self, msg): + from calibre.gui2.dialogs.select_formats import SelectFormats + fmts = self.library_view.model().db.all_formats() + d = SelectFormats([x.lower() for x in fmts], msg, parent=self) + if d.exec_() != d.Accepted: + return None + return d.selected_formats + + def _get_selected_ids(self, err_title=_('Cannot delete')): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, err_title, _('No book selected')) + d.exec_() + return set([]) + return set(map(self.library_view.model().id, rows)) + + def delete_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + _('Choose formats to be deleted')) + if not fmts: + return + for id in ids: + for fmt in fmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + if ids: + self.tags_view.recount() + + def delete_all_but_selected_formats(self, *args): + ids = self._get_selected_ids() + if not ids: + return + fmts = self._get_selected_formats( + '

'+_('Choose formats not to be deleted')) + if fmts is None: + return + for id in ids: + bfmts = self.library_view.model().db.formats(id, index_is_id=True) + if bfmts is None: + continue + bfmts = set([x.lower() for x in bfmts.split(',')]) + rfmts = bfmts - set(fmts) + for fmt in rfmts: + self.library_view.model().db.remove_format(id, fmt, + index_is_id=True, notify=False) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + if ids: + self.tags_view.recount() + + + def delete_covers(self, *args): + ids = self._get_selected_ids() + if not ids: + return + for id in ids: + self.library_view.model().db.remove_cover(id) + self.library_view.model().refresh_ids(ids) + self.library_view.model().current_changed(self.library_view.currentIndex(), + self.library_view.currentIndex()) + + def delete_books(self, *args): + ''' + Delete selected books from device or library. + ''' + view = self.current_view() + rows = view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + return + if self.stack.currentIndex() == 0: + if not confirm('

'+_('The selected books will be ' + 'permanently deleted and the files ' + 'removed from your computer. Are you sure?') + +'

', 'library_delete_books', self): + return + ci = view.currentIndex() + row = None + if ci.isValid(): + row = ci.row() + ids_deleted = view.model().delete_books(rows) + for v in (self.memory_view, self.card_a_view, self.card_b_view): + if v is None: + continue + v.model().clear_ondevice(ids_deleted) + if row is not None: + ci = view.model().index(row, 0) + if ci.isValid(): + view.setCurrentIndex(ci) + sm = view.selectionModel() + sm.select(ci, sm.Select) + else: + if not confirm('

'+_('The selected books will be ' + 'permanently deleted ' + 'from your device. Are you sure?') + +'

', 'device_delete_books', self): + return + if self.stack.currentIndex() == 1: + view = self.memory_view + elif self.stack.currentIndex() == 2: + view = self.card_a_view + else: + view = self.card_b_view + paths = view.model().paths(rows) + job = self.remove_paths(paths) + self.delete_memory[job] = (paths, view.model()) + view.model().mark_for_deletion(job, rows) + self.status_bar.show_message(_('Deleting books from device.'), 1000) + + # }}} + +class EditMetadataAction(object): # {{{ + + def download_metadata(self, checked, covers=True, set_metadata=True, + set_social_metadata=None): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot download metadata'), + _('No books selected')) + d.exec_() + return + db = self.library_view.model().db + ids = [db.id(row.row()) for row in rows] + if set_social_metadata is None: + get_social_metadata = config['get_social_metadata'] + else: + get_social_metadata = set_social_metadata + from calibre.gui2.metadata import DownloadMetadata + self._download_book_metadata = DownloadMetadata(db, ids, + get_covers=covers, set_metadata=set_metadata, + get_social_metadata=get_social_metadata) + self._download_book_metadata.start() + if set_social_metadata is not None and set_social_metadata: + x = _('social metadata') + else: + x = _('covers') if covers and not set_metadata else _('metadata') + self.progress_indicator.start( + _('Downloading %s for %d book(s)')%(x, len(ids))) + self._book_metadata_download_check = QTimer(self) + self.connect(self._book_metadata_download_check, + SIGNAL('timeout()'), self.book_metadata_download_check, + Qt.QueuedConnection) + self._book_metadata_download_check.start(100) + + def book_metadata_download_check(self): + if self._download_book_metadata.is_alive(): + return + self._book_metadata_download_check.stop() + self.progress_indicator.stop() + cr = self.library_view.currentIndex().row() + x = self._download_book_metadata + self._download_book_metadata = None + if x.exception is None: + self.library_view.model().refresh_ids( + x.updated, cr) + if x.failures: + details = ['%s: %s'%(title, reason) for title, + reason in x.failures.values()] + details = '%s\n'%('\n'.join(details)) + warning_dialog(self, _('Failed to download some metadata'), + _('Failed to download metadata for the following:'), + det_msg=details).exec_() + else: + err = _('Failed to download metadata:') + error_dialog(self, _('Error'), err, det_msg=x.tb).exec_() + + + def edit_metadata(self, checked, bulk=None): + ''' + Edit metadata of selected books in library. + ''' + rows = self.library_view.selectionModel().selectedRows() + previous = self.library_view.currentIndex() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + + if bulk or (bulk is None and len(rows) > 1): + return self.edit_bulk_metadata(checked) + + def accepted(id): + self.library_view.model().refresh_ids([id]) + + for row in rows: + self._metadata_view_id = self.library_view.model().db.id(row.row()) + d = MetadataSingleDialog(self, row.row(), + self.library_view.model().db, + accepted_callback=accepted, + cancel_all=rows.index(row) < len(rows)-1) + self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), + self.metadata_view_format) + d.exec_() + if d.cancel_all: + break + if rows: + current = self.library_view.currentIndex() + m = self.library_view.model() + m.refresh_cover_cache(map(m.id, rows)) + if self.cover_flow: + self.cover_flow.dataChanged() + m.current_changed(current, previous) + self.tags_view.recount() + + def edit_bulk_metadata(self, checked): + ''' + Edit metadata of selected books in library in bulk. + ''' + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot edit metadata'), + _('No books selected')) + d.exec_() + return + if MetadataBulkDialog(self, rows, + self.library_view.model().db).changed: + self.library_view.model().resort(reset=False) + self.library_view.model().research() + self.tags_view.recount() + + # Merge books {{{ + def merge_books(self, safe_merge=False): + ''' + Merge selected books in library. + ''' + if self.stack.currentIndex() != 0: + return + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + return error_dialog(self, _('Cannot merge books'), + _('No books selected'), show=True) + if len(rows) < 2: + return error_dialog(self, _('Cannot merge books'), + _('At least two books must be selected for merging'), + show=True) + dest_id, src_books, src_ids = self.books_to_merge(rows) + if safe_merge: + if not confirm('

'+_( + 'All book formats and metadata from the selected books ' + 'will be added to the first selected book.

' + 'The second and subsequently selected books will not ' + 'be deleted or changed.

' + 'Please confirm you want to proceed.') + +'

', 'merge_books_safe', self): + return + self.add_formats(dest_id, src_books) + self.merge_metadata(dest_id, src_ids) + else: + if not confirm('

'+_( + 'All book formats and metadata from the selected books will be merged ' + 'into the first selected book.

' + 'After merger the second and ' + 'subsequently selected books will be deleted.

' + 'All book formats of the first selected book will be kept ' + 'and any duplicate formats in the second and subsequently selected books ' + 'will be permanently deleted from your computer.

' + 'Are you sure you want to proceed?') + +'

', 'merge_books', self): + return + if len(rows)>5: + if not confirm('

'+_('You are about to merge more than 5 books. ' + 'Are you sure you want to proceed?') + +'

', 'merge_too_many_books', self): + return + self.add_formats(dest_id, src_books) + self.merge_metadata(dest_id, src_ids) + self.delete_books_after_merge(src_ids) + # leave the selection highlight on first selected book + dest_row = rows[0].row() + for row in rows: + if row.row() < rows[0].row(): + dest_row -= 1 + ci = self.library_view.model().index(dest_row, 0) + if ci.isValid(): + self.library_view.setCurrentIndex(ci) + + def add_formats(self, dest_id, src_books, replace=False): + for src_book in src_books: + if src_book: + fmt = os.path.splitext(src_book)[-1].replace('.', '').upper() + with open(src_book, 'rb') as f: + self.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, + notify=False, replace=replace) + + def books_to_merge(self, rows): + src_books = [] + src_ids = [] + m = self.library_view.model() + for i, row in enumerate(rows): + id_ = m.id(row) + if i == 0: + dest_id = id_ + else: + src_ids.append(id_) + dbfmts = m.db.formats(id_, index_is_id=True) + if dbfmts: + for fmt in dbfmts.split(','): + src_books.append(m.db.format_abspath(id_, fmt, + index_is_id=True)) + return [dest_id, src_books, src_ids] + + def delete_books_after_merge(self, ids_to_delete): + self.library_view.model().delete_books_by_id(ids_to_delete) + + def merge_metadata(self, dest_id, src_ids): + db = self.library_view.model().db + dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True) + orig_dest_comments = dest_mi.comments + for src_id in src_ids: + src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) + if src_mi.comments and orig_dest_comments != src_mi.comments: + if not dest_mi.comments or len(dest_mi.comments) == 0: + dest_mi.comments = src_mi.comments + else: + dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments) + if src_mi.title and src_mi.title and (not dest_mi.title or + dest_mi.title == _('Unknown')): + dest_mi.title = src_mi.title + if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] == + _('Unknown')): + dest_mi.authors = src_mi.authors + dest_mi.author_sort = src_mi.author_sort + if src_mi.tags: + if not dest_mi.tags: + dest_mi.tags = src_mi.tags + else: + for tag in src_mi.tags: + dest_mi.tags.append(tag) + if src_mi.cover and not dest_mi.cover: + dest_mi.cover = src_mi.cover + if not dest_mi.publisher: + dest_mi.publisher = src_mi.publisher + if not dest_mi.rating: + dest_mi.rating = src_mi.rating + if not dest_mi.series: + dest_mi.series = src_mi.series + dest_mi.series_index = src_mi.series_index + db.set_metadata(dest_id, dest_mi, ignore_errors=False) + # }}} + + # }}} + +class SaveToDiskAction(object): # {{{ + + def save_single_format_to_disk(self, checked): + self.save_to_disk(checked, False, prefs['output_format']) + + def save_specific_format_disk(self, fmt): + self.save_to_disk(False, False, fmt) + + def save_to_single_dir(self, checked): + self.save_to_disk(checked, True) + + def save_single_fmt_to_single_dir(self, *args): + self.save_to_disk(False, single_dir=True, + single_format=prefs['output_format']) + + def save_to_disk(self, checked, single_dir=False, single_format=None): + rows = self.current_view().selectionModel().selectedRows() + if not rows or len(rows) == 0: + return error_dialog(self, _('Cannot save to disk'), + _('No books selected'), show=True) + path = choose_dir(self, 'save to disk dialog', + _('Choose destination directory')) + if not path: + return + + if self.current_view() is self.library_view: + from calibre.gui2.add import Saver + from calibre.library.save_to_disk import config + opts = config().parse() + if single_format is not None: + opts.formats = single_format + # Special case for Kindle annotation files + if single_format.lower() in ['mbp','pdr','tan']: + opts.to_lowercase = False + opts.save_cover = False + opts.write_opf = False + opts.template = opts.send_template + if single_dir: + opts.template = opts.template.split('/')[-1].strip() + if not opts.template: + opts.template = '{title} - {authors}' + self._saver = Saver(self, self.library_view.model().db, + Dispatcher(self._books_saved), rows, path, opts, + spare_server=self.spare_server) + + else: + paths = self.current_view().model().paths(rows) + self.device_manager.save_books( + Dispatcher(self.books_saved), paths, path) + + + def _books_saved(self, path, failures, error): + self._saver = None + if error: + return error_dialog(self, _('Error while saving'), + _('There was an error while saving.'), + error, show=True) + if failures: + failures = [u'%s\n\t%s'% + (title, '\n\t'.join(err.splitlines())) for title, err in + failures] + + warning_dialog(self, _('Could not save some books'), + _('Could not save some books') + ', ' + + _('Click the show details button to see which ones.'), + u'\n\n'.join(failures), show=True) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + + def books_saved(self, job): + if job.failed: + return self.device_job_exception(job) + + # }}} + +class GenerateCatalogAction(object): # {{{ + + def generate_catalog(self): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) < 2: + rows = xrange(self.library_view.model().rowCount(QModelIndex())) + ids = map(self.library_view.model().id, rows) + + dbspec = None + if not ids: + return error_dialog(self, _('No books selected'), + _('No books selected to generate catalog for'), + show=True) + + # Calling gui2.tools:generate_catalog() + ret = generate_catalog(self, dbspec, ids, self.device_manager.device) + if ret is None: + return + + func, args, desc, out, sync, title = ret + + fmt = os.path.splitext(out)[1][1:].upper() + job = self.job_manager.run_job( + Dispatcher(self.catalog_generated), func, args=args, + description=desc) + job.catalog_file_path = out + job.fmt = fmt + job.catalog_sync, job.catalog_title = sync, title + self.status_bar.show_message(_('Generating %s catalog...')%fmt) + + def catalog_generated(self, job): + if job.result: + # Search terms nulled catalog results + return error_dialog(self, _('No books found'), + _("No books to catalog\nCheck exclude tags"), + show=True) + if job.failed: + return self.job_exception(job) + id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) + self.library_view.model().reset() + if job.catalog_sync: + sync = dynamic.get('catalogs_to_be_synced', set([])) + sync.add(id) + dynamic.set('catalogs_to_be_synced', sync) + self.status_bar.show_message(_('Catalog generated.'), 3000) + self.sync_catalogs() + if job.fmt not in ['EPUB','MOBI']: + export_dir = choose_dir(self, _('Export Catalog Directory'), + _('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower())) + if export_dir: + destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) + shutil.copyfile(job.catalog_file_path, destination) + + # }}} + +class FetchNewsAction(object): # {{{ + + def download_scheduled_recipe(self, arg): + func, args, desc, fmt, temp_files = \ + fetch_scheduled_recipe(arg) + job = self.job_manager.run_job( + Dispatcher(self.scheduled_recipe_fetched), func, args=args, + description=desc) + self.conversion_jobs[job] = (temp_files, fmt, arg) + self.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000) + + def scheduled_recipe_fetched(self, job): + temp_files, fmt, arg = self.conversion_jobs.pop(job) + pt = temp_files[0] + if job.failed: + self.scheduler.recipe_download_failed(arg) + return self.job_exception(job) + id = self.library_view.model().add_news(pt.name, arg) + self.library_view.model().reset() + sync = dynamic.get('news_to_be_synced', set([])) + sync.add(id) + dynamic.set('news_to_be_synced', sync) + self.scheduler.recipe_downloaded(arg) + self.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) + self.email_news(id) + self.sync_news() + + # }}} + +class ConvertAction(object): # {{{ + + def auto_convert(self, book_ids, on_card, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted, extra_job_args=[on_card]) + + def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_mail, + extra_job_args=[delete_from_library, to, fmts]) + + def auto_convert_news(self, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_news) + + def auto_convert_catalogs(self, book_ids, format): + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) + if jobs == []: return + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_auto_converted_catalogs) + + def get_books_for_conversion(self): + rows = [r.row() for r in \ + self.library_view.selectionModel().selectedRows()] + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot convert'), + _('No books selected')) + d.exec_() + return None + return [self.library_view.model().db.id(r) for r in rows] + + def convert_ebook(self, checked, bulk=None): + book_ids = self.get_books_for_conversion() + if book_ids is None: return + previous = self.library_view.currentIndex() + rows = [x.row() for x in \ + self.library_view.selectionModel().selectedRows()] + num = 0 + if bulk or (bulk is None and len(book_ids) > 1): + self.__bulk_queue = convert_bulk_ebook(self, self.queue_convert_jobs, + self.library_view.model().db, book_ids, + out_format=prefs['output_format'], args=(rows, previous, + self.book_converted)) + if self.__bulk_queue is None: + return + num = len(self.__bulk_queue.book_ids) + else: + jobs, changed, bad = convert_single_ebook(self, + self.library_view.model().db, book_ids, out_format=prefs['output_format']) + self.queue_convert_jobs(jobs, changed, bad, rows, previous, + self.book_converted) + num = len(jobs) + + if num > 0: + self.status_bar.show_message(_('Starting conversion of %d book(s)') % + num, 2000) + + def queue_convert_jobs(self, jobs, changed, bad, rows, previous, + converted_func, extra_job_args=[]): + for func, args, desc, fmt, id, temp_files in jobs: + if id not in bad: + job = self.job_manager.run_job(Dispatcher(converted_func), + func, args=args, description=desc) + args = [temp_files, fmt, id]+extra_job_args + self.conversion_jobs[job] = tuple(args) + + if changed: + self.library_view.model().refresh_rows(rows) + current = self.library_view.currentIndex() + self.library_view.model().current_changed(current, previous) + + def book_auto_converted(self, job): + temp_files, fmt, book_id, on_card = self.conversion_jobs[job] + self.book_converted(job) + self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_mail(self, job): + temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] + self.book_converted(job) + self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_news(self, job): + temp_files, fmt, book_id = self.conversion_jobs[job] + self.book_converted(job) + self.sync_news(send_ids=[book_id], do_auto_convert=False) + + def book_auto_converted_catalogs(self, job): + temp_files, fmt, book_id = self.conversion_jobs[job] + self.book_converted(job) + self.sync_catalogs(send_ids=[book_id], do_auto_convert=False) + + def book_converted(self, job): + temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3] + try: + if job.failed: + self.job_exception(job) + return + data = open(temp_files[-1].name, 'rb') + self.library_view.model().db.add_format(book_id, \ + fmt, data, index_is_id=True) + data.close() + self.status_bar.show_message(job.description + \ + (' completed'), 2000) + finally: + for f in temp_files: + try: + if os.path.exists(f.name): + os.remove(f.name) + except: + pass + self.tags_view.recount() + if self.current_view() is self.library_view: + current = self.library_view.currentIndex() + self.library_view.model().current_changed(current, QModelIndex()) + + # }}} + +class ViewAction(object): # {{{ + + def view_format(self, row, format): + fmt_path = self.library_view.model().db.format_abspath(row, format) + if fmt_path: + self._view_file(fmt_path) + + def metadata_view_format(self, fmt): + fmt_path = self.library_view.model().db.\ + format_abspath(self._metadata_view_id, + fmt, index_is_id=True) + if fmt_path: + self._view_file(fmt_path) + + + def book_downloaded_for_viewing(self, job): + if job.failed: + self.device_job_exception(job) + return + self._view_file(job.result) + + def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True): + self.setCursor(Qt.BusyCursor) + try: + if internal: + args = [viewer] + if isosx and 'ebook' in viewer: + args.append('--raise-window') + if name is not None: + args.append(name) + self.job_manager.launch_gui_app(viewer, + kwargs=dict(args=args)) + else: + paths = os.environ.get('LD_LIBRARY_PATH', + '').split(os.pathsep) + paths = [x for x in paths if x] + if isfrozen and islinux and paths: + npaths = [x for x in paths if x != sys.frozen_path] + os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths) + QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name) + if isfrozen and islinux and paths: + os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) + time.sleep(2) # User feedback + finally: + self.unsetCursor() + + def _view_file(self, name): + ext = os.path.splitext(name)[1].upper().replace('.', '') + viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer' + internal = ext in config['internally_viewed_formats'] + self._launch_viewer(name, viewer, internal) + + def view_specific_format(self, triggered): + rows = self.library_view.selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot view'), _('No book selected')) + d.exec_() + return + + row = rows[0].row() + formats = self.library_view.model().db.formats(row).upper().split(',') + d = ChooseFormatDialog(self, _('Choose the format to view'), formats) + if d.exec_() == QDialog.Accepted: + format = d.format() + self.view_format(row, format) + + def _view_check(self, num, max_=3): + if num <= max_: + return True + return question_dialog(self, _('Multiple Books Selected'), + _('You are attempting to open %d books. Opening too many ' + 'books at once can be slow and have a negative effect on the ' + 'responsiveness of your computer. Once started the process ' + 'cannot be stopped until complete. Do you wish to continue?' + ) % num) + + def view_folder(self, *args): + rows = self.current_view().selectionModel().selectedRows() + if not rows or len(rows) == 0: + d = error_dialog(self, _('Cannot open folder'), + _('No book selected')) + d.exec_() + return + if not self._view_check(len(rows)): + return + for row in rows: + path = self.library_view.model().db.abspath(row.row()) + QDesktopServices.openUrl(QUrl.fromLocalFile(path)) + + + def view_book(self, triggered): + rows = self.current_view().selectionModel().selectedRows() + self._view_books(rows) + + def view_specific_book(self, index): + self._view_books([index]) + + def _view_books(self, rows): + if not rows or len(rows) == 0: + self._launch_viewer() + return + + if not self._view_check(len(rows)): + return + + if self.current_view() is self.library_view: + for row in rows: + if hasattr(row, 'row'): + row = row.row() + + formats = self.library_view.model().db.formats(row) + title = self.library_view.model().db.title(row) + if not formats: + error_dialog(self, _('Cannot view'), + _('%s has no available formats.')%(title,), show=True) + continue + + formats = formats.upper().split(',') + + + in_prefs = False + for format in prefs['input_format_order']: + if format in formats: + in_prefs = True + self.view_format(row, format) + break + if not in_prefs: + self.view_format(row, formats[0]) + else: + paths = self.current_view().model().paths(rows) + for path in paths: + pt = PersistentTemporaryFile('_viewer_'+\ + os.path.splitext(path)[1]) + self.persistent_files.append(pt) + pt.close() + self.device_manager.view_book(\ + Dispatcher(self.book_downloaded_for_viewing), + path, pt.name) + + # }}} + diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index f06d912a5d..6a9709cd8b 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -83,7 +83,6 @@ if pictureflow is not None: self.setFocusPolicy(Qt.WheelFocus) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) - self.setZoomFactor(150) def sizeHint(self): return self.minimumSize() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 9d7cf86dac..cf54e6c1f3 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -3,24 +3,26 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' # Imports {{{ -import os, traceback, Queue, time, socket, cStringIO, re +import os, traceback, Queue, time, socket, cStringIO, re, sys from threading import Thread, RLock from itertools import repeat from functools import partial from binascii import unhexlify from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \ - Qt, pyqtSignal + Qt, pyqtSignal, QColor, QPainter +from PyQt4.QtSvg import QSvgRenderer from calibre.customize.ui import available_input_formats, available_output_formats, \ device_plugins from calibre.devices.interface import DevicePlugin +from calibre.devices.errors import UserFeedback from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.utils.ipc.job import BaseJob from calibre.devices.scanner import DeviceScanner from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \ - pixmap_to_data, warning_dialog, \ - question_dialog + pixmap_to_data, warning_dialog, \ + question_dialog, info_dialog, choose_dir from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string from calibre import preferred_encoding, prints from calibre.utils.filenames import ascii_filename @@ -597,10 +599,204 @@ class Emailer(Thread): # {{{ # }}} -class DeviceMixin(object): +class DeviceMixin(object): # {{{ def __init__(self): self.db_book_uuid_cache = set() + self.device_error_dialog = error_dialog(self, _('Error'), + _('Error communicating with device'), ' ') + self.device_error_dialog.setModal(Qt.NonModal) + self.device_connected = None + self.emailer = Emailer() + self.emailer.start() + self.device_manager = DeviceManager(Dispatcher(self.device_detected), + self.job_manager, Dispatcher(self.status_bar.show_message)) + self.device_manager.start() + + def set_default_thumbnail(self, height): + r = QSvgRenderer(I('book.svg')) + pixmap = QPixmap(height, height) + pixmap.fill(QColor(255,255,255)) + p = QPainter(pixmap) + r.render(p) + p.end() + self.default_thumbnail = (pixmap.width(), pixmap.height(), + pixmap_to_data(pixmap)) + + def connect_to_folder(self): + dir = choose_dir(self, 'Select Device Folder', + _('Select folder to open as device')) + if dir is not None: + self.device_manager.connect_to_folder(dir) + + def disconnect_from_folder(self): + self.device_manager.disconnect_folder() + + def _sync_action_triggered(self, *args): + m = getattr(self, '_sync_menu', None) + if m is not None: + m.trigger_default() + + def create_device_menu(self): + self._sync_menu = DeviceMenu(self) + self.action_sync.setMenu(self._sync_menu) + self.connect(self._sync_menu, + SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), + self.dispatch_sync_event) + self._sync_menu.fetch_annotations.connect(self.fetch_annotations) + self._sync_menu.connect_to_folder.connect(self.connect_to_folder) + self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder) + if self.device_connected: + self._sync_menu.connect_to_folder_action.setEnabled(False) + if self.device_connected == 'folder': + self._sync_menu.disconnect_from_folder_action.setEnabled(True) + else: + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + else: + self._sync_menu.connect_to_folder_action.setEnabled(True) + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + + + + def device_job_exception(self, job): + ''' + Handle exceptions in threaded device jobs. + ''' + if isinstance(getattr(job, 'exception', None), UserFeedback): + ex = job.exception + func = {UserFeedback.ERROR:error_dialog, + UserFeedback.WARNING:warning_dialog, + UserFeedback.INFO:info_dialog}[ex.level] + return func(self, _('Failed'), ex.msg, det_msg=ex.details if + ex.details else '', show=True) + + try: + if 'Could not read 32 bytes on the control bus.' in \ + unicode(job.details): + error_dialog(self, _('Error talking to device'), + _('There was a temporary error talking to the ' + 'device. Please unplug and reconnect the device ' + 'and or reboot.')).show() + return + except: + pass + try: + prints(job.details, file=sys.stderr) + except: + pass + if not self.device_error_dialog.isVisible(): + self.device_error_dialog.setDetailedText(job.details) + self.device_error_dialog.show() + + # Device connected {{{ + def device_detected(self, connected, is_folder_device): + ''' + Called when a device is connected to the computer. + ''' + if connected: + self._sync_menu.connect_to_folder_action.setEnabled(False) + if is_folder_device: + self._sync_menu.disconnect_from_folder_action.setEnabled(True) + self.device_manager.get_device_information(\ + Dispatcher(self.info_read)) + self.set_default_thumbnail(\ + self.device_manager.device.THUMBNAIL_HEIGHT) + self.status_bar.show_message(_('Device: ')+\ + self.device_manager.device.__class__.get_gui_name()+\ + _(' detected.'), 3000) + self.device_connected = 'device' if not is_folder_device else 'folder' + self._sync_menu.enable_device_actions(True, + self.device_manager.device.card_prefix(), + self.device_manager.device) + self.location_view.model().device_connected(self.device_manager.device) + self.eject_action.setEnabled(True) + self.refresh_ondevice_info (device_connected = True, reset_only = True) + else: + self._sync_menu.connect_to_folder_action.setEnabled(True) + self._sync_menu.disconnect_from_folder_action.setEnabled(False) + self.device_connected = None + self._sync_menu.enable_device_actions(False) + self.location_view.model().update_devices() + self.vanity.setText(self.vanity_template%\ + dict(version=self.latest_version, device=' ')) + self.device_info = ' ' + if self.current_view() != self.library_view: + self.book_details.reset_info() + self.location_view.setCurrentIndex(self.location_view.model().index(0)) + self.eject_action.setEnabled(False) + self.refresh_ondevice_info (device_connected = False) + + def info_read(self, job): + ''' + Called once device information has been read. + ''' + if job.failed: + return self.device_job_exception(job) + info, cp, fs = job.result + self.location_view.model().update_devices(cp, fs) + self.device_info = _('Connected ')+info[0] + self.vanity.setText(self.vanity_template%\ + dict(version=self.latest_version, device=self.device_info)) + + self.device_manager.books(Dispatcher(self.metadata_downloaded)) + + def metadata_downloaded(self, job): + ''' + Called once metadata has been read for all books on the device. + ''' + if job.failed: + self.device_job_exception(job) + return + self.set_books_in_library(job.result, reset=True) + mainlist, cardalist, cardblist = job.result + self.memory_view.set_database(mainlist) + self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_a_view.set_database(cardalist) + self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.card_b_view.set_database(cardblist) + self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) + self.sync_news() + self.sync_catalogs() + self.refresh_ondevice_info(device_connected = True) + + def refresh_ondevice_info(self, device_connected, reset_only = False): + ''' + Force the library view to refresh, taking into consideration + books information + ''' + self.book_on_device(None, reset=True) + if reset_only: + return + self.library_view.set_device_connected(device_connected) + + # }}} + + def remove_paths(self, paths): + return self.device_manager.delete_books( + Dispatcher(self.books_deleted), paths) + + def books_deleted(self, job): + ''' + Called once deletion is done on the device + ''' + for view in (self.memory_view, self.card_a_view, self.card_b_view): + view.model().deletion_done(job, job.failed) + if job.failed: + self.device_job_exception(job) + return + + if self.delete_memory.has_key(job): + paths, model = self.delete_memory.pop(job) + self.device_manager.remove_books_from_metadata(paths, + self.booklists()) + model.paths_deleted(paths) + self.upload_booklists() + # Clear the ondevice info so it will be recomputed + self.book_on_device(None, None, reset=True) + # We want to reset all the ondevice flags in the library. Use a big + # hammer, so we don't need to worry about whether some succeeded or not + self.library_view.model().refresh() + def dispatch_sync_event(self, dest, delete, specific): rows = self.library_view.selectionModel().selectedRows() @@ -1220,3 +1416,6 @@ class DeviceMixin(object): # Correct the metadata cache on device. if self.device_manager.is_device_connected: self.device_manager.sync_booklists(None, booklists) + + # }}} + diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 5f42636dad..90afbbf0a2 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -47,7 +47,7 @@ class ToolbarMixin(object): # {{{ def __init__(self): md = QMenu() md.addAction(_('Edit metadata individually'), - partial(self.edit_metadata, False)) + partial(self.edit_metadata, False, bulk=False)) md.addSeparator() md.addAction(_('Edit metadata in bulk'), partial(self.edit_metadata, False, bulk=True)) diff --git a/src/calibre/gui2/pictureflow/pictureflow.cpp b/src/calibre/gui2/pictureflow/pictureflow.cpp index 60985a1a12..58b6cd32e0 100644 --- a/src/calibre/gui2/pictureflow/pictureflow.cpp +++ b/src/calibre/gui2/pictureflow/pictureflow.cpp @@ -85,7 +85,9 @@ typedef long PFreal; typedef unsigned short QRgb565; -#define FONT_SIZE 18 +#define REFLECTION_FACTOR 1.5 + +#define MAX(x, y) ((x > y) ? x : y) #define RGB565_RED_MASK 0xF800 #define RGB565_GREEN_MASK 0x07E0 @@ -124,6 +126,7 @@ inline PFreal floatToFixed(float val) return (PFreal)(val*PFREAL_ONE); } +// sinTable {{{ #define IANGLE_MAX 1024 #define IANGLE_MASK 1023 @@ -293,6 +296,7 @@ int main(int, char**) return 0; } #endif +// }}} inline PFreal fsin(int iangle) { @@ -315,6 +319,8 @@ struct SlideInfo PFreal cy; }; +// PicturePlowPrivate {{{ + class PictureFlowPrivate { public: @@ -369,6 +375,7 @@ private: int slideWidth; int slideHeight; + int fontSize; int zoom; int queueLength; @@ -406,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_) slideWidth = 200; slideHeight = 200; + fontSize = 10; zoom = 100; centerIndex = 0; @@ -542,8 +550,11 @@ void PictureFlowPrivate::showSlide(int index) void PictureFlowPrivate::resize(int w, int h) { - slideHeight = int(float(h)/2.); + if (w < 10) w = 10; + if (h < 10) h = 10; + slideHeight = int(float(h)/REFLECTION_FACTOR); slideWidth = int(float(slideHeight) * 2/3.); + fontSize = MAX(int(h/20.), 12); recalc(w, h); resetSlides(); triggerRender(); @@ -592,8 +603,8 @@ static QImage prepareSurface(QImage img, int w, int h) img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode); // slightly larger, to accomodate for the reflection - int hs = h * 2; - int hofs = h / 3; + int hs = int(h * REFLECTION_FACTOR); + int hofs = 0; // offscreen buffer: black is sweet QImage result(hs, w, QImage::Format_RGB16); @@ -715,13 +726,13 @@ void PictureFlowPrivate::render() QFont font = QFont(); font.setBold(true); - font.setPointSize(FONT_SIZE); + font.setPixelSize(fontSize); painter.setFont(font); painter.setPen(Qt::white); //painter.setPen(QColor(255,255,255,127)); if (centerIndex < slideCount() && centerIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3), Qt::AlignCenter, slideImages->caption(centerIndex)); painter.end(); @@ -766,7 +777,7 @@ void PictureFlowPrivate::render() QFont font = QFont(); font.setBold(true); - font.setPointSize(FONT_SIZE); + font.setPixelSize(fontSize); painter.setFont(font); int leftTextIndex = (step>0) ? centerIndex : centerIndex-1; @@ -774,12 +785,12 @@ void PictureFlowPrivate::render() painter.setPen(QColor(255,255,255, (255-fade) )); if (leftTextIndex < sc && leftTextIndex > -1) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3), Qt::AlignCenter, slideImages->caption(leftTextIndex)); painter.setPen(QColor(255,255,255, fade)); if (leftTextIndex+1 < sc && leftTextIndex > -2) - painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3), + painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3), Qt::AlignCenter, slideImages->caption(leftTextIndex+1)); @@ -893,7 +904,7 @@ int col1, int col2) int center = (sh*BILINEAR_STRETCH_VER/2); int dy = dist*BILINEAR_STRETCH_VER / h; #else - int center = (sh/2); + int center = sh/2; int dy = dist / h; #endif int p1 = center*PFREAL_ONE - dy/2; @@ -1110,8 +1121,9 @@ void PictureFlowPrivate::clearSurfaceCache() surfaceCache.clear(); } -// ----------------------------------------- +// }}} +// PictureFlow {{{ PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent) { d = new PictureFlowPrivate(this, queueLength); @@ -1387,3 +1399,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); } int FlowImages::count() { return 0; } QImage FlowImages::image(int index) { index=0; return QImage(); } QString FlowImages::caption(int index) {index=0; return QString(); } + +// }}} diff --git a/src/calibre/gui2/status.py b/src/calibre/gui2/status.py index 50a256ef2d..06c3e9c85f 100644 --- a/src/calibre/gui2/status.py +++ b/src/calibre/gui2/status.py @@ -2,9 +2,10 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' import os, collections -from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ - QSizePolicy, QScrollArea -from PyQt4.QtCore import Qt, QSize, pyqtSignal +from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \ + QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \ + QPropertyAnimation, QEasingCurve + from calibre import fit_image, preferred_encoding, isosx from calibre.gui2 import config @@ -50,6 +51,10 @@ class BookInfoDisplay(QWidget): def __init__(self, coverpath=I('book.svg')): QLabel.__init__(self) + self.animation = QPropertyAnimation(self, 'size', self) + self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) + self.animation.setDuration(1000) + self.animation.setStartValue(QSize(0, 0)) self.setMaximumWidth(81) self.setMaximumHeight(108) self.default_pixmap = QPixmap(coverpath) @@ -58,6 +63,7 @@ class BookInfoDisplay(QWidget): self.setPixmap(self.default_pixmap) def do_layout(self): + self.animation.stop() pixmap = self.pixmap() pwidth, pheight = pixmap.width(), pixmap.height() width, height = fit_image(pwidth, pheight, @@ -68,11 +74,12 @@ class BookInfoDisplay(QWidget): except ZeroDivisionError: aspect_ratio = 1 self.setMaximumWidth(int(aspect_ratio*self.maximumHeight())) + self.animation.setEndValue(self.maximumSize()) def setPixmap(self, pixmap): QLabel.setPixmap(self, pixmap) self.do_layout() - + self.animation.start() def sizeHint(self): return QSize(self.maximumWidth(), self.maximumHeight()) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 5824171213..682ede1978 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -9,58 +9,44 @@ __docformat__ = 'restructuredtext en' '''The main GUI''' -import collections, datetime, os, shutil, sys, textwrap, time -from xml.parsers.expat import ExpatError +import collections, os, sys, textwrap, time from Queue import Queue, Empty from threading import Thread -from functools import partial from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \ - QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ + QPixmap, QMenu, QIcon, pyqtSignal, \ QDialog, QDesktopServices, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \ - QMessageBox, QHelpEvent, QInputDialog,\ - QThread, pyqtSignal -from PyQt4.QtSvg import QSvgRenderer + QMessageBox, QHelpEvent -from calibre import prints, patheq, strftime -from calibre.constants import __version__, __appname__, isfrozen, islinux, \ - iswindows, isosx, filesystem_encoding, preferred_encoding -from calibre.utils.filenames import ascii_filename +from calibre import prints, patheq +from calibre.constants import __version__, __appname__, isosx from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import prefs, dynamic from calibre.utils.ipc.server import Server -from calibre.devices.errors import UserFeedback -from calibre.gui2 import warning_dialog, choose_files, error_dialog, \ - question_dialog,\ - pixmap_to_data, choose_dir, \ - Dispatcher, gprefs, \ - max_available_height, config, info_dialog, \ - GetMetadata +from calibre.gui2 import error_dialog, GetMetadata, \ + gprefs, max_available_height, config, info_dialog from calibre.gui2.cover_flow import CoverFlowMixin -from calibre.gui2.widgets import ProgressIndicator, IMAGE_EXTENSIONS +from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.wizard import move_library from calibre.gui2.dialogs.scheduler import Scheduler -from calibre.gui2.update import CheckForUpdates +from calibre.gui2.update import UpdateMixin from calibre.gui2.main_window import MainWindow from calibre.gui2.main_ui import Ui_MainWindow -from calibre.gui2.device import DeviceManager, DeviceMenu, DeviceMixin, Emailer +from calibre.gui2.device import DeviceMixin from calibre.gui2.jobs import JobManager, JobsDialog, JobsButton -from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog -from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog -from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \ - fetch_scheduled_recipe, generate_catalog from calibre.gui2.dialogs.config import ConfigDialog -from calibre.gui2.dialogs.choose_format import ChooseFormatDialog + from calibre.gui2.dialogs.book_info import BookInfo -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString from calibre.library.database2 import LibraryDatabase2 from calibre.library.caches import CoverCache -from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin from calibre.gui2.tag_view import TagBrowserMixin +from calibre.gui2.actions import AnnotationsAction, AddAction, DeleteAction, \ + EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, \ + ConvertAction, ViewAction + class Listener(Thread): # {{{ @@ -91,35 +77,29 @@ class Listener(Thread): # {{{ class SystemTrayIcon(QSystemTrayIcon): # {{{ + tooltip_requested = pyqtSignal(object) + def __init__(self, icon, parent): QSystemTrayIcon.__init__(self, icon, parent) def event(self, ev): if ev.type() == ev.ToolTip: evh = QHelpEvent(ev) - self.emit(SIGNAL('tooltip_requested(PyQt_PyObject)'), + self.tooltip_requested.emit( (self, evh.globalPos())) return True return QSystemTrayIcon.event(self, ev) # }}} -class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, +class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{ TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin, - SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin): + SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin, + AnnotationsAction, AddAction, DeleteAction, + EditMetadataAction, SaveToDiskAction, GenerateCatalogAction, FetchNewsAction, + ConvertAction, ViewAction): 'The main GUI' - def set_default_thumbnail(self, height): - r = QSvgRenderer(I('book.svg')) - pixmap = QPixmap(height, height) - pixmap.fill(QColor(255,255,255)) - p = QPainter(pixmap) - r.render(p) - p.end() - self.default_thumbnail = (pixmap.width(), pixmap.height(), - pixmap_to_data(pixmap)) - - self.last_time = datetime.datetime.now() def __init__(self, opts, parent=None): MainWindow.__init__(self, opts, parent) @@ -127,7 +107,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def initialize(self, library_path, db, listener, actions): opts = self.opts - self.last_time = datetime.datetime.now() self.preferences_action, self.quit_action = actions self.library_path = library_path self.spare_servers = [] @@ -152,6 +131,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # }}} LayoutMixin.__init__(self) + DeviceMixin.__init__(self) self.restriction_count_of_books_in_view = 0 self.restriction_count_of_books_in_library = 0 @@ -160,25 +140,18 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.progress_indicator = ProgressIndicator(self) self.verbose = opts.verbose self.get_metadata = GetMetadata() - self.emailer = Emailer() - self.emailer.start() self.upload_memory = {} self.delete_memory = {} self.conversion_jobs = {} self.persistent_files = [] self.metadata_dialogs = [] self.default_thumbnail = None - self.device_error_dialog = error_dialog(self, _('Error'), - _('Error communicating with device'), ' ') - self.device_error_dialog.setModal(Qt.NonModal) self.tb_wrapper = textwrap.TextWrapper(width=40) - self.device_connected = None self.viewers = collections.deque() self.content_server = None self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) self.system_tray_icon.setToolTip('calibre') - self.connect(self.system_tray_icon, - SIGNAL('tooltip_requested(PyQt_PyObject)'), + self.system_tray_icon.tooltip_requested.connect( self.job_manager.show_tooltip) if not config['systray_icon']: self.system_tray_icon.hide() @@ -216,17 +189,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), self.system_tray_icon_activated) - DeviceMixin.__init__(self) ####################### Start spare job server ######################## QTimer.singleShot(1000, self.add_spare_server) - ####################### Setup device detection ######################## - self.device_manager = DeviceManager(Dispatcher(self.device_detected), - self.job_manager, Dispatcher(self.status_bar.show_message)) - self.device_manager.start() - - ####################### Location View ######################## QObject.connect(self.location_view, SIGNAL('location_selected(PyQt_PyObject)'), @@ -248,12 +214,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.latest_version = ' ' self.vanity.setText(self.vanity_template%dict(version=' ', device=' ')) self.device_info = ' ' - if not opts.no_update_check: - self.update_checker = CheckForUpdates(self) - self.update_checker.update_found.connect(self.update_found, - type=Qt.QueuedConnection) - self.update_checker.start() - + UpdateMixin.__init__(self, opts) ####################### Status Bar ##################### self.status_bar.initialize(self.system_tray_icon) self.book_details.show_book_info.connect(self.show_book_info) @@ -332,8 +293,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.location_view.setCurrentIndex(self.location_view.model().index(0)) - self._add_filesystem_book = Dispatcher(self.__add_filesystem_book) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) + AddAction.__init__(self) self.read_settings() self.finalize_layout() @@ -342,39 +303,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, MainWindow.resizeEvent(self, ev) self.search.setMaximumWidth(self.width()-150) - def connect_to_folder(self): - dir = choose_dir(self, 'Select Device Folder', - _('Select folder to open as device')) - if dir is not None: - self.device_manager.connect_to_folder(dir) - - def disconnect_from_folder(self): - self.device_manager.disconnect_folder() - - def _sync_action_triggered(self, *args): - m = getattr(self, '_sync_menu', None) - if m is not None: - m.trigger_default() - - def create_device_menu(self): - self._sync_menu = DeviceMenu(self) - self.action_sync.setMenu(self._sync_menu) - self.connect(self._sync_menu, - SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), - self.dispatch_sync_event) - self._sync_menu.fetch_annotations.connect(self.fetch_annotations) - self._sync_menu.connect_to_folder.connect(self.connect_to_folder) - self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder) - if self.device_connected: - self._sync_menu.connect_to_folder_action.setEnabled(False) - if self.device_connected == 'folder': - self._sync_menu.disconnect_from_folder_action.setEnabled(True) - else: - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - else: - self._sync_menu.connect_to_folder_action.setEnabled(True) - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - def add_spare_server(self, *args): self.spare_servers.append(Server(limit=int(config['worker_limit']/2.0))) @@ -457,1307 +385,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, def booklists(self): return self.memory_view.model().db, self.card_a_view.model().db, self.card_b_view.model().db - ########################## Connect to device ############################## - - def save_device_view_settings(self): - model = self.location_view.model() - return - #self.memory_view.write_settings() - for x in range(model.rowCount()): - if x > 1: - if model.location_for_row(x) == 'carda': - self.card_a_view.write_settings() - elif model.location_for_row(x) == 'cardb': - self.card_b_view.write_settings() - - def device_detected(self, connected, is_folder_device): - ''' - Called when a device is connected to the computer. - ''' - if connected: - self._sync_menu.connect_to_folder_action.setEnabled(False) - if is_folder_device: - self._sync_menu.disconnect_from_folder_action.setEnabled(True) - self.device_manager.get_device_information(\ - Dispatcher(self.info_read)) - self.set_default_thumbnail(\ - self.device_manager.device.THUMBNAIL_HEIGHT) - self.status_bar.show_message(_('Device: ')+\ - self.device_manager.device.__class__.get_gui_name()+\ - _(' detected.'), 3000) - self.device_connected = 'device' if not is_folder_device else 'folder' - self._sync_menu.enable_device_actions(True, - self.device_manager.device.card_prefix(), - self.device_manager.device) - self.location_view.model().device_connected(self.device_manager.device) - self.eject_action.setEnabled(True) - self.refresh_ondevice_info (device_connected = True, reset_only = True) - else: - self._sync_menu.connect_to_folder_action.setEnabled(True) - self._sync_menu.disconnect_from_folder_action.setEnabled(False) - self.save_device_view_settings() - self.device_connected = None - self._sync_menu.enable_device_actions(False) - self.location_view.model().update_devices() - self.vanity.setText(self.vanity_template%\ - dict(version=self.latest_version, device=' ')) - self.device_info = ' ' - if self.current_view() != self.library_view: - self.book_details.reset_info() - self.location_view.setCurrentIndex(self.location_view.model().index(0)) - self.eject_action.setEnabled(False) - self.refresh_ondevice_info (device_connected = False) - - def info_read(self, job): - ''' - Called once device information has been read. - ''' - if job.failed: - return self.device_job_exception(job) - info, cp, fs = job.result - self.location_view.model().update_devices(cp, fs) - self.device_info = _('Connected ')+info[0] - self.vanity.setText(self.vanity_template%\ - dict(version=self.latest_version, device=self.device_info)) - - self.device_manager.books(Dispatcher(self.metadata_downloaded)) - - def metadata_downloaded(self, job): - ''' - Called once metadata has been read for all books on the device. - ''' - if job.failed: - if isinstance(job.exception, ExpatError): - error_dialog(self, _('Device database corrupted'), - _(''' -

The database of books on the reader is corrupted. Try the following: -

    -
  1. Unplug the reader. Wait for it to finish regenerating the database (i.e. wait till it is ready to be used). Plug it back in. Now it should work with %(app)s. If not try the next step.
  2. -
  3. Quit %(app)s. Find the file media.xml in the reader's main memory. Delete it. Unplug the reader. Wait for it to regenerate the file. Re-connect it and start %(app)s.
  4. -
- ''')%dict(app=__appname__)).exec_() - else: - self.device_job_exception(job) - return - self.set_books_in_library(job.result, reset=True) - mainlist, cardalist, cardblist = job.result - self.memory_view.set_database(mainlist) - self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.card_a_view.set_database(cardalist) - self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.card_b_view.set_database(cardblist) - self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA) - self.sync_news() - self.sync_catalogs() - self.refresh_ondevice_info(device_connected = True) - - ############################################################################ - ### Force the library view to refresh, taking into consideration books information - def refresh_ondevice_info(self, device_connected, reset_only = False): - self.book_on_device(None, reset=True) - if reset_only: - return - self.library_view.set_device_connected(device_connected) - ############################################################################ - - ######################### Fetch annotations ################################ - - def fetch_annotations(self, *args): - # Generate a path_map from selected ids - def get_ids_from_selected_rows(): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - return ids - - def get_formats(id): - formats = db.formats(id, index_is_id=True) - fmts = [] - if formats: - for format in formats.split(','): - fmts.append(format.lower()) - return fmts - - def generate_annotation_paths(ids, db, device): - # Generate path templates - # Individual storage mount points scanned/resolved in driver.get_annotations() - path_map = {} - for id in ids: - mi = db.get_metadata(id, index_is_id=True) - a_path = device.create_upload_path(os.path.abspath('/'), mi, 'x.bookmark', create_dirs=False) - path_map[id] = dict(path=a_path, fmts=get_formats(id)) - return path_map - - device = self.device_manager.device - - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db - - # Get the list of ids - ids = get_ids_from_selected_rows() - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to fetch annotations from'), - show=True) - - # Map ids to paths - path_map = generate_annotation_paths(ids, db, device) - - # Dispatch to devices.kindle.driver.get_annotations() - self.device_manager.annotations(Dispatcher(self.annotations_fetched), - path_map) - - def annotations_fetched(self, job): - from calibre.devices.usbms.device import Device - from calibre.ebooks.metadata import MetaInformation - from calibre.gui2.dialogs.progress import ProgressDialog - from calibre.library.cli import do_add_format - - class Updater(QThread): - - update_progress = pyqtSignal(int) - update_done = pyqtSignal() - FINISHED_READING_PCT_THRESHOLD = 96 - - def __init__(self, parent, db, annotation_map, done_callback): - QThread.__init__(self, parent) - self.db = db - self.pd = ProgressDialog(_('Merging user annotations into database'), '', - 0, len(job.result), parent=parent) - - self.am = annotation_map - self.done_callback = done_callback - self.connect(self.pd, SIGNAL('canceled()'), self.canceled) - self.pd.setModal(True) - self.pd.show() - self.update_progress.connect(self.pd.set_value, - type=Qt.QueuedConnection) - self.update_done.connect(self.pd.hide, type=Qt.QueuedConnection) - - def generate_annotation_html(self, bookmark): - # Returns
...
- last_read_location = bookmark.last_read_location - timestamp = datetime.datetime.utcfromtimestamp(bookmark.timestamp) - percent_read = bookmark.percent_read - - ka_soup = BeautifulSoup() - dtc = 0 - divTag = Tag(ka_soup,'div') - divTag['class'] = 'user_annotations' - - # Add the last-read location - spanTag = Tag(ka_soup, 'span') - spanTag['style'] = 'font-weight:bold' - if bookmark.book_format == 'pdf': - spanTag.insert(0,NavigableString( - _("%s
Last Page Read: %d (%d%%)") % \ - (strftime(u'%x', timestamp.timetuple()), - last_read_location, - percent_read))) - else: - spanTag.insert(0,NavigableString( - _("%s
Last Page Read: Location %d (%d%%)") % \ - (strftime(u'%x', timestamp.timetuple()), - last_read_location, - percent_read))) - - divTag.insert(dtc, spanTag) - dtc += 1 - divTag.insert(dtc, Tag(ka_soup,'br')) - dtc += 1 - - if bookmark.user_notes: - user_notes = bookmark.user_notes - annotations = [] - - # Add the annotations sorted by location - # Italicize highlighted text - for location in sorted(user_notes): - if user_notes[location]['text']: - annotations.append( - _('Location %d • %s
%s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'], - user_notes[location]['text'] if \ - user_notes[location]['type'] == 'Note' else \ - '%s' % user_notes[location]['text'])) - else: - if bookmark.book_format == 'pdf': - annotations.append( - _('Page %d • %s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'])) - else: - annotations.append( - _('Location %d • %s
') % \ - (user_notes[location]['displayed_location'], - user_notes[location]['type'])) - - for annotation in annotations: - divTag.insert(dtc, annotation) - dtc += 1 - - ka_soup.insert(0,divTag) - return ka_soup - - def mark_book_as_read(self,id): - read_tag = gprefs.get('catalog_epub_mobi_read_tag') - self.db.set_tags(id, [read_tag], append=True) - - def canceled(self): - self.pd.hide() - - def run(self): - ignore_tags = set(['Catalog','Clippings']) - for (i, id) in enumerate(self.am): - bm = Device.UserAnnotation(self.am[id][0],self.am[id][1]) - if bm.type == 'kindle_bookmark': - mi = self.db.get_metadata(id, index_is_id=True) - user_notes_soup = self.generate_annotation_html(bm.value) - if mi.comments: - a_offset = mi.comments.find('
') - ad_offset = mi.comments.find('
') - - if a_offset >= 0: - mi.comments = mi.comments[:a_offset] - if ad_offset >= 0: - mi.comments = mi.comments[:ad_offset] - if set(mi.tags).intersection(ignore_tags): - continue - if mi.comments: - hrTag = Tag(user_notes_soup,'hr') - hrTag['class'] = 'annotations_divider' - user_notes_soup.insert(0,hrTag) - - mi.comments += user_notes_soup.prettify() - else: - mi.comments = unicode(user_notes_soup.prettify()) - # Update library comments - self.db.set_comment(id, mi.comments) - - # Update 'read' tag except for Catalogs/Clippings - if bm.value.percent_read >= self.FINISHED_READING_PCT_THRESHOLD: - if not set(mi.tags).intersection(ignore_tags): - self.mark_book_as_read(id) - - # Add bookmark file to id - self.db.add_format_with_hooks(id, bm.value.bookmark_extension, - bm.value.path, index_is_id=True) - self.update_progress.emit(i) - elif bm.type == 'kindle_clippings': - # Find 'My Clippings' author=Kindle in database, or add - last_update = 'Last modified %s' % strftime(u'%x %X',bm.value['timestamp'].timetuple()) - mc_id = list(db.data.parse('title:"My Clippings"')) - if mc_id: - do_add_format(self.db, mc_id[0], 'TXT', bm.value['path']) - mi = self.db.get_metadata(mc_id[0], index_is_id=True) - mi.comments = last_update - self.db.set_metadata(mc_id[0], mi) - else: - mi = MetaInformation('My Clippings', authors = ['Kindle']) - mi.tags = ['Clippings'] - mi.comments = last_update - self.db.add_books([bm.value['path']], ['txt'], [mi]) - - self.update_done.emit() - self.done_callback(self.am.keys()) - - if not job.result: return - - if self.current_view() is not self.library_view: - return error_dialog(self, _('Use library only'), - _('User annotations generated from main library only'), - show=True) - db = self.library_view.model().db - - self.__annotation_updater = Updater(self, db, job.result, - Dispatcher(self.library_view.model().refresh_ids)) - self.__annotation_updater.start() - - - ############################################################################ - - ################################# Add books ################################ - - def add_recursive(self, single): - root = choose_dir(self, 'recursive book import root dir dialog', - 'Select root folder') - if not root: - return - from calibre.gui2.add import Adder - self._adder = Adder(self, - self.library_view.model().db, - Dispatcher(self._files_added), spare_server=self.spare_server) - self._adder.add_recursive(root, single) - - def add_recursive_single(self, *args): - ''' - Add books from the local filesystem to either the library or the device - recursively assuming one book per folder. - ''' - self.add_recursive(True) - - def add_recursive_multiple(self, *args): - ''' - Add books from the local filesystem to either the library or the device - recursively assuming multiple books per folder. - ''' - self.add_recursive(False) - - def add_empty(self, *args): - ''' - Add an empty book item to the library. This does not import any formats - from a book file. - ''' - num, ok = QInputDialog.getInt(self, _('How many empty books?'), - _('How many empty books should be added?'), 1, 1, 100) - if ok: - from calibre.ebooks.metadata import MetaInformation - for x in xrange(num): - self.library_view.model().db.import_book(MetaInformation(None), []) - self.library_view.model().books_added(num) - - def files_dropped(self, paths): - to_device = self.stack.currentIndex() != 0 - self._add_books(paths, to_device) - - def files_dropped_on_book(self, event, paths): - accept = False - if self.current_view() is not self.library_view: - return - db = self.library_view.model().db - current_idx = self.library_view.currentIndex() - if not current_idx.isValid(): return - cid = db.id(current_idx.row()) - for path in paths: - ext = os.path.splitext(path)[1].lower() - if ext: - ext = ext[1:] - if ext in IMAGE_EXTENSIONS: - pmap = QPixmap() - pmap.load(path) - if not pmap.isNull(): - accept = True - db.set_cover(cid, pmap) - elif ext in BOOK_EXTENSIONS: - db.add_format_with_hooks(cid, ext, path, index_is_id=True) - accept = True - if accept: - event.accept() - self.cover_cache.refresh([cid]) - self.library_view.model().current_changed(current_idx, current_idx) - - def __add_filesystem_book(self, paths, allow_device=True): - if isinstance(paths, basestring): - paths = [paths] - books = [path for path in map(os.path.abspath, paths) if os.access(path, - os.R_OK)] - - if books: - to_device = allow_device and self.stack.currentIndex() != 0 - self._add_books(books, to_device) - if to_device: - self.status_bar.show_message(\ - _('Uploading books to device.'), 2000) - - - def add_filesystem_book(self, paths, allow_device=True): - self._add_filesystem_book(paths, allow_device=allow_device) - - def add_books(self, *args): - ''' - Add books from the local filesystem to either the library or the device. - ''' - filters = [ - (_('Books'), BOOK_EXTENSIONS), - (_('EPUB Books'), ['epub']), - (_('LRF Books'), ['lrf']), - (_('HTML Books'), ['htm', 'html', 'xhtm', 'xhtml']), - (_('LIT Books'), ['lit']), - (_('MOBI Books'), ['mobi', 'prc', 'azw']), - (_('Topaz books'), ['tpz','azw1']), - (_('Text books'), ['txt', 'rtf']), - (_('PDF Books'), ['pdf']), - (_('Comics'), ['cbz', 'cbr', 'cbc']), - (_('Archives'), ['zip', 'rar']), - ] - to_device = self.stack.currentIndex() != 0 - if to_device: - filters = [(_('Supported books'), self.device_manager.device.FORMATS)] - - books = choose_files(self, 'add books dialog dir', 'Select books', - filters=filters) - if not books: - return - self._add_books(books, to_device) - - def _add_books(self, paths, to_device, on_card=None): - if on_card is None: - on_card = 'carda' if self.stack.currentIndex() == 2 else 'cardb' if self.stack.currentIndex() == 3 else None - if not paths: - return - from calibre.gui2.add import Adder - self.__adder_func = partial(self._files_added, on_card=on_card) - self._adder = Adder(self, - None if to_device else self.library_view.model().db, - Dispatcher(self.__adder_func), spare_server=self.spare_server) - self._adder.add(paths) - - def _files_added(self, paths=[], names=[], infos=[], on_card=None): - if paths: - self.upload_books(paths, - list(map(ascii_filename, names)), - infos, on_card=on_card) - self.status_bar.show_message( - _('Uploading books to device.'), 2000) - if getattr(self._adder, 'number_of_books_added', 0) > 0: - self.library_view.model().books_added(self._adder.number_of_books_added) - if hasattr(self, 'db_images'): - self.db_images.reset() - if getattr(self._adder, 'merged_books', False): - books = u'\n'.join([x if isinstance(x, unicode) else - x.decode(preferred_encoding, 'replace') for x in - self._adder.merged_books]) - info_dialog(self, _('Merged some books'), - _('Some duplicates were found and merged into the ' - 'following existing books:'), det_msg=books, show=True) - if getattr(self._adder, 'critical', None): - det_msg = [] - 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, _('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 = None - - - ############################################################################ - - ############################### Delete books ############################### - - def _get_selected_formats(self, msg): - from calibre.gui2.dialogs.select_formats import SelectFormats - fmts = self.library_view.model().db.all_formats() - d = SelectFormats([x.lower() for x in fmts], msg, parent=self) - if d.exec_() != d.Accepted: - return None - return d.selected_formats - - def _get_selected_ids(self, err_title=_('Cannot delete')): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, err_title, _('No book selected')) - d.exec_() - return set([]) - return set(map(self.library_view.model().id, rows)) - - def delete_selected_formats(self, *args): - ids = self._get_selected_ids() - if not ids: - return - fmts = self._get_selected_formats( - _('Choose formats to be deleted')) - if not fmts: - return - for id in ids: - for fmt in fmts: - self.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.tags_view.recount() - - def delete_all_but_selected_formats(self, *args): - ids = self._get_selected_ids() - if not ids: - return - fmts = self._get_selected_formats( - '

'+_('Choose formats not to be deleted')) - if fmts is None: - return - for id in ids: - bfmts = self.library_view.model().db.formats(id, index_is_id=True) - if bfmts is None: - continue - bfmts = set([x.lower() for x in bfmts.split(',')]) - rfmts = bfmts - set(fmts) - for fmt in rfmts: - self.library_view.model().db.remove_format(id, fmt, - index_is_id=True, notify=False) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - if ids: - self.tags_view.recount() - - - def delete_covers(self, *args): - ids = self._get_selected_ids() - if not ids: - return - for id in ids: - self.library_view.model().db.remove_cover(id) - self.library_view.model().refresh_ids(ids) - self.library_view.model().current_changed(self.library_view.currentIndex(), - self.library_view.currentIndex()) - - def delete_books(self, *args): - ''' - Delete selected books from device or library. - ''' - view = self.current_view() - rows = view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return - if self.stack.currentIndex() == 0: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted and the files ' - 'removed from your computer. Are you sure?') - +'

', 'library_delete_books', self): - return - ci = view.currentIndex() - row = None - if ci.isValid(): - row = ci.row() - ids_deleted = view.model().delete_books(rows) - for v in (self.memory_view, self.card_a_view, self.card_b_view): - if v is None: - continue - v.model().clear_ondevice(ids_deleted) - if row is not None: - ci = view.model().index(row, 0) - if ci.isValid(): - view.setCurrentIndex(ci) - sm = view.selectionModel() - sm.select(ci, sm.Select) - else: - if not confirm('

'+_('The selected books will be ' - 'permanently deleted ' - 'from your device. Are you sure?') - +'

', 'device_delete_books', self): - return - if self.stack.currentIndex() == 1: - view = self.memory_view - elif self.stack.currentIndex() == 2: - view = self.card_a_view - else: - view = self.card_b_view - paths = view.model().paths(rows) - job = self.remove_paths(paths) - self.delete_memory[job] = (paths, view.model()) - view.model().mark_for_deletion(job, rows) - self.status_bar.show_message(_('Deleting books from device.'), 1000) - - def remove_paths(self, paths): - return self.device_manager.delete_books(\ - Dispatcher(self.books_deleted), paths) - - def books_deleted(self, job): - ''' - Called once deletion is done on the device - ''' - for view in (self.memory_view, self.card_a_view, self.card_b_view): - view.model().deletion_done(job, job.failed) - if job.failed: - self.device_job_exception(job) - return - - if self.delete_memory.has_key(job): - paths, model = self.delete_memory.pop(job) - self.device_manager.remove_books_from_metadata(paths, - self.booklists()) - model.paths_deleted(paths) - self.upload_booklists() - # Clear the ondevice info so it will be recomputed - self.book_on_device(None, None, reset=True) - # We want to reset all the ondevice flags in the library. Use a big - # hammer, so we don't need to worry about whether some succeeded or not - self.library_view.model().refresh() - - ############################################################################ - - ############################### Edit metadata ############################## - - def download_metadata(self, checked, covers=True, set_metadata=True, - set_social_metadata=None): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot download metadata'), - _('No books selected')) - d.exec_() - return - db = self.library_view.model().db - ids = [db.id(row.row()) for row in rows] - if set_social_metadata is None: - get_social_metadata = config['get_social_metadata'] - else: - get_social_metadata = set_social_metadata - from calibre.gui2.metadata import DownloadMetadata - self._download_book_metadata = DownloadMetadata(db, ids, - get_covers=covers, set_metadata=set_metadata, - get_social_metadata=get_social_metadata) - self._download_book_metadata.start() - if set_social_metadata is not None and set_social_metadata: - x = _('social metadata') - else: - x = _('covers') if covers and not set_metadata else _('metadata') - self.progress_indicator.start( - _('Downloading %s for %d book(s)')%(x, len(ids))) - self._book_metadata_download_check = QTimer(self) - self.connect(self._book_metadata_download_check, - SIGNAL('timeout()'), self.book_metadata_download_check, - Qt.QueuedConnection) - self._book_metadata_download_check.start(100) - - def book_metadata_download_check(self): - if self._download_book_metadata.is_alive(): - return - self._book_metadata_download_check.stop() - self.progress_indicator.stop() - cr = self.library_view.currentIndex().row() - x = self._download_book_metadata - self._download_book_metadata = None - if x.exception is None: - self.library_view.model().refresh_ids( - x.updated, cr) - if x.failures: - details = ['%s: %s'%(title, reason) for title, - reason in x.failures.values()] - details = '%s\n'%('\n'.join(details)) - warning_dialog(self, _('Failed to download some metadata'), - _('Failed to download metadata for the following:'), - det_msg=details).exec_() - else: - err = _('Failed to download metadata:') - error_dialog(self, _('Error'), err, det_msg=x.tb).exec_() - - - def edit_metadata(self, checked, bulk=None): - ''' - Edit metadata of selected books in library. - ''' - rows = self.library_view.selectionModel().selectedRows() - previous = self.library_view.currentIndex() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - - if bulk or (bulk is None and len(rows) > 1): - return self.edit_bulk_metadata(checked) - - def accepted(id): - self.library_view.model().refresh_ids([id]) - - for row in rows: - self._metadata_view_id = self.library_view.model().db.id(row.row()) - d = MetadataSingleDialog(self, row.row(), - self.library_view.model().db, - accepted_callback=accepted, - cancel_all=rows.index(row) < len(rows)-1) - self.connect(d, SIGNAL('view_format(PyQt_PyObject)'), - self.metadata_view_format) - d.exec_() - if d.cancel_all: - break - if rows: - current = self.library_view.currentIndex() - m = self.library_view.model() - m.refresh_cover_cache(map(m.id, rows)) - if self.cover_flow: - self.cover_flow.dataChanged() - m.current_changed(current, previous) - self.tags_view.recount() - - def edit_bulk_metadata(self, checked): - ''' - Edit metadata of selected books in library in bulk. - ''' - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot edit metadata'), - _('No books selected')) - d.exec_() - return - if MetadataBulkDialog(self, rows, - self.library_view.model().db).changed: - self.library_view.model().resort(reset=False) - self.library_view.model().research() - self.tags_view.recount() - - ############################################################################ - - ############################### Merge books ############################## - def merge_books(self, safe_merge=False): - ''' - Merge selected books in library. - ''' - if self.stack.currentIndex() != 0: - return - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot merge books'), - _('No books selected'), show=True) - if len(rows) < 2: - return error_dialog(self, _('Cannot merge books'), - _('At least two books must be selected for merging'), - show=True) - dest_id, src_books, src_ids = self.books_to_merge(rows) - if safe_merge: - if not confirm('

'+_( - 'All book formats and metadata from the selected books ' - 'will be added to the first selected book.

' - 'The second and subsequently selected books will not ' - 'be deleted or changed.

' - 'Please confirm you want to proceed.') - +'

', 'merge_books_safe', self): - return - self.add_formats(dest_id, src_books) - self.merge_metadata(dest_id, src_ids) - else: - if not confirm('

'+_( - 'All book formats and metadata from the selected books will be merged ' - 'into the first selected book.

' - 'After merger the second and ' - 'subsequently selected books will be deleted.

' - 'All book formats of the first selected book will be kept ' - 'and any duplicate formats in the second and subsequently selected books ' - 'will be permanently deleted from your computer.

' - 'Are you sure you want to proceed?') - +'

', 'merge_books', self): - return - if len(rows)>5: - if not confirm('

'+_('You are about to merge more than 5 books. ' - 'Are you sure you want to proceed?') - +'

', 'merge_too_many_books', self): - return - self.add_formats(dest_id, src_books) - self.merge_metadata(dest_id, src_ids) - self.delete_books_after_merge(src_ids) - # leave the selection highlight on first selected book - dest_row = rows[0].row() - for row in rows: - if row.row() < rows[0].row(): - dest_row -= 1 - ci = self.library_view.model().index(dest_row, 0) - if ci.isValid(): - self.library_view.setCurrentIndex(ci) - - def add_formats(self, dest_id, src_books, replace=False): - for src_book in src_books: - if src_book: - fmt = os.path.splitext(src_book)[-1].replace('.', '').upper() - with open(src_book, 'rb') as f: - self.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True, - notify=False, replace=replace) - - def books_to_merge(self, rows): - src_books = [] - src_ids = [] - m = self.library_view.model() - for i, row in enumerate(rows): - id_ = m.id(row) - if i == 0: - dest_id = id_ - else: - src_ids.append(id_) - dbfmts = m.db.formats(id_, index_is_id=True) - if dbfmts: - for fmt in dbfmts.split(','): - src_books.append(m.db.format_abspath(id_, fmt, - index_is_id=True)) - return [dest_id, src_books, src_ids] - - def delete_books_after_merge(self, ids_to_delete): - self.library_view.model().delete_books_by_id(ids_to_delete) - - def merge_metadata(self, dest_id, src_ids): - db = self.library_view.model().db - dest_mi = db.get_metadata(dest_id, index_is_id=True, get_cover=True) - orig_dest_comments = dest_mi.comments - for src_id in src_ids: - src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) - if src_mi.comments and orig_dest_comments != src_mi.comments: - if not dest_mi.comments or len(dest_mi.comments) == 0: - dest_mi.comments = src_mi.comments - else: - dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments) - if src_mi.title and src_mi.title and (not dest_mi.title or - dest_mi.title == _('Unknown')): - dest_mi.title = src_mi.title - if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] == - _('Unknown')): - dest_mi.authors = src_mi.authors - dest_mi.author_sort = src_mi.author_sort - if src_mi.tags: - if not dest_mi.tags: - dest_mi.tags = src_mi.tags - else: - for tag in src_mi.tags: - dest_mi.tags.append(tag) - if src_mi.cover and not dest_mi.cover: - dest_mi.cover = src_mi.cover - if not dest_mi.publisher: - dest_mi.publisher = src_mi.publisher - if not dest_mi.rating: - dest_mi.rating = src_mi.rating - if not dest_mi.series: - dest_mi.series = src_mi.series - dest_mi.series_index = src_mi.series_index - db.set_metadata(dest_id, dest_mi, ignore_errors=False) - - ############################################################################ - - - ############################## Save to disk ################################ - def save_single_format_to_disk(self, checked): - self.save_to_disk(checked, False, prefs['output_format']) - - def save_specific_format_disk(self, fmt): - self.save_to_disk(False, False, fmt) - - def save_to_single_dir(self, checked): - self.save_to_disk(checked, True) - - def save_single_fmt_to_single_dir(self, *args): - self.save_to_disk(False, single_dir=True, - single_format=prefs['output_format']) - - def save_to_disk(self, checked, single_dir=False, single_format=None): - rows = self.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - return error_dialog(self, _('Cannot save to disk'), - _('No books selected'), show=True) - path = choose_dir(self, 'save to disk dialog', - _('Choose destination directory')) - if not path: - return - - if self.current_view() is self.library_view: - from calibre.gui2.add import Saver - from calibre.library.save_to_disk import config - opts = config().parse() - if single_format is not None: - opts.formats = single_format - # Special case for Kindle annotation files - if single_format.lower() in ['mbp','pdr','tan']: - opts.to_lowercase = False - opts.save_cover = False - opts.write_opf = False - opts.template = opts.send_template - if single_dir: - opts.template = opts.template.split('/')[-1].strip() - if not opts.template: - opts.template = '{title} - {authors}' - self._saver = Saver(self, self.library_view.model().db, - Dispatcher(self._books_saved), rows, path, opts, - spare_server=self.spare_server) - - else: - paths = self.current_view().model().paths(rows) - self.device_manager.save_books( - Dispatcher(self.books_saved), paths, path) - - - def _books_saved(self, path, failures, error): - self._saver = None - if error: - return error_dialog(self, _('Error while saving'), - _('There was an error while saving.'), - error, show=True) - if failures: - failures = [u'%s\n\t%s'% - (title, '\n\t'.join(err.splitlines())) for title, err in - failures] - - warning_dialog(self, _('Could not save some books'), - _('Could not save some books') + ', ' + - _('Click the show details button to see which ones.'), - u'\n\n'.join(failures), show=True) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) - - def books_saved(self, job): - if job.failed: - return self.device_job_exception(job) - - ############################################################################ - - ############################### Generate catalog ########################### - - def generate_catalog(self): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 2: - rows = xrange(self.library_view.model().rowCount(QModelIndex())) - ids = map(self.library_view.model().id, rows) - - dbspec = None - if not ids: - return error_dialog(self, _('No books selected'), - _('No books selected to generate catalog for'), - show=True) - - # Calling gui2.tools:generate_catalog() - ret = generate_catalog(self, dbspec, ids, self.device_manager.device) - if ret is None: - return - - func, args, desc, out, sync, title = ret - - fmt = os.path.splitext(out)[1][1:].upper() - job = self.job_manager.run_job( - Dispatcher(self.catalog_generated), func, args=args, - description=desc) - job.catalog_file_path = out - job.fmt = fmt - job.catalog_sync, job.catalog_title = sync, title - self.status_bar.show_message(_('Generating %s catalog...')%fmt) - - def catalog_generated(self, job): - if job.result: - # Search terms nulled catalog results - return error_dialog(self, _('No books found'), - _("No books to catalog\nCheck exclude tags"), - show=True) - if job.failed: - return self.job_exception(job) - id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title) - self.library_view.model().reset() - if job.catalog_sync: - sync = dynamic.get('catalogs_to_be_synced', set([])) - sync.add(id) - dynamic.set('catalogs_to_be_synced', sync) - self.status_bar.show_message(_('Catalog generated.'), 3000) - self.sync_catalogs() - if job.fmt not in ['EPUB','MOBI']: - export_dir = choose_dir(self, _('Export Catalog Directory'), - _('Select destination for %s.%s') % (job.catalog_title, job.fmt.lower())) - if export_dir: - destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) - shutil.copyfile(job.catalog_file_path, destination) - - ############################### Fetch news ################################# - - def download_scheduled_recipe(self, arg): - func, args, desc, fmt, temp_files = \ - fetch_scheduled_recipe(arg) - job = self.job_manager.run_job( - Dispatcher(self.scheduled_recipe_fetched), func, args=args, - description=desc) - self.conversion_jobs[job] = (temp_files, fmt, arg) - self.status_bar.show_message(_('Fetching news from ')+arg['title'], 2000) - - def scheduled_recipe_fetched(self, job): - temp_files, fmt, arg = self.conversion_jobs.pop(job) - pt = temp_files[0] - if job.failed: - self.scheduler.recipe_download_failed(arg) - return self.job_exception(job) - id = self.library_view.model().add_news(pt.name, arg) - self.library_view.model().reset() - sync = dynamic.get('news_to_be_synced', set([])) - sync.add(id) - dynamic.set('news_to_be_synced', sync) - self.scheduler.recipe_downloaded(arg) - self.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) - self.email_news(id) - self.sync_news() - - ############################################################################ - - ############################### Convert #################################### - - def auto_convert(self, book_ids, on_card, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted, extra_job_args=[on_card]) - - def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_mail, - extra_job_args=[delete_from_library, to, fmts]) - - def auto_convert_news(self, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_news) - - def auto_convert_catalogs(self, book_ids, format): - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format) - if jobs == []: return - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_auto_converted_catalogs) - - - - def get_books_for_conversion(self): - rows = [r.row() for r in \ - self.library_view.selectionModel().selectedRows()] - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot convert'), - _('No books selected')) - d.exec_() - return None - return [self.library_view.model().db.id(r) for r in rows] - - def convert_ebook(self, checked, bulk=None): - book_ids = self.get_books_for_conversion() - if book_ids is None: return - previous = self.library_view.currentIndex() - rows = [x.row() for x in \ - self.library_view.selectionModel().selectedRows()] - num = 0 - if bulk or (bulk is None and len(book_ids) > 1): - self.__bulk_queue = convert_bulk_ebook(self, self.queue_convert_jobs, - self.library_view.model().db, book_ids, - out_format=prefs['output_format'], args=(rows, previous, - self.book_converted)) - if self.__bulk_queue is None: - return - num = len(self.__bulk_queue.book_ids) - else: - jobs, changed, bad = convert_single_ebook(self, - self.library_view.model().db, book_ids, out_format=prefs['output_format']) - self.queue_convert_jobs(jobs, changed, bad, rows, previous, - self.book_converted) - num = len(jobs) - - if num > 0: - self.status_bar.show_message(_('Starting conversion of %d book(s)') % - num, 2000) - - def queue_convert_jobs(self, jobs, changed, bad, rows, previous, - converted_func, extra_job_args=[]): - for func, args, desc, fmt, id, temp_files in jobs: - if id not in bad: - job = self.job_manager.run_job(Dispatcher(converted_func), - func, args=args, description=desc) - args = [temp_files, fmt, id]+extra_job_args - self.conversion_jobs[job] = tuple(args) - - if changed: - self.library_view.model().refresh_rows(rows) - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, previous) - - def book_auto_converted(self, job): - temp_files, fmt, book_id, on_card = self.conversion_jobs[job] - self.book_converted(job) - self.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_mail(self, job): - temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] - self.book_converted(job) - self.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_news(self, job): - temp_files, fmt, book_id = self.conversion_jobs[job] - self.book_converted(job) - self.sync_news(send_ids=[book_id], do_auto_convert=False) - - def book_auto_converted_catalogs(self, job): - temp_files, fmt, book_id = self.conversion_jobs[job] - self.book_converted(job) - self.sync_catalogs(send_ids=[book_id], do_auto_convert=False) - - def book_converted(self, job): - temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3] - try: - if job.failed: - self.job_exception(job) - return - data = open(temp_files[-1].name, 'rb') - self.library_view.model().db.add_format(book_id, \ - fmt, data, index_is_id=True) - data.close() - self.status_bar.show_message(job.description + \ - (' completed'), 2000) - finally: - for f in temp_files: - try: - if os.path.exists(f.name): - os.remove(f.name) - except: - pass - self.tags_view.recount() - if self.current_view() is self.library_view: - current = self.library_view.currentIndex() - self.library_view.model().current_changed(current, QModelIndex()) - - #############################View book###################################### - - def view_format(self, row, format): - fmt_path = self.library_view.model().db.format_abspath(row, format) - if fmt_path: - self._view_file(fmt_path) - - def metadata_view_format(self, fmt): - fmt_path = self.library_view.model().db.\ - format_abspath(self._metadata_view_id, - fmt, index_is_id=True) - if fmt_path: - self._view_file(fmt_path) - - - def book_downloaded_for_viewing(self, job): - if job.failed: - self.device_job_exception(job) - return - self._view_file(job.result) - - def _launch_viewer(self, name=None, viewer='ebook-viewer', internal=True): - self.setCursor(Qt.BusyCursor) - try: - if internal: - args = [viewer] - if isosx and 'ebook' in viewer: - args.append('--raise-window') - if name is not None: - args.append(name) - self.job_manager.launch_gui_app(viewer, - kwargs=dict(args=args)) - else: - paths = os.environ.get('LD_LIBRARY_PATH', - '').split(os.pathsep) - paths = [x for x in paths if x] - if isfrozen and islinux and paths: - npaths = [x for x in paths if x != sys.frozen_path] - os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths) - QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name) - if isfrozen and islinux and paths: - os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths) - time.sleep(2) # User feedback - finally: - self.unsetCursor() - - def _view_file(self, name): - ext = os.path.splitext(name)[1].upper().replace('.', '') - viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer' - internal = ext in config['internally_viewed_formats'] - self._launch_viewer(name, viewer, internal) - - def view_specific_format(self, triggered): - rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot view'), _('No book selected')) - d.exec_() - return - - row = rows[0].row() - formats = self.library_view.model().db.formats(row).upper().split(',') - d = ChooseFormatDialog(self, _('Choose the format to view'), formats) - if d.exec_() == QDialog.Accepted: - format = d.format() - self.view_format(row, format) - - def _view_check(self, num, max_=3): - if num <= max_: - return True - return question_dialog(self, _('Multiple Books Selected'), - _('You are attempting to open %d books. Opening too many ' - 'books at once can be slow and have a negative effect on the ' - 'responsiveness of your computer. Once started the process ' - 'cannot be stopped until complete. Do you wish to continue?' - ) % num) - - def view_folder(self, *args): - rows = self.current_view().selectionModel().selectedRows() - if not rows or len(rows) == 0: - d = error_dialog(self, _('Cannot open folder'), - _('No book selected')) - d.exec_() - return - if not self._view_check(len(rows)): - return - for row in rows: - path = self.library_view.model().db.abspath(row.row()) - QDesktopServices.openUrl(QUrl.fromLocalFile(path)) - - - def view_book(self, triggered): - rows = self.current_view().selectionModel().selectedRows() - self._view_books(rows) - - def view_specific_book(self, index): - self._view_books([index]) - - def _view_books(self, rows): - if not rows or len(rows) == 0: - self._launch_viewer() - return - - if not self._view_check(len(rows)): - return - - if self.current_view() is self.library_view: - for row in rows: - if hasattr(row, 'row'): - row = row.row() - - formats = self.library_view.model().db.formats(row) - title = self.library_view.model().db.title(row) - if not formats: - error_dialog(self, _('Cannot view'), - _('%s has no available formats.')%(title,), show=True) - continue - - formats = formats.upper().split(',') - - - in_prefs = False - for format in prefs['input_format_order']: - if format in formats: - in_prefs = True - self.view_format(row, format) - break - if not in_prefs: - self.view_format(row, formats[0]) - else: - paths = self.current_view().model().paths(rows) - for path in paths: - pt = PersistentTemporaryFile('_viewer_'+\ - os.path.splitext(path)[1]) - self.persistent_files.append(pt) - pt.close() - self.device_manager.view_book(\ - Dispatcher(self.book_downloaded_for_viewing), - path, pt.name) - - - ############################################################################ - - ############################################################################ - - ############################### Do config ################################## def do_config(self, *args): if self.job_manager.has_jobs(): @@ -1808,10 +435,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.library_view.model().count_changed() prefs['library_path'] = self.library_path - ############################################################################ - - ################################ Book info ################################# - def show_book_info(self, *args): if self.current_view() is not self.library_view: error_dialog(self, _('No detailed info available'), @@ -1822,9 +445,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, if index.isValid(): BookInfo(self, self.library_view, index).show() - ############################################################################ - - ############################################################################ def location_selected(self, location): ''' Called when a location icon is clicked (e.g. Library) @@ -1858,35 +478,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, self.set_number_of_books_shown() - def device_job_exception(self, job): - ''' - Handle exceptions in threaded device jobs. - ''' - if isinstance(getattr(job, 'exception', None), UserFeedback): - ex = job.exception - func = {UserFeedback.ERROR:error_dialog, - UserFeedback.WARNING:warning_dialog, - UserFeedback.INFO:info_dialog}[ex.level] - return func(self, _('Failed'), ex.msg, det_msg=ex.details if - ex.details else '', show=True) - - try: - if 'Could not read 32 bytes on the control bus.' in \ - unicode(job.details): - error_dialog(self, _('Error talking to device'), - _('There was a temporary error talking to the ' - 'device. Please unplug and reconnect the device ' - 'and or reboot.')).show() - return - except: - pass - try: - prints(job.details, file=sys.stderr) - except: - pass - if not self.device_error_dialog.isVisible(): - self.device_error_dialog.setDetailedText(job.details) - self.device_error_dialog.show() def job_exception(self, job): if not hasattr(self, '_modeless_dialogs'): @@ -2066,26 +657,5 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, else: e.ignore() - def update_found(self, version): - os = 'windows' if iswindows else 'osx' if isosx else 'linux' - url = 'http://calibre-ebook.com/download_%s'%os - self.latest_version = '
' + _('' - 'Latest version: %s')%(url, version) - self.vanity.setText(self.vanity_template%\ - (dict(version=self.latest_version, - device=self.device_info))) - self.vanity.update() - if config.get('new_version_notification') and \ - dynamic.get('update to version %s'%version, True): - if question_dialog(self, _('Update available'), - _('%s has been updated to version %s. ' - 'See the new features. Visit the download pa' - 'ge?')%(__appname__, version)): - url = 'http://calibre-ebook.com/download_'+\ - ('windows' if iswindows else 'osx' if isosx else 'linux') - QDesktopServices.openUrl(QUrl(url)) - dynamic.set('update to version %s'%version, False) - - + # }}} diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index 92e9db1cf2..9dcd4d9084 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal ' import traceback -from PyQt4.QtCore import QThread, pyqtSignal +from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt import mechanize -from calibre.constants import __version__, iswindows, isosx +from calibre.constants import __appname__, __version__, iswindows, isosx from calibre import browser from calibre.utils.config import prefs +from calibre.gui2 import config, dynamic, question_dialog URL = 'http://status.calibre-ebook.com/latest' @@ -36,3 +37,35 @@ class CheckForUpdates(QThread): traceback.print_exc() self.sleep(self.INTERVAL) +class UpdateMixin(object): + + def __init__(self, opts): + if not opts.no_update_check: + self.update_checker = CheckForUpdates(self) + self.update_checker.update_found.connect(self.update_found, + type=Qt.QueuedConnection) + self.update_checker.start() + + def update_found(self, version): + os = 'windows' if iswindows else 'osx' if isosx else 'linux' + url = 'http://calibre-ebook.com/download_%s'%os + self.latest_version = '
' + _('' + 'Latest version: %s')%(url, version) + self.vanity.setText(self.vanity_template%\ + (dict(version=self.latest_version, + device=self.device_info))) + self.vanity.update() + if config.get('new_version_notification') and \ + dynamic.get('update to version %s'%version, True): + if question_dialog(self, _('Update available'), + _('%s has been updated to version %s. ' + 'See the new features. Visit the download pa' + 'ge?')%(__appname__, version)): + url = 'http://calibre-ebook.com/download_'+\ + ('windows' if iswindows else 'osx' if isosx else 'linux') + QDesktopServices.openUrl(QUrl(url)) + dynamic.set('update to version %s'%version, False) + + + diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index bb6001794a..57f9d0baaf 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools from itertools import repeat from datetime import timedelta -from PyQt4.QtCore import QThread, QReadWriteLock -from PyQt4.QtGui import QImage +from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt from calibre.utils.config import tweaks from calibre.utils.date import parse_date, now, UNDEFINED_DATE from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import title_sort +from calibre import fit_image class CoverCache(QThread): @@ -96,6 +96,11 @@ class CoverCache(QThread): img.loadFromData(data) if img.isNull(): continue + scaled, nwidth, nheight = fit_image(img.width(), + img.height(), 600, 800) + if scaled: + img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio, + Qt.SmoothTransformation) except: continue self.cache_lock.lockForWrite() diff --git a/src/calibre/manual/news_recipe.rst b/src/calibre/manual/news_recipe.rst index 14cc41d436..7e5045ea47 100644 --- a/src/calibre/manual/news_recipe.rst +++ b/src/calibre/manual/news_recipe.rst @@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML .. automember:: BasicNewsRecipe.remove_javascript -.. automethod:: BasicNewsRecipe.prepreprocess_html +.. automethod:: BasicNewsRecipe.skip_ad_pages .. automethod:: BasicNewsRecipe.preprocess_html diff --git a/src/calibre/utils/magick_draw.py b/src/calibre/utils/magick_draw.py index 160f4b70a5..2a259301db 100644 --- a/src/calibre/utils/magick_draw.py +++ b/src/calibre/utils/magick_draw.py @@ -175,7 +175,7 @@ def add_borders_to_image(path_to_image, left=0, top=0, right=0, bottom=0, p.DestroyMagickWand(canvas) def create_cover_page(top_lines, logo_path, width=590, height=750, - bgcolor='white', output_format='png'): + bgcolor='white', output_format='jpg'): ans = None with p.ImageMagick(): canvas = create_canvas(width, height, bgcolor) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 8977f64d60..9e05babecc 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -413,18 +413,19 @@ class BasicNewsRecipe(Recipe): return url return article.get('link', None) - def prepreprocess_html(self, soup): + def skip_ad_pages(self, soup): ''' This method is called with the source of each downloaded :term:`HTML` file, before any of the cleanup attributes like remove_tags, keep_only_tags are applied. Note that preprocess_regexps will have already been applied. - It can be used to do arbitrarily powerful pre-processing on the :term:`HTML`. - It should return `soup` after processing it. + It is meant to allow the recipe to skip ad pages. If the soup represents + an ad page, return the HTML of the real page. Otherwise return + None. `soup`: A `BeautifulSoup `_ instance containing the downloaded :term:`HTML`. ''' - return soup + return None def preprocess_html(self, soup): @@ -628,7 +629,7 @@ class BasicNewsRecipe(Recipe): self.web2disk_options = web2disk_option_parser().parse_args(web2disk_cmdline)[0] for extra in ('keep_only_tags', 'remove_tags', 'preprocess_regexps', - 'prepreprocess_html', 'preprocess_html', 'remove_tags_after', + 'skip_ad_pages', 'preprocess_html', 'remove_tags_after', 'remove_tags_before', 'is_link_wanted'): setattr(self.web2disk_options, extra, getattr(self, extra)) self.web2disk_options.postprocess_html = self._postprocess_html @@ -801,11 +802,6 @@ class BasicNewsRecipe(Recipe): .calibre_navbar { font-family:monospace; } - hr { - border-color:gray; - border-style:solid; - border-width:thin; - } ''' diff --git a/src/calibre/web/feeds/templates.py b/src/calibre/web/feeds/templates.py index b64795b816..7ebf7294ae 100644 --- a/src/calibre/web/feeds/templates.py +++ b/src/calibre/web/feeds/templates.py @@ -108,7 +108,7 @@ class TouchscreenNavBarTemplate(Template): navbar = DIV(CLASS('calibre_navbar', 'calibre_rescale_100', style='text-align:'+align)) if bottom: - navbar.append(HR()) + navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) text = 'This article was downloaded by ' p = PT(text, STRONG(__appname__), A(url, href=url), style='text-align:left') p[0].tail = ' from ' @@ -136,7 +136,7 @@ class TouchscreenNavBarTemplate(Template): navbar.iterchildren(reversed=True).next().tail = ' | ' if not bottom: - navbar.append(HR()) + navbar.append(DIV(style="border-top:1px solid gray;border-bottom:1em solid white")) self.root = HTML(head, BODY(navbar)) @@ -193,6 +193,8 @@ class TouchscreenIndexTemplate(Template): div = DIV( masthead_p, PT(date, style='text-align:center'), + #DIV(style="border-color:gray;border-top-style:solid;border-width:thin"), + DIV(style="border-top:1px solid gray;border-bottom:1em solid white"), toc) self.root = HTML(head, BODY(div)) @@ -256,10 +258,9 @@ class TouchscreenFeedTemplate(Template): head.append(STYLE(extra_css, type='text/css')) body = BODY(style='page-break-before:always') div = DIV( - H2(feed.title, - CLASS('calibre_feed_title', 'calibre_rescale_160')), - CLASS('calibre_rescale_100') - ) + H2(feed.title, CLASS('calibre_feed_title', 'calibre_rescale_160')), + DIV(style="border-top:1px solid gray;border-bottom:1em solid white") + ) body.append(div) if getattr(feed, 'image', None): div.append(DIV(IMG( @@ -278,17 +279,33 @@ class TouchscreenFeedTemplate(Template): if not getattr(article, 'downloaded', False): continue tr = TR() - td = TD( - A(article.title, CLASS('summary_headline','calibre_rescale_120', - href=article.url)) - ) - if article.author: - td.append(DIV(article.author, - CLASS('summary_byline', 'calibre_rescale_100'))) - if article.summary: - td.append(DIV(cutoff(article.text_summary), - CLASS('summary_text', 'calibre_rescale_100'))) - tr.append(td) + + if True: + div_td = DIV( + A(article.title, CLASS('summary_headline','calibre_rescale_120', + href=article.url)), + style="display:inline-block") + if article.author: + div_td.append(DIV(article.author, + CLASS('summary_byline', 'calibre_rescale_100'))) + if article.summary: + div_td.append(DIV(cutoff(article.text_summary), + CLASS('summary_text', 'calibre_rescale_100'))) + tr.append(TD(div_td)) + else: + td = TD( + A(article.title, CLASS('summary_headline','calibre_rescale_120', + href=article.url)) + ) + if article.author: + td.append(DIV(article.author, + CLASS('summary_byline', 'calibre_rescale_100'))) + if article.summary: + td.append(DIV(cutoff(article.text_summary), + CLASS('summary_text', 'calibre_rescale_100'))) + + tr.append(td) + toc.append(tr) div.append(toc) diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index bde91ec0d2..b6186f785d 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -136,7 +136,7 @@ class RecursiveFetcher(object): self.remove_tags_before = getattr(options, 'remove_tags_before', None) self.keep_only_tags = getattr(options, 'keep_only_tags', []) self.preprocess_html_ext = getattr(options, 'preprocess_html', lambda soup: soup) - self.prepreprocess_html_ext = getattr(options, 'prepreprocess_html', lambda soup: soup) + self.prepreprocess_html_ext = getattr(options, 'skip_ad_pages', lambda soup: None) self.postprocess_html_ext= getattr(options, 'postprocess_html', None) self._is_link_wanted = getattr(options, 'is_link_wanted', default_is_link_wanted) @@ -154,7 +154,9 @@ class RecursiveFetcher(object): nmassage.append((re.compile(r'', re.DOTALL), lambda m: '')) soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage) - soup = self.prepreprocess_html_ext(soup) + replace = self.prepreprocess_html_ext(soup) + if replace is not None: + soup = BeautifulSoup(xml_to_unicode(src, self.verbose, strip_encoding_pats=True)[0], markupMassage=nmassage) if self.keep_only_tags: body = Tag(soup, 'body')