diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 6f77a4d96c..99679283a7 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -60,7 +60,12 @@ class ANDROID(USBMS): 0x1004 : { 0x61cc : [0x100] }, # Archos - 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216], 0x1422 : [0x0216]}, + 0x0e79 : { + 0x1400 : [0x0222, 0x0216], + 0x1419 : [0x0216], + 0x1420 : [0x0216], + 0x1422 : [0x0216] + }, # Huawei # Disabled as this USB id is used by various USB flash drives @@ -84,10 +89,10 @@ class ANDROID(USBMS): 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', - 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD'] + 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', '7'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', - 'A70S', 'A101IT'] + 'A70S', 'A101IT', '7'] OSX_MAIN_MEM = 'Android Device Main Memory' diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 4cca94a6c6..b47cc373a7 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -30,6 +30,7 @@ NULL_VALUES = { 'author_sort_map': {}, 'authors' : [_('Unknown')], 'title' : _('Unknown'), + 'user_categories' : {}, 'language' : 'und' } diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index d5263a5052..d34a563110 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -471,8 +471,11 @@ def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8)) def dump_user_categories(cats): + if not cats: + cats = {} from calibre.ebooks.metadata.book.json_codec import object_to_unicode - return json.dumps(object_to_unicode(cats)) + return json.dumps(object_to_unicode(cats), ensure_ascii=False, + skipkeys=True) class OPF(object): # {{{ @@ -1182,7 +1185,7 @@ class OPFCreator(Metadata): a(CAL_ELEM('calibre:timestamp', self.timestamp.isoformat())) if self.publication_type is not None: a(CAL_ELEM('calibre:publication_type', self.publication_type)) - if self.user_categories is not None: + if self.user_categories: from calibre.ebooks.metadata.book.json_codec import object_to_unicode a(CAL_ELEM('calibre:user_categories', json.dumps(object_to_unicode(self.user_categories)))) @@ -1311,7 +1314,7 @@ def metadata_to_opf(mi, as_string=True): if mi.title_sort: meta('title_sort', mi.title_sort) if mi.user_categories: - meta('user_categories', json.dumps(mi.user_categories)) + meta('user_categories', dump_user_categories(mi.user_categories)) serialize_user_metadata(metadata, mi.get_all_user_metadata(False)) diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py index 74e184cc66..54d7d49d6d 100644 --- a/src/calibre/ebooks/metadata/sources/base.py +++ b/src/calibre/ebooks/metadata/sources/base.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re +import re, threading from calibre.customize import Plugin from calibre.utils.logging import ThreadSafeLog, FileStream @@ -30,7 +30,21 @@ class Source(Plugin): touched_fields = frozenset() + def __init__(self, *args, **kwargs): + Plugin.__init__(self, *args, **kwargs) + self._isbn_to_identifier_cache = {} + self.cache_lock = threading.RLock() + # Utility functions {{{ + + def cache_isbn_to_identifier(self, isbn, identifier): + with self.cache_lock: + self._isbn_to_identifier_cache[isbn] = identifier + + def cached_isbn_to_identifier(self, isbn): + with self.cache_lock: + return self._isbn_to_identifier_cache.get(isbn, None) + def get_author_tokens(self, authors, only_first_author=True): ''' Take a list of authors and return a list of tokens useful for an diff --git a/src/calibre/ebooks/metadata/sources/google.py b/src/calibre/ebooks/metadata/sources/google.py index 498c7574ea..0720b21ded 100644 --- a/src/calibre/ebooks/metadata/sources/google.py +++ b/src/calibre/ebooks/metadata/sources/google.py @@ -13,6 +13,7 @@ from functools import partial from lxml import etree +from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata.sources.base import Source from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.chardet import xml_to_unicode @@ -69,6 +70,7 @@ def to_metadata(browser, log, entry_, timeout): id_url = entry_id(entry_)[0].text + google_id = id_url.split('/')[-1] title_ = ': '.join([x.text for x in title(entry_)]).strip() authors = [x.text.strip() for x in creator(entry_) if x.text] if not authors: @@ -78,6 +80,7 @@ def to_metadata(browser, log, entry_, timeout): return None mi = Metadata(title_, authors) + mi.identifiers = {'google':google_id} try: raw = get_details(browser, id_url, timeout) feed = etree.fromstring(xml_to_unicode(clean_ascii_chars(raw), @@ -103,9 +106,12 @@ def to_metadata(browser, log, entry_, timeout): t = str(x.text).strip() if t[:5].upper() in ('ISBN:', 'LCCN:', 'OCLC:'): if t[:5].upper() == 'ISBN:': - isbns.append(t[5:]) + t = check_isbn(t[5:]) + if t: + isbns.append(t) if isbns: mi.isbn = sorted(isbns, key=len)[-1] + mi.all_isbns = isbns # Tags try: @@ -133,20 +139,6 @@ def to_metadata(browser, log, entry_, timeout): return mi -def get_all_details(br, log, entries, abort, result_queue, timeout): - for i in entries: - try: - ans = to_metadata(br, log, i, timeout) - if isinstance(ans, Metadata): - result_queue.put(ans) - except: - log.exception( - 'Failed to get metadata for identify entry:', - etree.tostring(i)) - if abort.is_set(): - break - - class GoogleBooks(Source): name = 'Google Books' @@ -185,6 +177,36 @@ class GoogleBooks(Source): 'min-viewability':'none', }) + def cover_url_from_identifiers(self, identifiers): + goog = identifiers.get('google', None) + if goog is None: + isbn = identifiers.get('isbn', None) + goog = self.cached_isbn_to_identifier(isbn) + if goog is not None: + return ('http://books.google.com/books?id=%s&printsec=frontcover&img=1' % + goog) + + def is_cover_image_valid(self, raw): + # When no cover is present, returns a PNG saying image not available + # Try for example google identifier llNqPwAACAAJ + # I have yet to see an actual cover in PNG format + return raw and len(raw) > 17000 and raw[1:4] != 'PNG' + + def get_all_details(self, br, log, entries, abort, result_queue, timeout): + for i in entries: + try: + ans = to_metadata(br, log, i, timeout) + if isinstance(ans, Metadata): + result_queue.put(ans) + for isbn in ans.all_isbns: + self.cache_isbn_to_identifier(isbn, + ans.identifiers['google']) + except: + log.exception( + 'Failed to get metadata for identify entry:', + etree.tostring(i)) + if abort.is_set(): + break def identify(self, log, result_queue, abort, title=None, authors=None, identifiers={}, timeout=5): @@ -207,8 +229,8 @@ class GoogleBooks(Source): return as_unicode(e) # There is no point running these queries in threads as google - # throttles requests returning Forbidden errors - get_all_details(br, log, entries, abort, result_queue, timeout) + # throttles requests returning 403 Forbidden errors + self.get_all_details(br, log, entries, abort, result_queue, timeout) return None @@ -218,8 +240,14 @@ if __name__ == '__main__': title_test) test_identify_plugin(GoogleBooks.name, [ + ( - {'title': 'Great Expectations', 'authors':['Charles Dickens']}, - [title_test('Great Expectations', exact=True)] + {'identifiers':{'isbn': '0743273567'}}, + [title_test('The great gatsby', exact=True)] ), + + #( + # {'title': 'Great Expectations', 'authors':['Charles Dickens']}, + # [title_test('Great Expectations', exact=True)] + #), ]) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 9389964962..ccc452f1f8 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -515,7 +515,7 @@ class Metadata(object): 'publisher', 'relation', 'rights', 'source', 'subject', 'title', 'type']) CALIBRE_TERMS = set(['series', 'series_index', 'rating', 'timestamp', - 'publication_type']) + 'publication_type', 'title_sort']) OPF_ATTRS = {'role': OPF('role'), 'file-as': OPF('file-as'), 'scheme': OPF('scheme'), 'event': OPF('event'), 'type': XSI('type'), 'lang': XML('lang'), 'id': 'id'} diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py index f1ce31f25b..19c209b74d 100644 --- a/src/calibre/ebooks/oeb/transforms/metadata.py +++ b/src/calibre/ebooks/oeb/transforms/metadata.py @@ -18,7 +18,8 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False): if mi.title_sort: if not m.title: m.add('title', mi.title_sort) - m.title[0].file_as = mi.title_sort + m.clear('title_sort') + m.add('title_sort', mi.title_sort) if not mi.is_null('authors'): m.filter('creator', lambda x : x.role.lower() in ['aut', '']) for a in mi.authors: diff --git a/src/calibre/gui2/dialogs/drm_error.ui b/src/calibre/gui2/dialogs/drm_error.ui index ff28ef5a48..c4b9a1cfdb 100644 --- a/src/calibre/gui2/dialogs/drm_error.ui +++ b/src/calibre/gui2/dialogs/drm_error.ui @@ -44,7 +44,8 @@ <p>This book is locked by <b>DRM</b>. To learn more about DRM and why you cannot read or convert this book in calibre, -<a href="http://bugs.calibre-ebook.com/wiki/DRM">click here</a>. + <a href="http://drmfree.calibre-ebook.com/about#drm">click here</a>.<p>A large number of recent, DRM free releases are + available at <a href="http://drmfree.calibre-ebook.com">Open Books</a>. true diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index 426747e044..c84b3180f7 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -209,7 +209,6 @@ class EmailMixin(object): # {{{ def __init__(self): self.emailer = Emailer(self.job_manager) - self.emailer.start() def send_by_mail(self, to, fmts, delete_from_library, send_ids=None, do_auto_convert=True, specific_format=None): @@ -255,6 +254,8 @@ class EmailMixin(object): # {{{ to_s = list(repeat(to, len(attachments))) if attachments: + if not self.emailer.is_alive(): + self.emailer.start() self.emailer.send_mails(jobnames, Dispatcher(partial(self.email_sent, remove=remove)), attachments, to_s, subjects, texts, attachment_names) @@ -325,6 +326,8 @@ class EmailMixin(object): # {{{ files, auto = self.library_view.model().\ get_preferred_formats_from_ids([id_], fmts) return files + if not self.emailer.is_alive(): + self.emailer.start() sent_mails = self.emailer.email_news(mi, remove, get_fmts, self.email_sent) if sent_mails: diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index a1fc5bb490..bc965b89fa 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -7,7 +7,7 @@ 0 0 670 - 392 + 422 @@ -136,7 +136,7 @@ - Tags browser category partitioning method: + Tags browser category &partitioning method: opt_tags_browser_partition_method @@ -157,7 +157,7 @@ if you never want subcategories - Collapse when more items than: + &Collapse when more items than: opt_tags_browser_collapse_at @@ -193,7 +193,7 @@ up into sub-categories. If the partition method is set to disable, this value is - Categories with hierarchical items: + Categories with &hierarchical items: opt_categories_using_hierarchy @@ -205,9 +205,9 @@ up into sub-categories. If the partition method is set to disable, this value is A comma-separated list of columns in which items containing periods are displayed in the tag browser trees. For example, if -this box contains 'tags' then tags of the form 'mystery.English' -and 'mystery.Thriller' will be displayed with English and Thriller -both under the label 'mystery'. If 'tags' is not in this box, +this box contains 'tags' then tags of the form 'Mystery.English' +and 'Mystery.Thriller' will be displayed with English and Thriller +both under 'Mystery'. If 'tags' is not in this box, then the tags will be displayed each on their own line. diff --git a/src/calibre/gui2/tag_view.py b/src/calibre/gui2/tag_view.py index 04b336d791..1033957656 100644 --- a/src/calibre/gui2/tag_view.py +++ b/src/calibre/gui2/tag_view.py @@ -262,7 +262,6 @@ class TagsView(QTreeView): # {{{ tag_name = t.name tag_id = t.id can_edit = getattr(t, 'can_edit', True) - print can_edit, getattr(t, 'original_name', t.name), t.name while item.type != TagTreeItem.CATEGORY: item = item.parent diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 9b9308d253..8844446de6 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -633,7 +633,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ mb.stop() self.hide_windows() - self.emailer.stop() + if self.emailer.is_alive(): + self.emailer.stop() try: try: if self.content_server is not None: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index e515abd709..dce0b34aef 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -174,7 +174,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.prefs = DBPrefs(self) defs = self.prefs.defaults defs['gui_restriction'] = defs['cs_restriction'] = '' - defs['categories_using_hierarchy'] = '' + defs['categories_using_hierarchy'] = [] # Migrate saved search and user categories to db preference scheme def migrate_preference(key, default):