From 8d7494e5ecc1d8d2b1d780993bf478323d97ece0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Aug 2012 16:55:06 +0530 Subject: [PATCH 01/77] Conversion: Add an option under Structure Detection to set the 'Start reading at' metadata with an XPath expression. Fixes #1043233 (Add ability to create start point in ebooks) --- src/calibre/ebooks/conversion/cli.py | 2 +- src/calibre/ebooks/conversion/plumber.py | 10 ++ .../ebooks/oeb/transforms/structure.py | 31 ++++++- .../gui2/convert/structure_detection.py | 7 +- .../gui2/convert/structure_detection.ui | 93 ++++++++++--------- 5 files changed, 94 insertions(+), 49 deletions(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index a860b75839..fb1974f93b 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -170,7 +170,7 @@ def add_pipeline_options(parser, plumber): 'chapter', 'chapter_mark', 'prefer_metadata_cover', 'remove_first_image', 'insert_metadata', 'page_breaks_before', - 'remove_fake_margins', + 'remove_fake_margins', 'start_reading_at', ] ), diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 6a7b5e586b..dcb5add27e 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -304,6 +304,16 @@ OptionRecommendation(name='chapter_mark', 'to mark chapters.') ), +OptionRecommendation(name='start_reading_at', + recommended_value=None, level=OptionRecommendation.LOW, + help=_('An XPath expression to detect the location in the document' + ' at which to start reading. Some ebook reading programs' + ' (most prominently the Kindle) use this location as the' + ' position at which to open the book. See the XPath tutorial' + ' in the calibre User Manual for further help using this' + ' feature.') + ), + OptionRecommendation(name='extra_css', recommended_value=None, level=OptionRecommendation.LOW, help=_('Either the path to a CSS stylesheet or raw CSS. ' diff --git a/src/calibre/ebooks/oeb/transforms/structure.py b/src/calibre/ebooks/oeb/transforms/structure.py index b90774bcc7..3af4d87a13 100644 --- a/src/calibre/ebooks/oeb/transforms/structure.py +++ b/src/calibre/ebooks/oeb/transforms/structure.py @@ -6,7 +6,7 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re +import re, uuid from lxml import etree from urlparse import urlparse @@ -80,6 +80,35 @@ class DetectStructure(object): if not node.title or not node.title.strip(): node.title = _('Unnamed') + if self.opts.start_reading_at: + self.detect_start_reading() + + def detect_start_reading(self): + expr = self.opts.start_reading_at + try: + expr = XPath(expr) + except: + self.log.warn( + 'Invalid start reading at XPath expression, ignoring: %s'%expr) + return + for item in self.oeb.spine: + if not hasattr(item.data, 'xpath'): continue + matches = expr(item.data) + if matches: + elem = matches[0] + eid = elem.get('id', None) + if not eid: + eid = u'start_reading_at_'+unicode(uuid.uuid4()).replace(u'-', u'') + elem.set('id', eid) + if u'text' in self.oeb.guide: + self.oeb.guide.remove(u'text') + self.oeb.guide.add(u'text', u'Start', item.href+u'#'+eid) + self.log('Setting start reading at position to %s in %s'%( + self.opts.start_reading_at, item.href)) + return + self.log.warn("Failed to find start reading at position: %s"% + self.opts.start_reading_at) + def detect_chapters(self): self.detected_chapters = [] diff --git a/src/calibre/gui2/convert/structure_detection.py b/src/calibre/gui2/convert/structure_detection.py index b58c473bd4..0946d13e46 100644 --- a/src/calibre/gui2/convert/structure_detection.py +++ b/src/calibre/gui2/convert/structure_detection.py @@ -20,7 +20,7 @@ class StructureDetectionWidget(Widget, Ui_Form): def __init__(self, parent, get_option, get_help, db=None, book_id=None): Widget.__init__(self, parent, - ['chapter', 'chapter_mark', + ['chapter', 'chapter_mark', 'start_reading_at', 'remove_first_image', 'remove_fake_margins', 'insert_metadata', 'page_breaks_before'] ) @@ -31,15 +31,18 @@ class StructureDetectionWidget(Widget, Ui_Form): self.opt_chapter.set_msg(_('Detect chapters at (XPath expression):')) self.opt_page_breaks_before.set_msg(_('Insert page breaks before ' '(XPath expression):')) + self.opt_start_reading_at.set_msg( + _('Start reading at (XPath expression):')) def break_cycles(self): Widget.break_cycles(self) def pre_commit_check(self): - for x in ('chapter', 'page_breaks_before'): + for x in ('chapter', 'page_breaks_before', 'start_reading_at'): x = getattr(self, 'opt_'+x) if not x.check(): error_dialog(self, _('Invalid XPath'), _('The XPath expression %s is invalid.')%x.text).exec_() return False return True + diff --git a/src/calibre/gui2/convert/structure_detection.ui b/src/calibre/gui2/convert/structure_detection.ui index 4ba90c1c2c..21d285fb33 100644 --- a/src/calibre/gui2/convert/structure_detection.ui +++ b/src/calibre/gui2/convert/structure_detection.ui @@ -14,10 +14,40 @@ Form - + + + + Remove &fake margins + + + + + + + The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field. + + + true + + + + + + + + + + Insert &metadata as page at start of book + + + + + + + - + Chapter &mark: @@ -27,44 +57,14 @@ - + 20 - - - - Remove first &image - - - - - - - Insert &metadata as page at start of book - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + Qt::Horizontal @@ -77,22 +77,25 @@ - - + + - The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field. - - - true + Remove first &image - - - - Remove &fake margins + + + + Qt::Vertical - + + + 20 + 40 + + + From 653ea57586a0a1d059c8e6e5b410761db8723d80 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Aug 2012 17:12:44 +0530 Subject: [PATCH 02/77] Handle string with NULL bytes in them in the ICU upper/lower functions --- src/calibre/utils/icu.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index f1f94dc175..66ee8fd59f 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -82,6 +82,17 @@ def icu_sort_key(collator, obj): obj = obj.replace(b'\0', b'') return _secondary_collator.sort_key(obj) +def icu_change_case(upper, locale, obj): + func = _icu.upper if upper else _icu.lower + try: + return func(locale, obj) + except TypeError: + if isinstance(obj, unicode): + obj = obj.replace(u'\0', u'') + else: + obj = obj.replace(b'\0', b'') + return func(locale, obj) + def py_find(pattern, source): pos = source.find(pattern) if pos > -1: @@ -163,10 +174,10 @@ case_sensitive_sort_key = py_case_sensitive_sort_key if _icu_not_ok else \ case_sensitive_strcmp = cmp if _icu_not_ok else icu_case_sensitive_strcmp upper = (lambda s: s.upper()) if _icu_not_ok else \ - partial(_icu.upper, get_locale()) + partial(icu_change_case, True, get_locale()) lower = (lambda s: s.lower()) if _icu_not_ok else \ - partial(_icu.lower, get_locale()) + partial(icu_change_case, False, get_locale()) title_case = (lambda s: s.title()) if _icu_not_ok else \ partial(_icu.title, get_locale()) From 847e90600158d6161175b861a4498bd5a0ee10ac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Aug 2012 19:19:30 +0530 Subject: [PATCH 03/77] Catalogs: Fix regression that broke sorting of non series titles before series titles --- src/calibre/library/catalogs/epub_mobi_builder.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index a8cee7e1c9..d823d80cb3 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -1294,7 +1294,7 @@ Author '{0}': def add_books_to_HTML_by_month(this_months_list, dtc): if len(this_months_list): - this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort) + this_months_list = sorted(this_months_list, key=lambda x: sort_key(self.booksByAuthorSorter_author_sort(x))) # Create a new month anchor date_string = strftime(u'%B %Y', current_date.timetuple()) @@ -3091,14 +3091,14 @@ Author '{0}': Sort non-series books before series books ''' if not book['series']: - key = '%s %s' % (capitalize(book['author_sort']), + key = '%s ~%s' % (capitalize(book['author_sort']), capitalize(book['title_sort'])) else: index = book['series_index'] integer = int(index) fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) - key = '%s ~%s %s' % (capitalize(book['author_sort']), + key = '%s %s %s' % (capitalize(book['author_sort']), self.generateSortTitle(book['series']), series_index) return key @@ -3228,9 +3228,9 @@ Author '{0}': # Hack to force the cataloged leading letter to be # an unadorned character if the accented version sorts before the unaccented exceptions = { - u'Ä':u'A', - u'Ö':u'O', - u'Ü':u'U' + u'??':u'A', + u'??':u'O', + u'??':u'U' } if key is not None: From 80300df3f45c7e1947dc072cd65a7f27019a4059 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 30 Aug 2012 19:27:53 +0530 Subject: [PATCH 04/77] samefile_windows(); Return true if the strings are the same upto case differences. --- src/calibre/utils/filenames.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index c843100157..d9fd12d466 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -203,6 +203,11 @@ def samefile_windows(src, dst): import win32file from pywintypes import error + samestring = (os.path.normcase(os.path.abspath(src)) == + os.path.normcase(os.path.abspath(dst))) + if samestring: + return True + def get_fileid(x): if isbytestring(x): x = x.decode(filesystem_encoding) try: From 47119d6f42cef6540d54b043aaf3208c97993743 Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 30 Aug 2012 11:59:07 -0600 Subject: [PATCH 05/77] Fix for by_author sorting error introduced with sort_key --- src/calibre/library/catalogs/epub_mobi_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index a8cee7e1c9..1860dcfe47 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -1294,7 +1294,7 @@ Author '{0}': def add_books_to_HTML_by_month(this_months_list, dtc): if len(this_months_list): - this_months_list = sorted(this_months_list, key=self.booksByAuthorSorter_author_sort) + this_months_list = sorted(this_months_list, key=lambda x: sort_key(self.booksByAuthorSorter_author_sort(x))) # Create a new month anchor date_string = strftime(u'%B %Y', current_date.timetuple()) @@ -3091,14 +3091,14 @@ Author '{0}': Sort non-series books before series books ''' if not book['series']: - key = '%s %s' % (capitalize(book['author_sort']), + key = '%s ~%s' % (capitalize(book['author_sort']), capitalize(book['title_sort'])) else: index = book['series_index'] integer = int(index) fraction = index-integer series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) - key = '%s ~%s %s' % (capitalize(book['author_sort']), + key = '%s %s %s' % (capitalize(book['author_sort']), self.generateSortTitle(book['series']), series_index) return key From 7e3e206779387ee104711017e1cfa1f9ff0fffb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 00:03:17 +0530 Subject: [PATCH 06/77] ... --- src/calibre/library/catalogs/epub_mobi_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index d823d80cb3..1860dcfe47 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -3228,9 +3228,9 @@ Author '{0}': # Hack to force the cataloged leading letter to be # an unadorned character if the accented version sorts before the unaccented exceptions = { - u'??':u'A', - u'??':u'O', - u'??':u'U' + u'Ä':u'A', + u'Ö':u'O', + u'Ü':u'U' } if key is not None: From e50338f9c7a497221397ee36f91db578d9fc1509 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 08:41:50 +0530 Subject: [PATCH 07/77] version 0.8.67 --- Changelog.yaml | 50 ++++++++++++++++++++++++++++++++++++++++ src/calibre/constants.py | 2 +- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Changelog.yaml b/Changelog.yaml index 975d77247a..b0bd4dea38 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -19,6 +19,56 @@ # new recipes: # - title: +- version: 0.8.67 + date: 2012-08-31 + + new features: + - title: "PDF Output: Generate a PDF Outline based on the Table of Contents of the input document" + + - title: "Conversion: Add an option under Structure Detection to set the 'Start reading at' metadata with an XPath expression." + tickets: [1043233] + + - title: "Speed up changing the title and author of files with books larger than 3MB by avoiding an unnecessary extra copy." + + - title: "Wireless device driver: Make detecting and connecting to devices easier on networks where mdns is disabled" + + - title: "PDF Output: Allow choosing the default font family and size when generating PDF files (under PDF Options) in the conversion dialog" + + - title: "Metadata dialog: Comments editor: Allow specifying the name of a link when using the insert link button." + tickets: [1042683] + + - title: "Remove the unmaintained pdfmanipulate command line utility. There are many other tools that provide similar functionality, for example, pdftk and podofo" + + bug fixes: + - title: "Catalogs: Fix regression that broke sorting of non series titles before series titles" + + - title: "PDF Output: Do not create duplicate embedded fonts in the PDF for every individual HTML file in the input document" + + - title: "Fix regression that broke DnD of files having a # character in their names to the book details panel" + + - title: "PDF Output: Allow generating PDF files with more than 512 pages on windows." + tickets: [1041614] + + - title: "Fix minor bug in handling of the completion popups when using the next/previous buttons in the edit metadata dialog" + ticket: [1041389] + + improved recipes: + - Coding Horror + - TIME Magazine + + new recipes: + - title: Cumhuriyet Yzarlar + author: Sethi Eksi + + - title: Arcadia + author: Masahiro Hasegawa + + - title: Business Week Magazine and Chronicle of Higher Education + author: Rick Shang + + - title: CIPER Chile + author: Darko Miletic + - version: 0.8.66 date: 2012-08-24 diff --git a/src/calibre/constants.py b/src/calibre/constants.py index c1e0faba36..24c4791554 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -4,7 +4,7 @@ __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' __docformat__ = 'restructuredtext en' __appname__ = u'calibre' -numeric_version = (0, 8, 66) +numeric_version = (0, 8, 67) __version__ = u'.'.join(map(unicode, numeric_version)) __author__ = u"Kovid Goyal " From 40ce3d50371a899ec2f22b4fe05a864584dd7502 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 09:12:31 +0530 Subject: [PATCH 08/77] IGN:Tag release --- src/calibre/translations/calibre.pot | 731 +++++++++++---------------- 1 file changed, 308 insertions(+), 423 deletions(-) diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index 7c00385294..ecdb4a1e5d 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -4,9 +4,9 @@ # msgid "" msgstr "" -"Project-Id-Version: calibre 0.8.66\n" -"POT-Creation-Date: 2012-08-24 10:47+IST\n" -"PO-Revision-Date: 2012-08-24 10:47+IST\n" +"Project-Id-Version: calibre 0.8.67\n" +"POT-Creation-Date: 2012-08-31 08:42+IST\n" +"PO-Revision-Date: 2012-08-31 08:42+IST\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -30,13 +30,12 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/hanvon/driver.py:101 #: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:74 #: /home/kovid/work/calibre/src/calibre/devices/kindle/driver.py:77 -#: /home/kovid/work/calibre/src/calibre/devices/kobo/books.py:24 +#: /home/kovid/work/calibre/src/calibre/devices/kobo/books.py:25 #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:656 -#: /home/kovid/work/calibre/src/calibre/devices/mtp/unix/driver.py:165 -#: /home/kovid/work/calibre/src/calibre/devices/mtp/windows/driver.py:151 +#: /home/kovid/work/calibre/src/calibre/devices/mtp/unix/driver.py:191 +#: /home/kovid/work/calibre/src/calibre/devices/mtp/windows/driver.py:152 #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:70 #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:71 -#: /home/kovid/work/calibre/src/calibre/devices/prs500/books.py:267 #: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:661 #: /home/kovid/work/calibre/src/calibre/devices/prst1/driver.py:468 #: /home/kovid/work/calibre/src/calibre/devices/prst1/driver.py:469 @@ -129,23 +128,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ereader/writer.py:174 #: /home/kovid/work/calibre/src/calibre/ebooks/pdb/palmdoc/writer.py:29 #: /home/kovid/work/calibre/src/calibre/ebooks/pdb/ztxt/writer.py:27 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:82 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:83 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:73 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:74 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:63 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:64 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:52 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:65 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:66 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:63 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:64 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:62 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:63 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:81 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:82 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:111 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:112 +#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:108 +#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:109 #: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:425 #: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:433 #: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166 @@ -154,7 +138,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/add.py:159 #: /home/kovid/work/calibre/src/calibre/gui2/add.py:166 #: /home/kovid/work/calibre/src/calibre/gui2/book_details.py:667 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:42 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/__init__.py:41 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:124 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:143 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:145 @@ -188,12 +172,12 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/library/database2.py:585 #: /home/kovid/work/calibre/src/calibre/library/database2.py:593 #: /home/kovid/work/calibre/src/calibre/library/database2.py:604 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2180 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2334 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:2758 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3405 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3407 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3544 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2189 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2343 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:2767 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3414 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3416 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3553 #: /home/kovid/work/calibre/src/calibre/library/server/content.py:250 #: /home/kovid/work/calibre/src/calibre/library/server/content.py:251 #: /home/kovid/work/calibre/src/calibre/library/server/mobile.py:247 @@ -201,9 +185,6 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/library/server/opds.py:163 #: /home/kovid/work/calibre/src/calibre/library/server/xml.py:79 #: /home/kovid/work/calibre/src/calibre/utils/localization.py:188 -#: /home/kovid/work/calibre/src/calibre/utils/podofo/__init__.py:46 -#: /home/kovid/work/calibre/src/calibre/utils/podofo/__init__.py:66 -#: /home/kovid/work/calibre/src/calibre/utils/podofo/__init__.py:88 #: /home/kovid/work/calibre/src/calibre/web/feeds/recipes/collection.py:45 #: /home/kovid/work/calibre/src/calibre/web/feeds/recipes/collection.py:53 msgid "Unknown" @@ -885,26 +866,26 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:666 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:67 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:668 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1053 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1052 #: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:852 #: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:875 msgid "Yes" msgstr "" #: /home/kovid/work/calibre/src/calibre/db/fields.py:163 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1208 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1207 msgid "Main" msgstr "" #: /home/kovid/work/calibre/src/calibre/db/fields.py:165 #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:77 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1210 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1209 msgid "Card A" msgstr "" #: /home/kovid/work/calibre/src/calibre/db/fields.py:167 #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:79 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:1212 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:1211 msgid "Card B" msgstr "" @@ -1047,14 +1028,14 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1199 #: /home/kovid/work/calibre/src/calibre/library/database2.py:370 #: /home/kovid/work/calibre/src/calibre/library/database2.py:383 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3262 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3271 #: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:187 msgid "News" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2769 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3218 -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3236 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3227 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3245 msgid "Catalog" msgstr "" @@ -1111,8 +1092,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:264 #: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:268 #: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:324 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:857 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:859 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:885 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:887 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:277 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:279 msgid "Transferring books to device..." @@ -1122,8 +1103,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:344 #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:480 #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:515 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:870 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:881 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:898 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:909 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:301 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:332 msgid "Adding books to device metadata listing..." @@ -1145,8 +1126,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/bambook/driver.py:374 #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:468 #: /home/kovid/work/calibre/src/calibre/devices/kobo/driver.py:475 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:901 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:907 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:929 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:935 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:366 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:371 msgid "Removing books from device metadata listing..." @@ -1173,7 +1154,6 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/blackberry/driver.py:37 #: /home/kovid/work/calibre/src/calibre/devices/eb600/driver.py:281 #: /home/kovid/work/calibre/src/calibre/devices/nuut2/driver.py:18 -#: /home/kovid/work/calibre/src/calibre/devices/prs500/driver.py:90 msgid "Kovid Goyal" msgstr "" @@ -1611,10 +1591,6 @@ msgstr "" msgid "Communicate with the Nuut2 eBook reader." msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/prs500/driver.py:89 -msgid "Communicate with the Sony PRS-500 eBook reader." -msgstr "" - #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:22 msgid "Communicate with Sony eBook readers older than the PRST1." msgstr "" @@ -1624,23 +1600,23 @@ msgid "Comments have been removed as the SONY reader chokes on them" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:66 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:114 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:133 msgid "All by title" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:67 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:115 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:134 msgid "All by author" msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:70 #: /home/kovid/work/calibre/src/calibre/devices/prst1/driver.py:68 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:132 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:151 msgid "Comma separated list of metadata fields to turn into collections on the device. Possibilities include: " msgstr "" #: /home/kovid/work/calibre/src/calibre/devices/prs505/driver.py:73 -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:135 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:154 #, python-format msgid ". Two special collections are available: %(abt)s:%(abtv)s and %(aba)s:%(abav)s. Add these values to the list to enable them. The collections will be given the name provided after the \":\" character." msgstr "" @@ -1709,84 +1685,84 @@ msgstr "" msgid "Set this option if you want the author on the Sony to appear the same way the T1 sets it. This means it will only show the first author for books with multiple authors. Leave this disabled if you use Metadata Plugboards." msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:54 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:62 msgid "SmartDevice" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:56 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:64 msgid "Communicate with Smart Device apps" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:118 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:137 msgid "Enable connections at startup" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:119 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:138 msgid "Check this box to allow connections when calibre starts" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:121 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:140 msgid "Security password" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:122 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:141 msgid "Enter a password that the device app must use to connect to calibre" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:124 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:143 msgid "Use fixed network port" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:125 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:144 msgid "If checked, use the port number in the \"Port\" box, otherwise the driver will pick a random port" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:127 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:146 msgid "Port number: " msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:128 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:147 msgid "Enter the port number the driver is to use if the \"fixed port\" box is checked" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:129 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:148 msgid "Print extra debug information" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:130 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:149 msgid "Check this box if requested when reporting problems" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:140 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:159 msgid "Enable the no-activity timeout" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:141 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:160 #, python-format msgid "If this box is checked, calibre will automatically disconnect if a connected device does nothing for %d minutes. Unchecking this box disables this timeout, so calibre will never automatically disconnect." msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:591 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:616 #, python-format msgid "Too many connection attempts from %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:705 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:733 #: /home/kovid/work/calibre/src/calibre/devices/usbms/driver.py:95 msgid "Get device information..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:976 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1013 #, python-format msgid "Invalid port in options: %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:986 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1021 #, python-format msgid "Failed to connect to port %d. Try a different value." msgstr "" -#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:999 +#: /home/kovid/work/calibre/src/calibre/devices/smart_device_app/driver.py:1033 msgid "Failed to allocate a random port" msgstr "" @@ -2435,6 +2411,27 @@ msgstr "" msgid "Preserve the aspect ratio of the cover, instead of stretching it to fill the full first page of the generated pdf." msgstr "" +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:93 +msgid "The font family used to render serif fonts" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:96 +msgid "The font family used to render sans-serif fonts" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:99 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:103 +msgid "The font family used to render monospaced fonts" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:106 +msgid "The default font size" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pdf_output.py:109 +msgid "The default font size for monospaced text" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plugins/pml_output.py:22 msgid "Specify the character encoding of the output document. The default is cp1252." msgstr "" @@ -2660,261 +2657,265 @@ msgid "Specify how to mark detected chapters. A value of \"pagebreak\" will inse msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:309 +msgid "An XPath expression to detect the location in the document at which to start reading. Some ebook reading programs (most prominently the Kindle) use this location as the position at which to open the book. See the XPath tutorial in the calibre User Manual for further help using this feature." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:319 msgid "Either the path to a CSS stylesheet or raw CSS. This CSS will be appended to the style rules from the source file, so it can be used to override those rules." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:317 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:327 msgid "A comma separated list of CSS properties that will be removed from all CSS style rules. This is useful if the presence of some style information prevents it from being overridden on your device. For example: font-family,color,margin-left,margin-right" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:328 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:338 msgid "An XPath expression. Page breaks are inserted before the specified elements. To disable use the expression: /" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:334 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:344 msgid "Some documents specify page margins by specifying a left and right margin on each individual paragraph. calibre will try to detect and remove these margins. Sometimes, this can cause the removal of margins that should not have been removed. In this case you can disable the removal." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:345 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:355 #, python-format msgid "Set the top margin in pts. Default is %default. Setting this to less than zero will cause no margin to be set. Note: 72 pts equals 1 inch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:351 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:361 #, python-format msgid "Set the bottom margin in pts. Default is %default. Setting this to less than zero will cause no margin to be set. Note: 72 pts equals 1 inch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:357 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:367 #, python-format msgid "Set the left margin in pts. Default is %default. Setting this to less than zero will cause no margin to be set. Note: 72 pts equals 1 inch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:363 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:373 #, python-format msgid "Set the right margin in pts. Default is %default. Setting this to less than zero will cause no margin to be set. Note: 72 pts equals 1 inch" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:370 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:380 msgid "Change text justification. A value of \"left\" converts all justified text in the source to left aligned (i.e. unjustified) text. A value of \"justify\" converts all unjustified text to justified. A value of \"original\" (the default) does not change justification in the source file. Note that only some output formats support justification." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:380 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:390 msgid "Remove spacing between paragraphs. Also sets an indent on paragraphs of 1.5em. Spacing removal will not work if the source file does not use paragraphs (

or

tags)." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:387 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:397 msgid "When calibre removes blank lines between paragraphs, it automatically sets a paragraph indent, to ensure that paragraphs can be easily distinguished. This option controls the width of that indent (in em). If you set this value negative, then the indent specified in the input document is used, that is, calibre does not change the indentation." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:396 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:406 msgid "Use the cover detected from the source file in preference to the specified cover." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:402 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:412 msgid "Insert a blank line between paragraphs. Will not work if the source file does not use paragraphs (

or

tags)." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:409 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:419 msgid "Set the height of the inserted blank lines (in em). The height of the lines between paragraphs will be twice the value set here." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:416 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:426 msgid "Remove the first image from the input ebook. Useful if the input document has a cover image that is not identified as a cover. In this case, if you set a cover in calibre, the output document will end up with two cover images if you do not specify this option." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:425 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:435 msgid "Insert the book metadata at the start of the book. This is useful if your ebook reader does not support displaying/searching metadata directly." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:433 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:443 msgid "Convert plain quotes, dashes and ellipsis to their typographically correct equivalents. For details, see http://daringfireball.net/projects/smartypants" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:441 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:451 msgid "Convert fancy quotes, dashes and ellipsis to their plain equivalents." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:449 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:459 msgid "Read metadata from the specified OPF file. Metadata read from this file will override any metadata in the source file." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:456 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:466 #, python-format msgid "Transliterate unicode characters to an ASCII representation. Use with care because this will replace unicode characters with ASCII. For instance it will replace \"%s\" with \"Mikhail Gorbachiov\". Also, note that in cases where there are multiple representations of a character (characters shared by Chinese and Japanese for instance) the representation based on the current calibre interface language will be used." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:471 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:481 msgid "Preserve ligatures present in the input document. A ligature is a special rendering of a pair of characters like ff, fi, fl et cetera. Most readers do not have support for ligatures in their default fonts, so they are unlikely to render correctly. By default, calibre will turn a ligature into the corresponding pair of normal characters. This option will preserve them instead." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:483 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:493 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:38 msgid "Set the title." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:487 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:497 msgid "Set the authors. Multiple authors should be separated by ampersands." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:492 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:502 msgid "The version of the title to be used for sorting. " msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:496 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:506 msgid "String to be used when sorting by author. " msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:500 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:510 msgid "Set the cover to the specified file or URL" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:504 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:514 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:54 msgid "Set the ebook description." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:508 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:518 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:56 msgid "Set the ebook publisher." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:512 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:522 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:60 msgid "Set the series this ebook belongs to." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:516 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:526 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:62 msgid "Set the index of the book in this series." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:520 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:530 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:64 msgid "Set the rating. Should be a number between 1 and 5." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:524 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:534 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:66 msgid "Set the ISBN of the book." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:528 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:538 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:68 msgid "Set the tags for the book. Should be a comma separated list." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:532 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:542 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:70 msgid "Set the book producer." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:536 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:546 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/cli.py:72 msgid "Set the language." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:540 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:550 msgid "Set the publication date." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:544 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:554 msgid "Set the book timestamp (no longer used anywhere)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:548 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:558 msgid "Enable heuristic processing. This option must be set for any heuristic processing to take place." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:553 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:563 msgid "Detect unformatted chapter headings and sub headings. Change them to h2 and h3 tags. This setting will not create a TOC, but can be used in conjunction with structure detection to create one." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:560 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:570 msgid "Look for common words and patterns that denote italics and italicize them." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:565 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:575 msgid "Turn indentation created from multiple non-breaking space entities into CSS indents." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:570 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:580 msgid "Scale used to determine the length at which a line should be unwrapped. Valid values are a decimal between 0 and 1. The default is 0.4, just below the median line length. If only a few lines in the document require unwrapping this value should be reduced" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:578 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:588 msgid "Unwrap lines using punctuation and other formatting clues." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:582 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:592 msgid "Remove empty paragraphs from the document when they exist between every other paragraph" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:587 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:597 msgid "Left aligned scene break markers are center aligned. Replace soft scene breaks that use multiple blank lines with horizontal rules." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:593 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:603 msgid "Replace scene breaks with the specified text. By default, the text from the input document is used." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:598 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:608 msgid "Analyze hyphenated words throughout the document. The document itself is used as a dictionary to determine whether hyphens should be retained or removed." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:604 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:614 msgid "Looks for occurrences of sequential

or

tags. The tags are renumbered to prevent splitting in the middle of chapter headings." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:610 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:620 msgid "Search pattern (regular expression) to be replaced with sr1-replace." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:615 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:625 msgid "Replacement to replace the text found with sr1-search." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:619 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:629 msgid "Search pattern (regular expression) to be replaced with sr2-replace." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:624 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:634 msgid "Replacement to replace the text found with sr2-search." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:628 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:638 msgid "Search pattern (regular expression) to be replaced with sr3-replace." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:633 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:643 msgid "Replacement to replace the text found with sr3-search." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:637 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:647 msgid "Path to a file containing search and replace regular expressions. The file must contain alternating lines of regular expression followed by replacement pattern (which can be an empty line). The regular expression must be in the python regex syntax and the file must be UTF-8 encoded." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:746 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:756 msgid "Could not find an ebook inside the archive" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:804 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:814 msgid "Values of series index and rating must be numbers. Ignoring" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:811 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:821 msgid "Failed to parse date/time" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:973 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:983 msgid "Converting input to HTML..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:1000 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:1010 msgid "Running transforms on ebook..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:1113 +#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:1123 msgid "Creating" msgstr "" @@ -3114,11 +3115,11 @@ msgstr "" msgid "Convert LRS to LRS, useful for debugging." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:457 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:485 msgid "Invalid LRF file. Could not set metadata." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:582 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:610 msgid "" "%prog [options] mybook.lrf\n" "\n" @@ -3127,59 +3128,59 @@ msgid "" "\n" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:589 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:617 msgid "Set the book title" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:591 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:619 msgid "Set sort key for the title" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:593 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:621 msgid "Set the author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:595 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:623 msgid "Set sort key for the author" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:597 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:625 msgid "The category this book belongs to. E.g.: History" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:600 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:628 msgid "Path to a graphic that will be set as this files' thumbnail" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:603 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:631 msgid "Path to a txt file containing the comment to be stored in the lrf file." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:607 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:635 msgid "Extract thumbnail from LRF file" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:608 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:636 msgid "Set the publisher" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:609 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:637 msgid "Set the book classification" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:610 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:638 msgid "Set the book creator" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:611 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:639 msgid "Set the book producer" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:613 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:641 msgid "Extract cover from LRF file. Note that the LRF format has no defined cover, so we use some heuristics to guess the cover." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:615 +#: /home/kovid/work/calibre/src/calibre/ebooks/lrf/meta.py:643 msgid "Set book ID" msgstr "" @@ -3202,7 +3203,6 @@ msgid "No" msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:769 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:45 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/quickview.py:85 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:58 @@ -3231,7 +3231,6 @@ msgid "Publisher" msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:772 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:49 msgid "Producer" msgstr "" @@ -3636,182 +3635,10 @@ msgstr "" msgid "Sidebar" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/cli.py:31 -msgid "" -"command ...\n" -"\n" -"command can be one of the following:\n" -"[%%commands]\n" -"\n" -"Use %prog command --help to get more information about a specific command\n" -"\n" -"Manipulate a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:29 -msgid "" -"[options] file.pdf\n" -"\n" -"Crop a PDF file.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:38 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:32 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:34 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:36 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:34 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:33 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:41 -msgid "Path to output file. By default a file is created in the current directory." -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:41 +#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/outline_writer.py:49 +#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/outline_writer.py:57 #, python-format -msgid "Number of pixels to crop from the left most x (default is %s)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:44 -#, python-format -msgid "Number of pixels to crop from the left most y (default is %s)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:47 -#, python-format -msgid "Number of pixels to crop from the right most x (default is %s)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:50 -#, python-format -msgid "Number of pixels to crop from the right most y (default is %s)" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:53 -msgid "A file generated by ghostscript which allows each page to be individually cropped `gs -dSAFER -dNOPAUSE -dBATCH -sDEVICE=bbox file.pdf 2> bounding`" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:73 -msgid "Crop Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/crop.py:73 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:60 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:54 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:56 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:54 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:53 -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:61 -msgid "Options to control the transformation of pdf" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:23 -msgid "" -"[options] file.pdf password\n" -"\n" -"Decrypt a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/decrypt.py:60 -msgid "Decrypt Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:25 -msgid "" -"[options] file.pdf password\n" -"\n" -"Encrypt a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/encrypt.py:54 -msgid "Encrypt Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:21 -msgid "" -"file.pdf ...\n" -"\n" -"Get info about a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:46 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:49 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:305 -msgid "Author" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:47 -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:27 -msgid "Subject" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:48 -msgid "Creator" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:50 -msgid "Pages" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:51 -msgid "File Size" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:52 -msgid "PDF Version" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:25 -msgid "" -"[options] file1.pdf file2.pdf ...\n" -"\n" -"Metadata will be used from the first PDF specified.\n" -"\n" -"Merges individual PDFs.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/merge.py:56 -msgid "Merge Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:25 -msgid "" -"[options] file.pdf\n" -"\n" -"Reverse a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/reverse.py:54 -msgid "Reverse Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:24 -msgid "" -"file.pdf degrees\n" -"\n" -"Rotate pages of a PDF clockwise.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:53 -msgid "Rotate Options:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:25 -msgid "" -"\n" -"%prog %%name [options] file.pdf page_to_split_on ...\n" -"%prog %%name [options] file.pdf page_range_to_split_on ...\n" -"\t\n" -"Ex.\n" -"\t\n" -"%prog %%name file.pdf 6\n" -"%prog %%name file.pdf 6-12\n" -"%prog %%name file.pdf 6-12 8 10 9-20\n" -"\n" -"Split a PDF.\n" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:61 -msgid "Split Options:" +msgid "Page %d" msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/pdftohtml.py:71 @@ -4766,6 +4593,10 @@ msgstr "" msgid "Do you want wireless device connections to be started automatically when calibre starts?" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/device.py:245 +msgid "Many IP addresses. See Start/Stop dialog." +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_collections.py:13 msgid "Manage collections" msgstr "" @@ -5761,12 +5592,12 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_input_ui.py:36 #: /home/kovid/work/calibre/src/calibre/gui2/convert/pdb_output_ui.py:47 #: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_input_ui.py:43 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:54 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:96 #: /home/kovid/work/calibre/src/calibre/gui2/convert/pmlz_output_ui.py:46 #: /home/kovid/work/calibre/src/calibre/gui2/convert/rb_output_ui.py:33 #: /home/kovid/work/calibre/src/calibre/gui2/convert/search_and_replace_ui.py:145 #: /home/kovid/work/calibre/src/calibre/gui2/convert/snb_output_ui.py:42 -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:59 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/convert/toc_ui.py:70 #: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_input_ui.py:91 #: /home/kovid/work/calibre/src/calibre/gui2/convert/txt_output_ui.py:87 @@ -6187,19 +6018,23 @@ msgstr "" msgid "Choose background color" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:194 +#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:209 msgid "Create link" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:195 -msgid "Enter URL" +#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:215 +msgid "Enter &URL:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:554 +#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:216 +msgid "Enter name (optional):" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:577 msgid "Normal view" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:555 +#: /home/kovid/work/calibre/src/calibre/gui2/comments_editor.py:578 msgid "HTML Source" msgstr "" @@ -7011,20 +6846,60 @@ msgstr "" msgid "PDF Output" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:55 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:97 msgid "&Paper Size:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:56 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:98 msgid "&Orientation:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:57 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:99 +msgid "&Custom size:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:100 msgid "Preserve &aspect ratio of cover" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:58 -msgid "&Custom size:" +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:101 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:362 +msgid "Se&rif family:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:102 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:363 +msgid "&Sans family:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:103 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:364 +msgid "&Monospace family:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:104 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:369 +msgid "S&tandard font:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:105 +msgid "Default font si&ze:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:106 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:108 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:366 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:368 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:383 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:385 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:387 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:391 +msgid " px" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/pdf_output_ui.py:107 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:367 +msgid "Monospace &font size:" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/pml_output.py:14 @@ -7307,35 +7182,39 @@ msgstr "" msgid "Insert page breaks before (XPath expression):" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:42 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:35 +msgid "Start reading at (XPath expression):" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:44 #: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:39 msgid "Invalid XPath" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:43 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection.py:45 #: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:40 #, python-format msgid "The XPath expression %s is invalid." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:60 -msgid "Chapter &mark:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:61 -msgid "Remove first &image" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:62 -msgid "Insert &metadata as page at start of book" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:63 -msgid "The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field." +msgid "Remove &fake margins" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:64 -msgid "Remove &fake margins" +msgid "The header and footer removal options have been replaced by the Search & Replace options. Click the Search & Replace category in the bar to the left to use these options. Leave the replace field blank and enter your header/footer removal regexps into the search field." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:65 +msgid "Insert &metadata as page at start of book" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:66 +msgid "Chapter &mark:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/convert/structure_detection_ui.py:67 +msgid "Remove first &image" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/convert/toc.py:16 @@ -8329,6 +8208,12 @@ msgstr "" msgid "All checked books will be permanently deleted from your device. Please verify the list." msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:49 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:305 +msgid "Author" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76 msgid "Location" msgstr "" @@ -9112,7 +8997,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:65 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:213 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:83 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:92 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:148 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email.py:81 msgid "&Show password" @@ -9959,79 +9844,104 @@ msgstr "" msgid "Choose formats" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:21 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:46 msgid "Use a password if calibre is running on a network that is not secure. For example, if you run calibre on a laptop, use that laptop in an airport, and want to connect your smart device to calibre, you should use a password." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:27 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:52 msgid "Check this box if you want calibre to automatically start the smart device interface when calibre starts. You should not do this if you are using a network that is not secure and you are not setting a password." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:33 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:58 msgid "Check this box if you want calibre to use a fixed network port. Normally you will not need to do this. However, if your device consistently fails to connect to calibre, try checking this box and entering a number." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:39 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:64 msgid "Try 9090. If calibre says that it fails to connect to the port, try another number. You can use any number between 8,000 and 32,000." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:79 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:85 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:90 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:70 +msgid "These are the IP addresses for this computer. If you decide to have your device connect to calibre using a fixed IP address, one of these addresses should be the one you use. It is unlikely but possible that the correct IP address is not listed here, in which case you will need to go to your computer's control panel to get a complete list of your computer's network interfaces and IP addresses." +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:101 +msgid "Enable automatic metadata management" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:104 +msgid "Enabling automatic metadata management tells calibre to send any changes you made to books' metadata when your device is connected, which is the most useful setting when using the wireless device interface. If automatic metadata management is not enabled, changes are sent only when you re-send the book. You can get more information or change this preference to some other choice at Preferences -> Sending books to devices -> Metadata management" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:123 +msgid "Automatic metadata management is enabled" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:137 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:143 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:148 msgid "Invalid port number" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:80 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:138 msgid "You must provide a port number." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:86 -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:91 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:144 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:149 msgid "The port must be a number between 8000 and 32000." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:106 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:164 #: /home/kovid/work/calibre/src/calibre/gui2/ui.py:394 msgid "Problem starting the wireless device" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:107 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice.py:165 #: /home/kovid/work/calibre/src/calibre/gui2/ui.py:395 #, python-format msgid "The wireless device driver did not start. It said \"%s\"" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:78 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:85 msgid "Smart device control" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:79 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:86 msgid "" "

Start wireless device connections.\n" "

You may see some messages from your computer's firewall or anti-virus manager asking you if it is OK for calibre to connect to the network. Please answer yes. If you do not, wireless connections will not work." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:81 -msgid "Optional password for security" +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:88 +msgid "Calibre IP addresses:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:82 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:89 +msgid "Possibe IP addresses:" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:90 msgid "Optional &password:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:84 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:91 +msgid "Optional password for security" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:93 msgid "Optional &fixed port:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:85 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:94 msgid "Optional port number" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:86 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:95 msgid "&Use a fixed port" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:87 +#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/smartdevice_ui.py:96 msgid "&Automatically allow connections at calibre startup" msgstr "" @@ -11104,12 +11014,12 @@ msgid "LRF Viewer toolbar" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:131 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:521 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:514 msgid "Next Page" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:132 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:522 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:515 msgid "Previous Page" msgstr "" @@ -12668,6 +12578,10 @@ msgstr "" msgid "Email" msgstr "" +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:27 +msgid "Subject" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:32 msgid "Formats to email. The first matching format will be sent." msgstr "" @@ -12680,7 +12594,7 @@ msgstr "" msgid "If checked, downloaded news will be automatically mailed
to this email address (provided it is in one of the listed formats)." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:115 +#: /home/kovid/work/calibre/src/calibre/gui2/preferences/emailp.py:117 msgid "new email address" msgstr "" @@ -15082,39 +14996,10 @@ msgstr "" msgid "Configure Ebook viewer" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:362 -msgid "Se&rif family:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:363 -msgid "&Sans family:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:364 -msgid "&Monospace family:" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:365 msgid "&Default font size:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:366 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:368 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:383 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:385 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:387 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:391 -msgid " px" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:367 -msgid "Monospace &font size:" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:369 -msgid "S&tandard font:" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:370 msgid "Serif" msgstr "" @@ -15303,44 +15188,44 @@ msgstr "" msgid "No results found for:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:487 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:480 msgid "&Lookup in dictionary" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:492 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:485 msgid "View &image..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:495 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:488 msgid "&Search for next occurrence" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:500 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:493 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:138 msgid "Go to..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:512 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:505 msgid "Next Section" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:513 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:506 msgid "Previous Section" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:515 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:508 msgid "Document Start" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:516 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:509 msgid "Document End" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:518 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:511 msgid "Section Start" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:519 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:512 msgid "Section End" msgstr "" @@ -16938,17 +16823,17 @@ msgstr "" msgid "creating custom column " msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3570 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3579 #, python-format msgid "

Migrating old database to ebook library in %s

" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3599 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3608 #, python-format msgid "Copying %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/library/database2.py:3616 +#: /home/kovid/work/calibre/src/calibre/library/database2.py:3625 msgid "Compacting database" msgstr "" From 47dd9752f335908b147b8d93d4975e560056553c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 11:28:02 +0530 Subject: [PATCH 09/77] ... --- setup/translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/translations.py b/setup/translations.py index 28be777345..3f821b7556 100644 --- a/setup/translations.py +++ b/setup/translations.py @@ -152,7 +152,7 @@ class Translations(POT): # {{{ subprocess.check_call(['msgfmt', '-o', dest, iso639]) elif locale not in ('en_GB', 'en_CA', 'en_AU', 'si', 'ur', 'sc', 'ltg', 'nds', 'te', 'yi', 'fo', 'sq', 'ast', 'ml', 'ku', - 'fr_CA', 'him'): + 'fr_CA', 'him', 'jv', 'ka'): self.warn('No ISO 639 translations for locale:', locale) self.write_stats() From 6627f20c2cd4b628954adbb05ce52ee161c8e4f0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 11:56:47 +0530 Subject: [PATCH 10/77] MTP: Implement get_device_information() --- src/calibre/devices/mtp/base.py | 3 +- src/calibre/devices/mtp/driver.py | 76 ++++++++++++++++++++- src/calibre/devices/mtp/filesystem_cache.py | 28 ++++++++ src/calibre/devices/mtp/unix/driver.py | 20 +----- src/calibre/devices/mtp/windows/driver.py | 16 +---- src/calibre/devices/usbms/driver.py | 2 +- 6 files changed, 109 insertions(+), 36 deletions(-) diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 3e7dc63f87..4f8bbc991f 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -35,13 +35,14 @@ class MTPDeviceBase(DevicePlugin): DevicePlugin.__init__(self, *args, **kwargs) self.progress_reporter = None self.current_friendly_name = None + self.report_progress = lambda x, y: None def reset(self, key='-1', log_packets=False, report_progress=None, detected_device=None): pass def set_progress_reporter(self, report_progress): - self.progress_reporter = report_progress + self.report_progress = report_progress def get_gui_name(self): return self.current_friendly_name or self.name diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 54827d234a..53a1fe36db 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -7,14 +7,86 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from calibre.constants import iswindows +import json, pprint +from io import BytesIO + +from calibre.constants import iswindows, numeric_version +from calibre.utils.config import from_json, to_json +from calibre.utils.date import now, isoformat if iswindows: from calibre.devices.mtp.windows.driver import MTP_DEVICE as BASE BASE else: from calibre.devices.mtp.unix.driver import MTP_DEVICE as BASE +pprint class MTP_DEVICE(BASE): - pass + + METADATA_CACHE = 'metadata.calibre' + DRIVEINFO = 'driveinfo.calibre' + + def _update_drive_info(self, storage, location_code, name=None): + import uuid + f = storage.find_path((self.DRIVEINFO,)) + dinfo = {} + if f is not None: + stream = self.get_file(f) + try: + dinfo = json.load(stream, object_hook=from_json) + except: + dinfo = None + if dinfo.get('device_store_uuid', None) is None: + dinfo['device_store_uuid'] = unicode(uuid.uuid4()) + if dinfo.get('device_name', None) is None: + dinfo['device_name'] = self.current_friendly_name + if name is not None: + dinfo['device_name'] = name + dinfo['location_code'] = location_code + dinfo['last_library_uuid'] = getattr(self, 'current_library_uuid', None) + dinfo['calibre_version'] = '.'.join([unicode(i) for i in numeric_version]) + dinfo['date_last_connected'] = isoformat(now()) + dinfo['mtp_prefix'] = storage.storage_prefix + raw = json.dumps(dinfo, default=to_json) + self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) + self.driveinfo = dinfo + + def open(self, devices, library_uuid): + self.current_library_uuid = library_uuid + BASE.open(self, devices, library_uuid) + + def get_device_information(self, end_session=True): + self.report_progress(1.0, _('Get device information...')) + self.driveinfo = {} + for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, + 'A'), (self._cardb_id, 'B')): + if sid is None: continue + self._update_drive_info(self.filesystem_cache.storage(sid), location_code) + dinfo = self.get_basic_device_information() + return tuple( list(dinfo) + [self.driveinfo] ) + + def card_prefix(self, end_session=True): + ans = [None, None] + if self._carda_id is not None: + ans[0] = self.filesystem_cache.storage(self._carda_id).storage_prefix + if self._cardb_id is not None: + ans[1] = self.filesystem_cache.storage(self._cardb_id).storage_prefix + return tuple(ans) + +if __name__ == '__main__': + dev = MTP_DEVICE(None) + dev.startup() + try: + from calibre.devices.scanner import DeviceScanner + scanner = DeviceScanner() + scanner.scan() + devs = scanner.devices + cd = dev.detect_managed_devices(devs) + if cd is None: + raise ValueError('Failed to detect MTP device') + dev.open(cd, None) + pprint.pprint(dev.get_device_information()) + finally: + dev.shutdown() + diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 3370967054..cd97c5c2ed 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -47,6 +47,9 @@ class FileOrFolder(object): self.fs_cache = weakref.ref(fs_cache) self.deleted = False + if self.storage_id == self.object_id: + self.storage_prefix = 'mtp:::%s:::'%self.persistent_id + def __repr__(self): name = 'Folder' if self.is_folder else 'File' try: @@ -125,6 +128,26 @@ class FileOrFolder(object): return e return None + def find_path(self, path): + ''' + Find a path in this folder, where path is a + tuple of folder and file names like ('eBooks', 'newest', + 'calibre.epub'). Finding is case-insensitive. + ''' + parent = self + components = list(path) + while components: + child = components[0] + components = components[1:] + c = parent.folder_named(child) + if c is None: + c = parent.file_named(child) + if c is None: + return None + parent = c + return parent + + class FilesystemCache(object): def __init__(self, all_storage, entries): @@ -164,4 +187,9 @@ class FilesystemCache(object): for e in self.entries: e.dump(out=out) + def storage(self, storage_id): + for e in self.entries: + if e.storage_id == storage_id: + return e + diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index e179647629..5d7d767a9b 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -46,14 +46,6 @@ class MTP_DEVICE(MTPDeviceBase): def set_debug_level(self, lvl): self.libmtp.set_debug_level(lvl) - def report_progress(self, sent, total): - try: - p = int(sent/total * 100) - except ZeroDivisionError: - p = 100 - if self.progress_reporter is not None: - self.progress_reporter(p) - @synchronous def detect_managed_devices(self, devices_on_system, force_refresh=False): if self.libmtp is None: return None @@ -212,19 +204,10 @@ class MTP_DEVICE(MTPDeviceBase): return self._filesystem_cache @synchronous - def get_device_information(self, end_session=True): + def get_basic_device_information(self): d = self.dev return (self.current_friendly_name, d.device_version, d.device_version, '') - @synchronous - def card_prefix(self, end_session=True): - ans = [None, None] - if self._carda_id is not None: - ans[0] = 'mtp:::%d:::'%self._carda_id - if self._cardb_id is not None: - ans[1] = 'mtp:::%d:::'%self._cardb_id - return tuple(ans) - @synchronous def total_space(self, end_session=True): ans = [0, 0, 0] @@ -298,6 +281,7 @@ class MTP_DEVICE(MTPDeviceBase): if not ok: raise DeviceError('Failed to get file: %s with errors: %s'%( f.full_path, self.format_errorstack(errs))) + stream.seek(0) return stream @synchronous diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 63eef1df66..0506f63054 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -203,24 +203,11 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name = devdata.get('friendly_name', None) @same_thread - def get_device_information(self, end_session=True): + def get_basic_device_information(self): d = self.dev.data dv = d.get('device_version', '') - for sid, location_code in ( (self._main_id, 'main'), (self._carda_id, - 'A'), (self._cardb_id, 'B')): - if sid is None: continue - # TODO: Implement the drive info dict return (self.current_friendly_name, dv, dv, '') - @same_thread - def card_prefix(self, end_session=True): - ans = [None, None] - if self._carda_id is not None: - ans[0] = 'mtp:::%s:::'%self._carda_id - if self._cardb_id is not None: - ans[1] = 'mtp:::%s:::'%self._cardb_id - return tuple(ans) - @same_thread def total_space(self, end_session=True): ans = [0, 0, 0] @@ -260,6 +247,7 @@ class MTP_DEVICE(MTPDeviceBase): except Exception as e: raise DeviceError('Failed to fetch the file %s with error: %s'% f.full_path, as_unicode(e)) + stream.seek(0) return stream @same_thread diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index b86d61182d..12e30073ac 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -63,7 +63,7 @@ class USBMS(CLI, Device): dinfo = {} if dinfo.get('device_store_uuid', None) is None: dinfo['device_store_uuid'] = unicode(uuid.uuid4()) - if dinfo.get('device_name') is None: + if dinfo.get('device_name', None) is None: dinfo['device_name'] = self.get_gui_name() if name is not None: dinfo['device_name'] = name From 8d881f9f7f647849cd013d775a4f81728a52739e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 12:00:50 +0530 Subject: [PATCH 11/77] ... --- src/calibre/devices/mtp/driver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 53a1fe36db..c3e34a2be5 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -73,6 +73,14 @@ class MTP_DEVICE(BASE): ans[1] = self.filesystem_cache.storage(self._cardb_id).storage_prefix return tuple(ans) + def set_driveinfo_name(self, location_code, name): + sid = {'main':self._main_id, 'A':self._carda_id, + 'B':self._cardb_id}.get(location_code, None) + if sid is None: + return + self._update_drive_info(self.filesystem_cache.storage(sid), + location_code, name=name) + if __name__ == '__main__': dev = MTP_DEVICE(None) dev.startup() From b7f2788244479b03753718d0b4a20799f2a7be6f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 15:11:08 +0530 Subject: [PATCH 12/77] ... --- src/calibre/ebooks/pdf/writer.py | 14 +++++++++----- src/calibre/gui2/convert/__init__.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index d6ffa42107..d37db69a81 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -137,11 +137,15 @@ class Page(QWebPage): std = {'serif':opts.pdf_serif_family, 'sans':opts.pdf_sans_family, 'mono':opts.pdf_mono_family}.get(opts.pdf_standard_font, opts.pdf_serif_family) - settings.setFontFamily(QWebSettings.StandardFont, std) - settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family) - settings.setFontFamily(QWebSettings.SansSerifFont, - opts.pdf_sans_family) - settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family) + if std: + settings.setFontFamily(QWebSettings.StandardFont, std) + if opts.pdf_serif_family: + settings.setFontFamily(QWebSettings.SerifFont, opts.pdf_serif_family) + if opts.pdf_sans_family: + settings.setFontFamily(QWebSettings.SansSerifFont, + opts.pdf_sans_family) + if opts.pdf_mono_family: + settings.setFontFamily(QWebSettings.FixedFont, opts.pdf_mono_family) def javaScriptConsoleMessage(self, msg, lineno, msgid): self.log.debug(u'JS:', unicode(msg)) diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index e01238a2e5..38fb641987 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -143,7 +143,7 @@ class Widget(QWidget): ans = None return ans elif isinstance(g, QFontComboBox): - ans = unicode(QFontInfo(g.currentFont().family())) + return unicode(QFontInfo(g.currentFont()).family()) elif isinstance(g, EncodingComboBox): ans = unicode(g.currentText()).strip() try: From 7a3d5937e7b650cfa96f62a27b5cf05e20e72cd5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 17:53:45 +0530 Subject: [PATCH 13/77] Fix #1044345 (Coby Kyros 7035 Not recognized) --- src/calibre/devices/android/driver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 45672fdbd1..9e8aa5fe17 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -197,7 +197,8 @@ class ANDROID(USBMS): 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', - 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0'] + 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0', + 'COBY_MID'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', @@ -216,7 +217,7 @@ class ANDROID(USBMS): 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID', - 'S5830I_CARD', 'MID7042', 'LINK-CREATE'] + 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', @@ -226,7 +227,7 @@ class ANDROID(USBMS): 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875', 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', - 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042'] + 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035'] OSX_MAIN_MEM = 'Android Device Main Memory' From 944fb4a7febbe46b205c9959950b9d36b9ceb46c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 18:11:14 +0530 Subject: [PATCH 14/77] ... --- src/calibre/utils/filenames.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/utils/filenames.py b/src/calibre/utils/filenames.py index d9fd12d466..65451dab9c 100644 --- a/src/calibre/utils/filenames.py +++ b/src/calibre/utils/filenames.py @@ -229,6 +229,10 @@ def samefile(src, dst): symlinks, case insensitivity, mapped drives, etc. Returns True iff both paths exist and point to the same file on disk. + + Note: On windows will return True if the two string are identical (upto + case) even if the file does not exist. This is because I have no way of + knowing how reliable the GetFileInformationByHandle method is. ''' if iswindows: return samefile_windows(src, dst) From 359813d7ebce8ad98a04de33fd8d215e7dd483a6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 18:45:27 +0530 Subject: [PATCH 15/77] MTP: Implement getting list of books and their metadata from device --- src/calibre/devices/mtp/base.py | 33 ++++++- src/calibre/devices/mtp/books.py | 38 ++++++++ src/calibre/devices/mtp/driver.py | 101 +++++++++++++++++++- src/calibre/devices/mtp/filesystem_cache.py | 14 +++ src/calibre/devices/mtp/unix/driver.py | 2 +- src/calibre/devices/mtp/windows/driver.py | 2 +- 6 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 src/calibre/devices/mtp/books.py diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 4f8bbc991f..3b71e40619 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -7,10 +7,17 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from functools import wraps +import re +from functools import wraps, partial +from calibre import prints +from calibre.constants import DEBUG from calibre.devices.interface import DevicePlugin +def debug(*args, **kwargs): + if DEBUG: + prints('MTP:', *args, **kwargs) + def synchronous(func): @wraps(func) def synchronizer(self, *args, **kwargs): @@ -53,4 +60,28 @@ class MTPDeviceBase(DevicePlugin): # return False return False + def build_template_regexp(self): + return None + # TODO: Implement this + def replfunc(match, seen=None): + v = match.group(1) + if v in ['authors', 'author_sort']: + v = 'author' + if v in ('title', 'series', 'series_index', 'isbn', 'author'): + if v not in seen: + seen.add(v) + return '(?P<' + v + '>.+?)' + return '(.+?)' + s = set() + f = partial(replfunc, seen=s) + template = None + try: + template = self.save_template().rpartition('/')[2] + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + except: + prints(u'Failed to parse template: %r'%template) + template = u'{title} - {authors}' + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + + diff --git a/src/calibre/devices/mtp/books.py b/src/calibre/devices/mtp/books.py new file mode 100644 index 0000000000..c02923702e --- /dev/null +++ b/src/calibre/devices/mtp/books.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os + +from calibre.devices.interface import BookList as BL +from calibre.ebooks.metadata.book.base import Metadata +from calibre.ebooks.metadata.book.json_codec import JsonCodec + +class BookList(BL): + + def __init__(self, storage_id): + self.storage_id = storage_id + + def supports_collections(self): + return False + +class Book(Metadata): + + def __init__(self, storage_id, lpath, other=None): + Metadata.__init__(self, _('Unknown'), other=other) + self.storage_id, self.lpath = storage_id, lpath + self.lpath = self.lpath.replace(os.sep, '/') + self.mtp_relpath = tuple([icu_lower(x) for x in self.lpath.split('/')]) + + def matches_file(self, mtp_file): + return (self.storage_id == mtp_file.storage_id and + self.mtp_relpath == mtp_file.mtp_relpath) + +class JSONCodec(JsonCodec): + pass + diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index c3e34a2be5..f1e9bdbcff 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -7,10 +7,13 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json, pprint +import json, pprint, traceback from io import BytesIO +from calibre import prints from calibre.constants import iswindows, numeric_version +from calibre.devices.mtp.base import debug +from calibre.ptempfile import SpooledTemporaryFile from calibre.utils.config import from_json, to_json from calibre.utils.date import now, isoformat @@ -25,6 +28,7 @@ class MTP_DEVICE(BASE): METADATA_CACHE = 'metadata.calibre' DRIVEINFO = 'driveinfo.calibre' + CAN_SET_METADATA = [] def _update_drive_info(self, storage, location_code, name=None): import uuid @@ -81,6 +85,98 @@ class MTP_DEVICE(BASE): self._update_drive_info(self.filesystem_cache.storage(sid), location_code, name=name) + def books(self, oncard=None, end_session=True): + from calibre.devices.mtp.books import JSONCodec + from calibre.devices.mtp.books import BookList, Book + sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard, + self._main_id) + if sid is None: + return BookList(None) + + bl = BookList(sid) + # If True then there is a mismatch between the ebooks on the device and + # the metadata cache + need_sync = False + all_books = list(self.filesystem_cache.iterebooks(sid)) + steps = len(all_books) + 2 + count = 0 + + self.report_progress(0, _('Reading metadata from device')) + # Read the cache if it exists + storage = self.filesystem_cache.storage(sid) + cache = storage.find_path((self.METADATA_CACHE,)) + if cache is not None: + json_codec = JSONCodec() + try: + stream = self.get_file(cache) + json_codec.decode_from_file(stream, bl, Book, sid) + except: + need_sync = True + + relpath_cache = {b.mtp_relpath:i for i, b in enumerate(bl)} + + for mtp_file in all_books: + count += 1 + relpath = mtp_file.mtp_relpath + idx = relpath_cache.get(relpath, None) + if idx is not None: + cached_metadata = bl[idx] + del relpath_cache[relpath] + if cached_metadata.size == mtp_file.size: + debug('Using cached metadata for', + '/'.join(mtp_file.full_path)) + continue # No need to update metadata + book = cached_metadata + else: + book = Book(sid, '/'.join(relpath)) + bl.append(book) + + need_sync = True + self.report_progress(count/steps, _('Reading metadata from %s')% + ('/'.join(relpath))) + try: + book.smart_update(self.read_file_metadata(mtp_file)) + debug('Read metadata for', '/'.join(mtp_file.full_path)) + except: + prints('Failed to read metadata from', + '/'.join(mtp_file.full_path)) + traceback.print_exc() + book.size = mtp_file.size + + # Remove books in the cache that no longer exist + for idx in sorted(relpath_cache.itervalues(), reverse=True): + del bl[idx] + need_sync = True + + if need_sync: + self.report_progress(count/steps, _('Updating metadata cache on device')) + self.write_metadata_cache(storage, bl) + self.report_progress(1, _('Finished reading metadata from device')) + + def read_file_metadata(self, mtp_file): + from calibre.ebooks.metadata.meta import get_metadata + from calibre.customize.ui import quick_metadata + ext = mtp_file.name.rpartition('.')[-1].lower() + stream = self.get_file(mtp_file) + with quick_metadata: + return get_metadata(stream, stream_type=ext, + force_read_metadata=True, + pattern=self.build_template_regexp()) + + def write_metadata_cache(self, storage, bl): + from calibre.devices.mtp.books import JSONCodec + + if bl.storage_id != storage.storage_id: + # Just a sanity check, should never happen + return + + json_codec = JSONCodec() + stream = SpooledTemporaryFile(10*(1024**2)) + json_codec.encode_to_file(stream, bl) + size = stream.tell() + stream.seek(0) + self.put_file(storage, self.METADATA_CACHE, stream, size) + if __name__ == '__main__': dev = MTP_DEVICE(None) dev.startup() @@ -92,8 +188,9 @@ if __name__ == '__main__': cd = dev.detect_managed_devices(devs) if cd is None: raise ValueError('Failed to detect MTP device') + dev.set_progress_reporter(prints) dev.open(cd, None) - pprint.pprint(dev.get_device_information()) + dev.books() finally: dev.shutdown() diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index cd97c5c2ed..ba2206d191 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -14,6 +14,9 @@ from future_builtins import map from calibre import human_readable, prints, force_unicode from calibre.utils.icu import sort_key, lower +from calibre.ebooks import BOOK_EXTENSIONS + +bexts = frozenset(BOOK_EXTENSIONS) class FileOrFolder(object): @@ -50,6 +53,9 @@ class FileOrFolder(object): if self.storage_id == self.object_id: self.storage_prefix = 'mtp:::%s:::'%self.persistent_id + self.is_ebook = (not self.is_folder and + self.name.rpartition('.')[-1].lower() in bexts) + def __repr__(self): name = 'Folder' if self.is_folder else 'File' try: @@ -147,6 +153,9 @@ class FileOrFolder(object): parent = c return parent + @property + def mtp_relpath(self): + return tuple(x.lower() for x in self.full_path[1:]) class FilesystemCache(object): @@ -192,4 +201,9 @@ class FilesystemCache(object): if e.storage_id == storage_id: return e + def iterebooks(self, storage_id): + for x in self.id_map.itervalues(): + if x.storage_id == storage_id and x.is_ebook: + yield x + diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 5d7d767a9b..9244ac198c 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -17,7 +17,6 @@ from calibre.constants import plugins from calibre.ptempfile import SpooledTemporaryFile from calibre.devices.errors import OpenFailed, DeviceError from calibre.devices.mtp.base import MTPDeviceBase, synchronous -from calibre.devices.mtp.filesystem_cache import FilesystemCache MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' 'bcd serial manufacturer product') @@ -175,6 +174,7 @@ class MTP_DEVICE(MTPDeviceBase): @property def filesystem_cache(self): if self._filesystem_cache is None: + from calibre.devices.mtp.filesystem_cache import FilesystemCache with self.lock: storage, all_items, all_errs = [], [], [] for sid, capacity in zip([self._main_id, self._carda_id, diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 0506f63054..191d69560d 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -17,7 +17,6 @@ from calibre.constants import plugins, __appname__, numeric_version from calibre.ptempfile import SpooledTemporaryFile from calibre.devices.errors import OpenFailed, DeviceError from calibre.devices.mtp.base import MTPDeviceBase -from calibre.devices.mtp.filesystem_cache import FilesystemCache class ThreadingViolation(Exception): @@ -143,6 +142,7 @@ class MTP_DEVICE(MTPDeviceBase): @property def filesystem_cache(self): if self._filesystem_cache is None: + from calibre.devices.mtp.filesystem_cache import FilesystemCache ts = self.total_space() all_storage = [] items = [] From f59ac23c9c3277760be4a9889be59840a4ecaba6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 18:47:02 +0530 Subject: [PATCH 16/77] ... --- src/calibre/devices/mtp/driver.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index f1e9bdbcff..7ba02e2aaa 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -30,6 +30,11 @@ class MTP_DEVICE(BASE): DRIVEINFO = 'driveinfo.calibre' CAN_SET_METADATA = [] + def open(self, devices, library_uuid): + self.current_library_uuid = library_uuid + BASE.open(self, devices, library_uuid) + + # Device information {{{ def _update_drive_info(self, storage, location_code, name=None): import uuid f = storage.find_path((self.DRIVEINFO,)) @@ -55,10 +60,6 @@ class MTP_DEVICE(BASE): self.put_file(storage, self.DRIVEINFO, BytesIO(raw), len(raw)) self.driveinfo = dinfo - def open(self, devices, library_uuid): - self.current_library_uuid = library_uuid - BASE.open(self, devices, library_uuid) - def get_device_information(self, end_session=True): self.report_progress(1.0, _('Get device information...')) self.driveinfo = {} @@ -84,7 +85,9 @@ class MTP_DEVICE(BASE): return self._update_drive_info(self.filesystem_cache.storage(sid), location_code, name=name) + # }}} + # Get list of books from device, with metadata {{{ def books(self, oncard=None, end_session=True): from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import BookList, Book @@ -176,6 +179,7 @@ class MTP_DEVICE(BASE): size = stream.tell() stream.seek(0) self.put_file(storage, self.METADATA_CACHE, stream, size) + # }}} if __name__ == '__main__': dev = MTP_DEVICE(None) From 2292991006ae91ec1678f7e0d9edb07727840bc4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 19:40:09 +0530 Subject: [PATCH 17/77] EPUB metadata: When there are multiple tags use the one with the earliest date as the published date --- src/calibre/ebooks/metadata/opf2.py | 41 +++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index d132fe15d0..7f8a01f8bd 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -510,6 +510,7 @@ class OPF(object): # {{{ tags_path = XPath('descendant::*[re:match(name(), "subject", "i")]') isbn_path = XPath('descendant::*[re:match(name(), "identifier", "i") and '+ '(re:match(@scheme, "isbn", "i") or re:match(@opf:scheme, "isbn", "i"))]') + pubdate_path = XPath('descendant::*[re:match(name(), "date", "i")]') raster_cover_path = XPath('descendant::*[re:match(name(), "meta", "i") and ' + 're:match(@name, "cover", "i") and @content]') identifier_path = XPath('descendant::*[re:match(name(), "identifier", "i")]') @@ -538,8 +539,6 @@ class OPF(object): # {{{ formatter=float, none_is=1) title_sort = TitleSortField('title_sort', is_dc=False) rating = MetadataField('rating', is_dc=False, formatter=float) - pubdate = MetadataField('date', formatter=parse_date, - renderer=isoformat) publication_type = MetadataField('publication_type', is_dc=False) timestamp = MetadataField('timestamp', is_dc=False, formatter=parse_date, renderer=isoformat) @@ -852,6 +851,44 @@ class OPF(object): # {{{ return property(fget=fget, fset=fset) + @dynamic_property + def pubdate(self): + + def fget(self): + ans = None + for match in self.pubdate_path(self.metadata): + try: + val = parse_date(etree.tostring(match, encoding=unicode, + method='text', with_tail=False).strip()) + except: + continue + if ans is None or val < ans: + ans = val + return ans + + def fset(self, val): + least_val = least_elem = None + for match in self.pubdate_path(self.metadata): + try: + cval = parse_date(etree.tostring(match, encoding=unicode, + method='text', with_tail=False).strip()) + except: + match.getparent().remove(match) + else: + if not val: + match.getparent().remove(match) + if least_val is None or cval < least_val: + least_val, least_elem = cval, match + + if val: + if least_val is None: + least_elem = self.create_metadata_element('date') + + least_elem.attrib.clear() + least_elem.text = isoformat(val) + + return property(fget=fget, fset=fset) + @dynamic_property def isbn(self): From 8bec5211c19513b8a55ecaad62a1c0177d36cea7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 21:50:23 +0530 Subject: [PATCH 18/77] Refactor to make build_template_regexp a utility function --- src/calibre/devices/__init__.py | 25 +++++++++++++++++++++- src/calibre/devices/mtp/base.py | 32 ++++++++--------------------- src/calibre/devices/usbms/driver.py | 23 +++------------------ 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index 37ec55c149..ab772c6905 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Device drivers. ''' -import sys, time, pprint, operator +import sys, time, pprint, operator, re from functools import partial from StringIO import StringIO @@ -27,6 +27,29 @@ def strftime(epoch, zone=time.gmtime): src[2] = INVERSE_MONTH_MAP[int(src[2])] return ' '.join(src) +def build_template_regexp(template): + from calibre import prints + + def replfunc(match, seen=None): + v = match.group(1) + if v in ['authors', 'author_sort']: + v = 'author' + if v in ('title', 'series', 'series_index', 'isbn', 'author'): + if v not in seen: + seen.add(v) + return '(?P<' + v + '>.+?)' + return '(.+?)' + s = set() + f = partial(replfunc, seen=s) + + try: + template = template.rpartition('/')[2] + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + except: + prints(u'Failed to parse template: %r'%template) + template = u'{title} - {authors}' + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + def get_connected_device(): from calibre.customize.ui import device_plugins from calibre.devices.scanner import DeviceScanner diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 3b71e40619..346a44e6e8 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -7,8 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re -from functools import wraps, partial +from functools import wraps from calibre import prints from calibre.constants import DEBUG @@ -61,27 +60,12 @@ class MTPDeviceBase(DevicePlugin): return False def build_template_regexp(self): - return None - # TODO: Implement this - def replfunc(match, seen=None): - v = match.group(1) - if v in ['authors', 'author_sort']: - v = 'author' - if v in ('title', 'series', 'series_index', 'isbn', 'author'): - if v not in seen: - seen.add(v) - return '(?P<' + v + '>.+?)' - return '(.+?)' - s = set() - f = partial(replfunc, seen=s) - template = None - try: - template = self.save_template().rpartition('/')[2] - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') - except: - prints(u'Failed to parse template: %r'%template) - template = u'{title} - {authors}' - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') - + from calibre.devices import build_template_regexp + # TODO: Use the device specific template here + return build_template_regexp(self.default_save_template) + @property + def default_save_template(cls): + from calibre.library.save_to_disk import config + return config().parse().send_template diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 12e30073ac..f6c7556fd8 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -10,7 +10,7 @@ driver. It is intended to be subclassed with the relevant parts implemented for a particular device. ''' -import os, re, time, json, functools, shutil +import os, time, json, shutil from itertools import cycle from calibre.constants import numeric_version @@ -404,25 +404,8 @@ class USBMS(CLI, Device): @classmethod def build_template_regexp(cls): - def replfunc(match, seen=None): - v = match.group(1) - if v in ['authors', 'author_sort']: - v = 'author' - if v in ('title', 'series', 'series_index', 'isbn', 'author'): - if v not in seen: - seen.add(v) - return '(?P<' + v + '>.+?)' - return '(.+?)' - s = set() - f = functools.partial(replfunc, seen=s) - template = None - try: - template = cls.save_template().rpartition('/')[2] - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') - except: - prints(u'Failed to parse template: %r'%template) - template = u'{title} - {authors}' - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + from calibre.devices import build_template_regexp + return build_template_regexp(cls.save_template()) @classmethod def path_to_unicode(cls, path): From 0c4227c036b285f9dd71da74047b7154f5968785 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 22:49:20 +0530 Subject: [PATCH 19/77] ... --- src/calibre/utils/magick/magick.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index 8713d2cebb..f01af3a552 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -1238,7 +1238,7 @@ static PyMethodDef magick_Image_methods[] = { }, {"quantize", (PyCFunction)magick_Image_quantize, METH_VARARGS, - "quantize(number_colors, colorspace, treedepth, dither, measure_error) \n\n nalyzes the colors within a reference image and chooses a fixed number of colors to represent the image. The goal of the algorithm is to minimize the color difference between the input and output image while minimizing the processing time." + "quantize(number_colors, colorspace, treedepth, dither, measure_error) \n\n analyzes the colors within a reference image and chooses a fixed number of colors to represent the image. The goal of the algorithm is to minimize the color difference between the input and output image while minimizing the processing time." }, {NULL} /* Sentinel */ From e6ff759719124c6f2ea25d936b7794ddff197a03 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 31 Aug 2012 22:56:43 +0530 Subject: [PATCH 20/77] Make create_upload_path a utility function --- src/calibre/devices/__init__.py | 87 ++++++++++++++++++++++++++++- src/calibre/devices/usbms/device.py | 82 ++++----------------------- 2 files changed, 96 insertions(+), 73 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index ab772c6905..abe7d98b9f 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Device drivers. ''' -import sys, time, pprint, operator, re +import sys, time, pprint, operator, re, os from functools import partial from StringIO import StringIO @@ -50,6 +50,91 @@ def build_template_regexp(template): template = u'{title} - {authors}' return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') +def create_upload_path(mdata, fname, template, sanitize, + prefix_path='', + path_type=os.path, + maxlen=250, + use_subdirs=True, + news_in_folder=True, + filename_callback=lambda x, y:x, + sanitize_path_components=lambda x: x + ): + from calibre.library.save_to_disk import get_components, config + from calibre.utils.filenames import shorten_components_to + + special_tag = None + if mdata.tags: + for t in mdata.tags: + if t.startswith(_('News')) or t.startswith('/'): + special_tag = t + break + + if mdata.tags and _('News') in mdata.tags: + try: + p = mdata.pubdate + date = (p.year, p.month, p.day) + except: + today = time.localtime() + date = (today[0], today[1], today[2]) + template = u"{title}_%d-%d-%d" % date + + fname = sanitize(fname) + ext = path_type.splitext(fname)[1] + + opts = config().parse() + if not isinstance(template, unicode): + template = template.decode('utf-8') + app_id = str(getattr(mdata, 'application_id', '')) + id_ = mdata.get('id', fname) + extra_components = get_components(template, mdata, id_, + timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1) + if not extra_components: + extra_components.append(sanitize(filename_callback(fname, + mdata))) + else: + extra_components[-1] = sanitize(filename_callback(extra_components[-1]+ext, mdata)) + + if extra_components[-1] and extra_components[-1][0] in ('.', '_'): + extra_components[-1] = 'x' + extra_components[-1][1:] + + if special_tag is not None: + name = extra_components[-1] + extra_components = [] + tag = special_tag + if tag.startswith(_('News')): + if news_in_folder: + extra_components.append('News') + else: + for c in tag.split('/'): + c = sanitize(c) + if not c: continue + extra_components.append(c) + extra_components.append(name) + + if not use_subdirs: + extra_components = extra_components[-1:] + + def remove_trailing_periods(x): + ans = x + while ans.endswith('.'): + ans = ans[:-1].strip() + if not ans: + ans = 'x' + return ans + + extra_components = list(map(remove_trailing_periods, extra_components)) + if prefix_path: + prefix_path = path_type.abspath(prefix_path) + components = shorten_components_to(maxlen - len(prefix_path), extra_components) + components = sanitize_path_components(components) + if prefix_path: + filepath = path_type.join(prefix_path, *components) + else: + filepath = path_type.join(*components) + + return filepath + + def get_connected_device(): from calibre.customize.ui import device_plugins from calibre.devices.scanner import DeviceScanner diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 600916128d..4d4b198de0 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -19,7 +19,7 @@ from calibre.devices.errors import (DeviceError, FreeSpaceError, WrongDestinationError) from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.constants import iswindows, islinux, isosx, isfreebsd, plugins -from calibre.utils.filenames import ascii_filename as sanitize, shorten_components_to +from calibre.utils.filenames import ascii_filename as sanitize if isosx: usbobserver, usbobserver_err = plugins['usbobserver'] @@ -1052,78 +1052,16 @@ class Device(DeviceConfig, DevicePlugin): pass def create_upload_path(self, path, mdata, fname, create_dirs=True): - path = os.path.abspath(path) - maxlen = self.MAX_PATH_LEN - - special_tag = None - if mdata.tags: - for t in mdata.tags: - if t.startswith(_('News')) or t.startswith('/'): - special_tag = t - break - + from calibre.devices import create_upload_path settings = self.settings() - template = self.save_template() - if mdata.tags and _('News') in mdata.tags: - try: - p = mdata.pubdate - date = (p.year, p.month, p.day) - except: - today = time.localtime() - date = (today[0], today[1], today[2]) - template = "{title}_%d-%d-%d" % date - use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs - - fname = sanitize(fname) - ext = os.path.splitext(fname)[1] - - from calibre.library.save_to_disk import get_components - from calibre.library.save_to_disk import config - opts = config().parse() - if not isinstance(template, unicode): - template = template.decode('utf-8') - app_id = str(getattr(mdata, 'application_id', '')) - id_ = mdata.get('id', fname) - extra_components = get_components(template, mdata, id_, - timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1) - if not extra_components: - extra_components.append(sanitize(self.filename_callback(fname, - mdata))) - else: - extra_components[-1] = sanitize(self.filename_callback(extra_components[-1]+ext, mdata)) - - if extra_components[-1] and extra_components[-1][0] in ('.', '_'): - extra_components[-1] = 'x' + extra_components[-1][1:] - - if special_tag is not None: - name = extra_components[-1] - extra_components = [] - tag = special_tag - if tag.startswith(_('News')): - if self.NEWS_IN_FOLDER: - extra_components.append('News') - else: - for c in tag.split('/'): - c = sanitize(c) - if not c: continue - extra_components.append(c) - extra_components.append(name) - - if not use_subdirs: - extra_components = extra_components[-1:] - - def remove_trailing_periods(x): - ans = x - while ans.endswith('.'): - ans = ans[:-1].strip() - if not ans: - ans = 'x' - return ans - - extra_components = list(map(remove_trailing_periods, extra_components)) - components = shorten_components_to(maxlen - len(path), extra_components) - components = self.sanitize_path_components(components) - filepath = os.path.join(path, *components) + filepath = create_upload_path(mdata, fname, self.save_template(), sanitize, + prefix_path=os.path.abspath(path), + maxlen=self.MAX_PATH_LEN, + use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs, + news_in_folder = self.NEWS_IN_FOLDER, + filename_callback=self.filename_callback, + sanitize_path_components=self.sanitize_path_components + ) filedir = os.path.dirname(filepath) if create_dirs and not os.path.exists(filedir): From 30b3be2f0b0fe1384a7a84a5e28c64c4841f59a0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 00:37:47 +0530 Subject: [PATCH 21/77] ImageMagick: get and set image color depth --- src/calibre/utils/magick/magick.c | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index f01af3a552..94ac4dae83 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -1104,6 +1104,40 @@ magick_Image_type_setter(magick_Image *self, PyObject *val, void *closure) { // }}} +// Image.depth {{{ +static PyObject * +magick_Image_depth_getter(magick_Image *self, void *closure) { + NULL_CHECK(NULL) + + return Py_BuildValue("n", MagickGetImageDepth(self->wand)); +} + +static int +magick_Image_depth_setter(magick_Image *self, PyObject *val, void *closure) { + size_t depth; + + NULL_CHECK(-1) + + if (val == NULL) { + PyErr_SetString(PyExc_TypeError, "Cannot delete image depth"); + return -1; + } + + if (!PyInt_Check(val)) { + PyErr_SetString(PyExc_TypeError, "Depth must be an integer"); + return -1; + } + + depth = (size_t)PyInt_AsSsize_t(val); + if (!MagickSetImageDepth(self->wand, depth)) { + PyErr_Format(PyExc_ValueError, "Could not set image depth to %lu", depth); + return -1; + } + + return 0; +} + +// }}} // Image.destroy {{{ static PyObject * @@ -1260,6 +1294,12 @@ static PyGetSetDef magick_Image_getsetters[] = { (char *)"the image type: UndefinedType, BilevelType, GrayscaleType, GrayscaleMatteType, PaletteType, PaletteMatteType, TrueColorType, TrueColorMatteType, ColorSeparationType, ColorSeparationMatteType, or OptimizeType.", NULL}, + {(char *)"depth", + (getter)magick_Image_depth_getter, (setter)magick_Image_depth_setter, + (char *)"the image depth.", + NULL}, + + {NULL} /* Sentinel */ }; From b562f0324ce7a8d3e1d2a95b06b30b1cfbf92240 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 00:39:10 +0530 Subject: [PATCH 22/77] ... --- src/calibre/utils/magick/magick.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index 94ac4dae83..6fbee2c77d 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -1138,6 +1138,7 @@ magick_Image_depth_setter(magick_Image *self, PyObject *val, void *closure) { } // }}} + // Image.destroy {{{ static PyObject * From f4ade0dc3bb8a4517b3a18f387251d7cdd3a7888 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 09:31:07 +0530 Subject: [PATCH 23/77] ... --- src/calibre/devices/__init__.py | 2 -- src/calibre/devices/mtp/base.py | 8 ++++++-- src/calibre/devices/mtp/driver.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index abe7d98b9f..e67c17c063 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -123,8 +123,6 @@ def create_upload_path(mdata, fname, template, sanitize, return ans extra_components = list(map(remove_trailing_periods, extra_components)) - if prefix_path: - prefix_path = path_type.abspath(prefix_path) components = shorten_components_to(maxlen - len(prefix_path), extra_components) components = sanitize_path_components(components) if prefix_path: diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 346a44e6e8..865eeefb37 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -61,11 +61,15 @@ class MTPDeviceBase(DevicePlugin): def build_template_regexp(self): from calibre.devices import build_template_regexp - # TODO: Use the device specific template here - return build_template_regexp(self.default_save_template) + return build_template_regexp(self.save_template) @property def default_save_template(cls): from calibre.library.save_to_disk import config return config().parse().send_template + @property + def save_template(self): + # TODO: Use the device specific template here + return self.default_save_template + diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 7ba02e2aaa..2ec9e4e586 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json, pprint, traceback +import json, pprint, traceback, posixpath from io import BytesIO from calibre import prints @@ -29,6 +29,8 @@ class MTP_DEVICE(BASE): METADATA_CACHE = 'metadata.calibre' DRIVEINFO = 'driveinfo.calibre' CAN_SET_METADATA = [] + NEWS_IN_FOLDER = True + MAX_PATH_LEN = 230 def open(self, devices, library_uuid): self.current_library_uuid = library_uuid @@ -181,6 +183,18 @@ class MTP_DEVICE(BASE): self.put_file(storage, self.METADATA_CACHE, stream, size) # }}} + def create_upload_path(self, path, mdata, fname): + from calibre.devices import create_upload_path + from calibre.utils.filenames import ascii_filename as sanitize + filepath = create_upload_path(mdata, fname, self.save_template, sanitize, + prefix_path=path, + path_type=posixpath, + maxlen=self.MAX_PATH_LEN, + use_subdirs = True, + news_in_folder = self.NEWS_IN_FOLDER, + ) + return tuple(x.lower() for x in filepath.split('/')) + if __name__ == '__main__': dev = MTP_DEVICE(None) dev.startup() From c8b8825b1819b7f0ec16cd2e8d67b8f8cabdf562 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 09:48:20 +0530 Subject: [PATCH 24/77] Add an option under Preferences->Look & Feel->Book Details to hide the cover in the book details panel --- src/calibre/gui2/__init__.py | 1 + src/calibre/gui2/book_details.py | 6 ++- src/calibre/gui2/preferences/look_feel.py | 1 + src/calibre/gui2/preferences/look_feel.ui | 55 +++++++++++++---------- 4 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 00f5bef03d..8f275ec065 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -101,6 +101,7 @@ gprefs.defaults['auto_add_auto_convert'] = True gprefs.defaults['ui_style'] = 'calibre' if iswindows or isosx else 'system' gprefs.defaults['tag_browser_old_look'] = False gprefs.defaults['book_list_tooltips'] = True +gprefs.defaults['bd_show_cover'] = True # }}} NONE = QVariant() #: Null value to return from the data function of item models diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index 3e20f8c67c..bf5fbe77bd 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -297,7 +297,8 @@ class CoverView(QWidget): # {{{ self.pixmap = self.default_pixmap self.do_layout() self.update() - if not same_item and not config['disable_animations']: + if (not same_item and not config['disable_animations'] and + self.isVisible()): self.animation.start() def paintEvent(self, event): @@ -512,6 +513,7 @@ class DetailsLayout(QLayout): # {{{ self.do_layout(r) def cover_height(self, r): + if not self._children[0].widget().isVisible(): return 0 mh = min(int(r.height()/2.), int(4/3. * r.width())+1) try: ph = self._children[0].widget().pixmap.height() @@ -522,6 +524,7 @@ class DetailsLayout(QLayout): # {{{ return mh def cover_width(self, r): + if not self._children[0].widget().isVisible(): return 0 mw = 1 + int(3/4. * r.height()) try: pw = self._children[0].widget().pixmap.width() @@ -660,6 +663,7 @@ class BookDetails(QWidget): # {{{ self.update_layout() def update_layout(self): + self.cover_view.setVisible(gprefs['bd_show_cover']) self._layout.do_layout(self.rect()) self.cover_view.update_tooltip(self.current_path) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 8ca6b96379..65e6ab6d9f 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -106,6 +106,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 'calibre')]) r('book_list_tooltips', gprefs) r('tag_browser_old_look', gprefs, restart_required=True) + r('bd_show_cover', gprefs) r('cover_flow_queue_length', config, restart_required=True) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index bb60f3db2a..2a4397b22d 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -212,19 +212,32 @@ Book Details - + + + + Note that <b>comments</b> will always be displayed at the end, regardless of the position you assign here. + + + true + + + + + + + Use &Roman numerals for series + + + true + + + + Select displayed metadata - - - - true - - - @@ -247,6 +260,13 @@ + + + + true + + + @@ -288,23 +308,10 @@ Manage Authors. You can use the values {author} and - - + + - Use &Roman numerals for series - - - true - - - - - - - Note that <b>comments</b> will always be displayed at the end, regardless of the position you assign here. - - - true + Show &cover in the book details panel From bc959778e4bd8c06ae27e2f6cc9b36f22a6e53da Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 11:48:28 +0530 Subject: [PATCH 25/77] ... --- src/calibre/devices/mtp/base.py | 5 ----- src/calibre/devices/mtp/driver.py | 16 +++++++++------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 865eeefb37..516b68ae1e 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -32,11 +32,6 @@ class MTPDeviceBase(DevicePlugin): author = 'Kovid Goyal' version = (1, 0, 0) - THUMBNAIL_HEIGHT = 128 - CAN_SET_METADATA = [] - - BACKLOADING_ERROR_MESSAGE = None - def __init__(self, *args, **kwargs): DevicePlugin.__init__(self, *args, **kwargs) self.progress_reporter = None diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 2ec9e4e586..72e8df9df8 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json, pprint, traceback, posixpath +import json, traceback, posixpath, importlib from io import BytesIO from calibre import prints @@ -17,12 +17,8 @@ from calibre.ptempfile import SpooledTemporaryFile from calibre.utils.config import from_json, to_json from calibre.utils.date import now, isoformat -if iswindows: - from calibre.devices.mtp.windows.driver import MTP_DEVICE as BASE - BASE -else: - from calibre.devices.mtp.unix.driver import MTP_DEVICE as BASE -pprint +BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%( + 'windows' if iswindows else 'unix')).MTP_DEVICE class MTP_DEVICE(BASE): @@ -31,6 +27,11 @@ class MTP_DEVICE(BASE): CAN_SET_METADATA = [] NEWS_IN_FOLDER = True MAX_PATH_LEN = 230 + THUMBNAIL_HEIGHT = 160 + THUMBNAIL_WIDTH = 120 + CAN_SET_METADATA = [] + BACKLOADING_ERROR_MESSAGE = None + MANAGES_DEVICE_PRESENCE = True def open(self, devices, library_uuid): self.current_library_uuid = library_uuid @@ -157,6 +158,7 @@ class MTP_DEVICE(BASE): self.report_progress(count/steps, _('Updating metadata cache on device')) self.write_metadata_cache(storage, bl) self.report_progress(1, _('Finished reading metadata from device')) + return bl def read_file_metadata(self, mtp_file): from calibre.ebooks.metadata.meta import get_metadata From 786729fa6f3dc35b01c792ab271fe3a925bed9fd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 12:02:19 +0530 Subject: [PATCH 26/77] ... --- src/calibre/devices/mtp/books.py | 4 +++- src/calibre/devices/mtp/driver.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/books.py b/src/calibre/devices/mtp/books.py index c02923702e..2179c49a8a 100644 --- a/src/calibre/devices/mtp/books.py +++ b/src/calibre/devices/mtp/books.py @@ -12,6 +12,7 @@ import os from calibre.devices.interface import BookList as BL from calibre.ebooks.metadata.book.base import Metadata from calibre.ebooks.metadata.book.json_codec import JsonCodec +from calibre.utils.date import utcnow class BookList(BL): @@ -26,8 +27,9 @@ class Book(Metadata): def __init__(self, storage_id, lpath, other=None): Metadata.__init__(self, _('Unknown'), other=other) self.storage_id, self.lpath = storage_id, lpath - self.lpath = self.lpath.replace(os.sep, '/') + self.lpath = self.path = self.lpath.replace(os.sep, '/') self.mtp_relpath = tuple([icu_lower(x) for x in self.lpath.split('/')]) + self.datetime = utcnow().timetuple() def matches_file(self, mtp_file): return (self.storage_id == mtp_file.storage_id and diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 72e8df9df8..385914a9c9 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -183,6 +183,16 @@ class MTP_DEVICE(BASE): size = stream.tell() stream.seek(0) self.put_file(storage, self.METADATA_CACHE, stream, size) + + def sync_booklists(self, booklists, end_session=True): + for bl in booklists: + if getattr(bl, 'storage_id', None) is None: + continue + storage = self.filesystem_cache.storage(bl.storage_id) + if storage is None: + continue + self.write_metadata_cache(storage, bl) + # }}} def create_upload_path(self, path, mdata, fname): From 5601852363ede69773d1ea3bbe39c11a9c5c5537 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 12:54:12 +0530 Subject: [PATCH 27/77] Enable detection of MTP devices in the GUI and with ebook-device, with a tweak. Note that MTP support is not yet completed. --- src/calibre/customize/builtins.py | 7 +++- src/calibre/devices/cli.py | 20 +++++----- src/calibre/devices/interface.py | 41 +++++++++++++++++++ src/calibre/gui2/device.py | 65 +++++++++++++++++++++++-------- 4 files changed, 104 insertions(+), 29 deletions(-) diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 469195627c..c7dc6a5b95 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -675,7 +675,6 @@ from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX from calibre.devices.smart_device_app.driver import SMART_DEVICE_APP - # Order here matters. The first matched device is the one used. plugins += [ HANLINV3, @@ -749,6 +748,12 @@ plugins += [ SMART_DEVICE_APP, USER_DEFINED, ] + +from calibre.utils.config_base import tweaks +if tweaks.get('test_mtp_driver', False): + from calibre.devices.mtp.driver import MTP_DEVICE + plugins.append(MTP_DEVICE) + # }}} # New metadata download plugins {{{ diff --git a/src/calibre/devices/cli.py b/src/calibre/devices/cli.py index 95181bf639..c7b105998d 100755 --- a/src/calibre/devices/cli.py +++ b/src/calibre/devices/cli.py @@ -9,7 +9,7 @@ For usage information run the script. import StringIO, sys, time, os from optparse import OptionParser -from calibre import __version__, __appname__ +from calibre import __version__, __appname__, human_readable from calibre.devices.errors import PathError from calibre.utils.terminfo import TerminalController from calibre.devices.errors import ArgumentError, DeviceError, DeviceLocked @@ -18,16 +18,6 @@ from calibre.devices.scanner import DeviceScanner MINIMUM_COL_WIDTH = 12 #: Minimum width of columns in ls output -def human_readable(size): - """ Convert a size in bytes into a human readle form """ - if size < 1024: divisor, suffix = 1, "" - elif size < 1024*1024: divisor, suffix = 1024., "K" - elif size < 1024*1024*1024: divisor, suffix = 1024*1024, "M" - elif size < 1024*1024*1024*1024: divisor, suffix = 1024*1024, "G" - size = str(size/divisor) - if size.find(".") > -1: size = size[:size.find(".")+2] - return size + suffix - class FileFormatter(object): def __init__(self, file, term): self.term = term @@ -207,11 +197,19 @@ def main(): scanner = DeviceScanner() scanner.scan() connected_devices = [] + for d in device_plugins(): try: d.startup() except: print ('Startup failed for device plugin: %s'%d) + if d.MANAGES_DEVICE_PRESENCE: + cd = d.detect_managed_devices(scanner.devices) + if cd is not None: + connected_devices.append((cd, d)) + dev = d + break + continue ok, det = scanner.is_device_connected(d) if ok: dev = d diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 2c5f60ecfe..6d859c8f89 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -81,6 +81,19 @@ class DevicePlugin(Plugin): #: by. NUKE_COMMENTS = None + #: If True indicates that this driver completely manages device detection, + #: ejecting and so forth. If you set this to True, you *must* implement the + #: detect_managed_devices and debug_managed_device_detection methods. + #: A driver with this set to true is responsible for detection of devices, + #: managing a blacklist of devices, a list of ejected devices and so forth. + #: calibre will periodically call the detect_managed_devices() method and + #: is it returns a detected device, calibre will call open(). open() will + #: be called every time a device is returned even is previous calls to open() + #: failed, therefore the driver must maintain its own blacklist of failed + #: devices. Similarly, when ejecting, calibre will call eject() and then + #: assuming the next call to detect_managed_devices() returns None, it will + #: call post_yank_cleanup(). + MANAGES_DEVICE_PRESENCE = False @classmethod def get_gui_name(cls): @@ -196,6 +209,34 @@ class DevicePlugin(Plugin): return True, dev return False, None + def detect_managed_devices(self, devices_on_system, force_refresh=False): + ''' + Called only if MANAGES_DEVICE_PRESENCE is True. + + Scan for devices that this driver can handle. Should return a device + object if a device is found. This object will be passed to the open() + method as the connected_device. If no device is found, return None. + + This method is called periodically by the GUI, so make sure it is not + too resource intensive. Use a cache to avoid repeatedly scanning the + system. + + :param devices_on_system: Set of USB devices found on the system. + + :param force_refresh: If True and the driver uses a cache to prevent + repeated scanning, the cache must be flushed. + ''' + raise NotImplementedError() + + def debug_managed_device_detection(self, devices_on_system, output): + ''' + Called only if MANAGES_DEVICE_PRESENCE is True. + + Should write information about the devices detected on the system to + output, which is a file like object. + ''' + raise NotImplementedError() + # }}} def reset(self, key='-1', log_packets=False, report_progress=None, diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 03e1932035..d5879042b4 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -128,6 +128,10 @@ class DeviceManager(Thread): # {{{ self.setDaemon(True) # [Device driver, Showing in GUI, Ejected] self.devices = list(device_plugins()) + self.managed_devices = [x for x in self.devices if + not x.MANAGES_DEVICE_PRESENCE] + self.unmanaged_devices = [x for x in self.devices if + x.MANAGES_DEVICE_PRESENCE] self.sleep_time = sleep_time self.connected_slot = connected_slot self.jobs = Queue.Queue(0) @@ -182,12 +186,15 @@ class DeviceManager(Thread): # {{{ prints('Unable to open device', str(dev)) prints(tb) continue - self.connected_device = dev - self.connected_device_kind = device_kind - self.connected_slot(True, device_kind) + self.after_device_connect(dev, device_kind) return True return False + def after_device_connect(self, dev, device_kind): + self.connected_device = dev + self.connected_device_kind = device_kind + self.connected_slot(True, device_kind) + def connected_device_removed(self): while True: try: @@ -215,22 +222,45 @@ class DeviceManager(Thread): # {{{ def detect_device(self): self.scanner.scan() + if self.is_device_connected: - connected, detected_device = \ - self.scanner.is_device_connected(self.connected_device, - only_presence=True) - if not connected: - if DEBUG: - # Allow the device subsystem to output debugging info about - # why it thinks the device is not connected. Used, for e.g. - # in the can_handle() method of the T1 driver + if self.connected_device.MANAGES_DEVICE_PRESENCE: + cd = self.connected_device.detect_managed_devices(self.scanner.devices) + if cd is None: + self.connected_device_removed() + else: + connected, detected_device = \ self.scanner.is_device_connected(self.connected_device, - only_presence=True, debug=True) - self.connected_device_removed() + only_presence=True) + if not connected: + if DEBUG: + # Allow the device subsystem to output debugging info about + # why it thinks the device is not connected. Used, for e.g. + # in the can_handle() method of the T1 driver + self.scanner.is_device_connected(self.connected_device, + only_presence=True, debug=True) + self.connected_device_removed() else: + for dev in self.unmanaged_devices: + try: + cd = dev.detect_managed_devices(self.scanner.devices) + except: + prints('Error during device detection for %s:'%dev) + traceback.print_exc() + else: + if cd is not None: + try: + dev.open(cd, self.current_library_uuid) + except: + prints('Error while trying to open %s (Driver: %s)'% + (cd, dev)) + traceback.print_exc() + else: + self.after_device_connect(dev, 'unmanaged-device') + return try: possibly_connected_devices = [] - for device in self.devices: + for device in self.managed_devices: if device in self.ejected_devices: continue try: @@ -248,7 +278,7 @@ class DeviceManager(Thread): # {{{ prints('Connect to device failed, retrying in 5 seconds...') time.sleep(5) if not self.do_connect(possibly_connected_devices, - device_kind='usb'): + device_kind='device'): if DEBUG: prints('Device connect failed again, giving up') except OpenFailed as e: @@ -264,9 +294,10 @@ class DeviceManager(Thread): # {{{ # disconnect a device def umount_device(self, *args): if self.is_device_connected and not self.job_manager.has_device_jobs(): - if self.connected_device_kind == 'device': + if self.connected_device_kind in {'unmanaged-device', 'device'}: self.connected_device.eject() - self.ejected_devices.add(self.connected_device) + if self.connected_device_kind != 'unmanaged-device': + self.ejected_devices.add(self.connected_device) self.connected_slot(False, self.connected_device_kind) elif hasattr(self.connected_device, 'unmount_device'): # As we are on the wrong thread, this call must *not* do From 63e6014edcdcc3509508466e4db801c99096e785 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 13:11:24 +0530 Subject: [PATCH 28/77] ... --- src/calibre/devices/interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 6d859c8f89..7512446905 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -234,6 +234,9 @@ class DevicePlugin(Plugin): Should write information about the devices detected on the system to output, which is a file like object. + + Should return True if a device was detected and successfully opened, + otherwise False. ''' raise NotImplementedError() From 023b94760868afa0023f072d299dd22a78ebebaf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 13:41:37 +0530 Subject: [PATCH 29/77] Debug device detection for MTP devices --- src/calibre/devices/__init__.py | 103 ++++++++++++---------- src/calibre/devices/mtp/unix/driver.py | 2 + src/calibre/devices/mtp/windows/driver.py | 52 ++++++++++- 3 files changed, 109 insertions(+), 48 deletions(-) diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index e67c17c063..89d0e4e026 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -221,54 +221,65 @@ def debug(ioreg_to_tmp=False, buf=None, plugins=None): out('Available plugins:', textwrap.fill(' '.join([x.__class__.__name__ for x in devplugins]))) out(' ') - out('Looking for devices...') + found_dev = False for dev in devplugins: - connected, det = s.is_device_connected(dev, debug=True) - if connected: - out('\t\tDetected possible device', dev.__class__.__name__) - connected_devices.append((dev, det)) - - out(' ') - errors = {} - success = False - out('Devices possibly connected:', end=' ') - for dev, det in connected_devices: - out(dev.name, end=', ') - if not connected_devices: - out('None', end='') - out(' ') - for dev, det in connected_devices: - out('Trying to open', dev.name, '...', end=' ') - try: - dev.reset(detected_device=det) - dev.open(det, None) - out('OK') - except: - import traceback - errors[dev] = traceback.format_exc() - out('failed') - continue - success = True - if hasattr(dev, '_main_prefix'): - out('Main memory:', repr(dev._main_prefix)) - out('Total space:', dev.total_space()) - break - if not success and errors: - out('Opening of the following devices failed') - for dev,msg in errors.items(): - out(dev) - out(msg) - out(' ') - - if ioreg is not None: - ioreg = 'IOREG Output\n'+ioreg + if not dev.MANAGES_DEVICE_PRESENCE: continue + out('Looking for devices of type:', dev.__class__.__name__) + if dev.debug_managed_device_detection(s.devices, buf): + found_dev = True + break out(' ') - if ioreg_to_tmp: - open('/tmp/ioreg.txt', 'wb').write(ioreg) - out('Dont forget to send the contents of /tmp/ioreg.txt') - out('You can open it with the command: open /tmp/ioreg.txt') - else: - out(ioreg) + + if not found_dev: + out('Looking for devices...') + for dev in devplugins: + if dev.MANAGES_DEVICE_PRESENCE: continue + connected, det = s.is_device_connected(dev, debug=True) + if connected: + out('\t\tDetected possible device', dev.__class__.__name__) + connected_devices.append((dev, det)) + + out(' ') + errors = {} + success = False + out('Devices possibly connected:', end=' ') + for dev, det in connected_devices: + out(dev.name, end=', ') + if not connected_devices: + out('None', end='') + out(' ') + for dev, det in connected_devices: + out('Trying to open', dev.name, '...', end=' ') + try: + dev.reset(detected_device=det) + dev.open(det, None) + out('OK') + except: + import traceback + errors[dev] = traceback.format_exc() + out('failed') + continue + success = True + if hasattr(dev, '_main_prefix'): + out('Main memory:', repr(dev._main_prefix)) + out('Total space:', dev.total_space()) + break + if not success and errors: + out('Opening of the following devices failed') + for dev,msg in errors.items(): + out(dev) + out(msg) + out(' ') + + if ioreg is not None: + ioreg = 'IOREG Output\n'+ioreg + out(' ') + if ioreg_to_tmp: + open('/tmp/ioreg.txt', 'wb').write(ioreg) + out('Dont forget to send the contents of /tmp/ioreg.txt') + out('You can open it with the command: open /tmp/ioreg.txt') + else: + out(ioreg) if hasattr(buf, 'getvalue'): return buf.getvalue().decode('utf-8') diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 9244ac198c..b4e8b44407 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -82,6 +82,8 @@ class MTP_DEVICE(MTPDeviceBase): @synchronous def debug_managed_device_detection(self, devices_on_system, output): + if self.currently_connected_dev is not None: + return True p = partial(prints, file=output) if self.libmtp is None: err = plugins['libmtp'][1] diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 191d69560d..abe38e5f7c 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -7,8 +7,8 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import time, threading -from functools import wraps +import time, threading, traceback +from functools import wraps, partial from future_builtins import zip from itertools import chain @@ -123,6 +123,54 @@ class MTP_DEVICE(MTPDeviceBase): return None + @same_thread + def debug_managed_device_detection(self, devices_on_system, output): + import pprint + p = partial(prints, file=output) + if self.currently_connected_pnp_id is not None: + return True + if self.wpd_error: + p('Cannot detect MTP devices') + p(self.wpd_error) + return False + try: + pnp_ids = frozenset(self.wpd.enumerate_devices()) + except: + p("Failed to get list of PNP ids on system") + p(traceback.format_exc()) + return False + + for pnp_id in pnp_ids: + try: + data = self.wpd.device_info(pnp_id) + except: + p('Failed to get data for device:', pnp_id) + p(traceback.format_exc()) + continue + protocol = data.get('protocol', '').lower() + if not protocol.startswith('mtp:'): continue + p('MTP device:', pnp_id) + p(pprint.pformat(data)) + if not self.is_suitable_wpd_device(data): + p('Not a suitable MTP device, ignoring\n') + continue + p('\nTrying to open:', pnp_id) + try: + self.open(pnp_id, 'debug-detection') + except: + p('Open failed:') + p(traceback.format_exc()) + continue + break + if self.currently_connected_pnp_id: + p('Opened', self.current_friendly_name, 'successfully') + p('Device info:') + p(pprint.pformat(self.dev.data)) + self.eject() + return True + p('No suitable MTP devices found') + return False + def is_suitable_wpd_device(self, devdata): # Check that protocol is MTP protocol = devdata.get('protocol', '').lower() From 1c3e62f35bf9527df9f0a218bd85272644fa4fdb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 14:51:52 +0530 Subject: [PATCH 30/77] MTP: Try to get modified date from the device --- src/calibre/devices/mtp/filesystem_cache.py | 10 +++++++++- src/calibre/devices/mtp/unix/libmtp.c | 3 ++- .../devices/mtp/windows/content_enumeration.cpp | 13 +++++++++++++ src/calibre/devices/mtp/windows/driver.py | 4 +++- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index ba2206d191..e4ef8ae898 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -11,8 +11,10 @@ import weakref, sys from collections import deque from operator import attrgetter from future_builtins import map +from datetime import datetime from calibre import human_readable, prints, force_unicode +from calibre.utils.date import local_tz from calibre.utils.icu import sort_key, lower from calibre.ebooks import BOOK_EXTENSIONS @@ -21,6 +23,8 @@ bexts = frozenset(BOOK_EXTENSIONS) class FileOrFolder(object): def __init__(self, entry, fs_cache): + self.all_storage_ids = fs_cache.all_storage_ids + self.object_id = entry['id'] self.is_folder = entry['is_folder'] self.storage_id = entry['storage_id'] @@ -31,7 +35,11 @@ class FileOrFolder(object): self.name = force_unicode(n, 'utf-8') self.persistent_id = entry.get('persistent_id', self.object_id) self.size = entry.get('size', 0) - self.all_storage_ids = fs_cache.all_storage_ids + md = entry.get('modified', 0) + try: + self.last_modified = datetime.fromtimestamp(md, local_tz) + except: + self.last_modified = datetime.fromtimestamp(0, local_tz) if self.storage_id not in self.all_storage_ids: raise ValueError('Storage id %s not valid for %s, valid values: %s'%(self.storage_id, diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index 86c9349d20..bf07c73a35 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -121,12 +121,13 @@ static uint16_t data_from_python(void *params, void *priv, uint32_t wantlen, uns static PyObject* build_file_metadata(LIBMTP_file_t *nf, uint32_t storage_id) { PyObject *ans = NULL; - ans = Py_BuildValue("{s:s, s:k, s:k, s:k, s:K, s:O}", + ans = Py_BuildValue("{s:s, s:k, s:k, s:k, s:K, s:L, s:O}", "name", (unsigned long)nf->filename, "id", (unsigned long)nf->item_id, "parent_id", (unsigned long)nf->parent_id, "storage_id", (unsigned long)storage_id, "size", nf->filesize, + "modified", (PY_LONG_LONG)nf->modificationdate, "is_folder", (nf->filetype == LIBMTP_FILETYPE_FOLDER) ? Py_True : Py_False ); diff --git a/src/calibre/devices/mtp/windows/content_enumeration.cpp b/src/calibre/devices/mtp/windows/content_enumeration.cpp index e1f439926c..7186bbdcdb 100644 --- a/src/calibre/devices/mtp/windows/content_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/content_enumeration.cpp @@ -34,6 +34,7 @@ static IPortableDeviceKeyCollection* create_filesystem_properties_collection() { ADDPROP(WPD_OBJECT_ISHIDDEN); ADDPROP(WPD_OBJECT_CAN_DELETE); ADDPROP(WPD_OBJECT_SIZE); + ADDPROP(WPD_OBJECT_DATE_MODIFIED); return properties; @@ -81,6 +82,16 @@ static void set_size_property(PyObject *dict, REFPROPERTYKEY key, const char *py } } +static void set_date_property(PyObject *dict, REFPROPERTYKEY key, const char *pykey, IPortableDeviceValues *properties) { + FLOAT val = 0; + PyObject *t; + + if (SUCCEEDED(properties->GetFloatValue(key, &val))) { + t = Py_BuildValue("d", (double)val); + if (t != NULL) { PyDict_SetItemString(dict, pykey, t); Py_DECREF(t); } + } +} + static void set_content_type_property(PyObject *dict, IPortableDeviceValues *properties) { GUID guid = GUID_NULL; BOOL is_folder = 0; @@ -103,6 +114,8 @@ static void set_properties(PyObject *obj, IPortableDeviceValues *values) { set_bool_property(obj, WPD_OBJECT_ISSYSTEM, "is_system", values); set_size_property(obj, WPD_OBJECT_SIZE, "size", values); + set_date_property(obj, WPD_OBJECT_DATE_MODIFIED, "modified", values); + } // }}} diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index abe38e5f7c..dbcad94fe0 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -248,7 +248,9 @@ class MTP_DEVICE(MTPDeviceBase): self._carda_id = storage[1]['id'] if len(storage) > 2: self._cardb_id = storage[2]['id'] - self.current_friendly_name = devdata.get('friendly_name', None) + self.current_friendly_name = devdata.get('friendly_name', + _('Unknown MTP device')) + self.currently_connected_pnp_id = connected_device @same_thread def get_basic_device_information(self): From 49d1fbad1805ed4c07e7faba6a567b0b457dc93d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 14:55:03 +0530 Subject: [PATCH 31/77] ... --- src/calibre/devices/mtp/windows/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index dbcad94fe0..aaadffbc68 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -21,7 +21,7 @@ from calibre.devices.mtp.base import MTPDeviceBase class ThreadingViolation(Exception): def __init__(self): - Exception.__init__('You cannot use the MTP driver from a thread other than the ' + Exception.__init__(self, 'You cannot use the MTP driver from a thread other than the ' ' thread in which startup() was called') def same_thread(func): From 9a110fb0d50a614b23738cec14a53365c443f35f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 14:55:26 +0530 Subject: [PATCH 32/77] ... --- src/calibre/devices/mtp/windows/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index aaadffbc68..92e9790734 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -21,7 +21,8 @@ from calibre.devices.mtp.base import MTPDeviceBase class ThreadingViolation(Exception): def __init__(self): - Exception.__init__(self, 'You cannot use the MTP driver from a thread other than the ' + Exception.__init__(self, + 'You cannot use the MTP driver from a thread other than the ' ' thread in which startup() was called') def same_thread(func): From d278180bde633cc280bfe8f7805ad15b3d88a0dd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 15:06:53 +0530 Subject: [PATCH 33/77] Windows: workaround for eject() not being called on the device thread --- src/calibre/devices/interface.py | 3 +++ src/calibre/devices/mtp/windows/driver.py | 24 ++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 7512446905..4777cafbe9 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -314,6 +314,9 @@ class DevicePlugin(Plugin): ''' Un-mount / eject the device from the OS. This does not check if there are pending GUI jobs that need to communicate with the device. + + NOTE: That this method may not be called on the same thread as the rest + of the device methods. ''' raise NotImplementedError() diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 92e9790734..faa5296547 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -51,6 +51,7 @@ class MTP_DEVICE(MTPDeviceBase): self._main_id = self._carda_id = self._cardb_id = None self.start_thread = None self._filesystem_cache = None + self.eject_dev_on_next_scan = False def startup(self): self.start_thread = threading.current_thread() @@ -75,6 +76,10 @@ class MTP_DEVICE(MTPDeviceBase): @same_thread def detect_managed_devices(self, devices_on_system, force_refresh=False): if self.wpd is None: return None + if self.eject_dev_on_next_scan: + self.eject_dev_on_next_scan = False + if self.currently_connected_pnp_id is not None: + self.do_eject() devices_on_system = frozenset(devices_on_system) if (force_refresh or @@ -213,19 +218,24 @@ class MTP_DEVICE(MTPDeviceBase): return self._filesystem_cache @same_thread - def post_yank_cleanup(self): - self.currently_connected_pnp_id = self.current_friendly_name = None - self._main_id = self._carda_id = self._cardb_id = None - self.dev = self._filesystem_cache = None - - @same_thread - def eject(self): + def do_eject(self): if self.currently_connected_pnp_id is None: return self.ejected_devices.add(self.currently_connected_pnp_id) self.currently_connected_pnp_id = self.current_friendly_name = None self._main_id = self._carda_id = self._cardb_id = None self.dev = self._filesystem_cache = None + + @same_thread + def post_yank_cleanup(self): + self.currently_connected_pnp_id = self.current_friendly_name = None + self._main_id = self._carda_id = self._cardb_id = None + self.dev = self._filesystem_cache = None + + def eject(self): + if self.currently_connected_pnp_id is None: return + self.eject_dev_on_next_scan = True + @same_thread def open(self, connected_device, library_uuid): self.dev = self._filesystem_cache = None From 7b2a947a86b66612a9244d420d579dbfb76db5f3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 15:22:49 +0530 Subject: [PATCH 34/77] ... --- src/calibre/devices/mtp/driver.py | 2 ++ src/calibre/devices/mtp/filesystem_cache.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 385914a9c9..4b26e23ef8 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -129,6 +129,7 @@ class MTP_DEVICE(BASE): cached_metadata = bl[idx] del relpath_cache[relpath] if cached_metadata.size == mtp_file.size: + cached_metadata.datetime = mtp_file.last_modified.timetuple() debug('Using cached metadata for', '/'.join(mtp_file.full_path)) continue # No need to update metadata @@ -148,6 +149,7 @@ class MTP_DEVICE(BASE): '/'.join(mtp_file.full_path)) traceback.print_exc() book.size = mtp_file.size + book.datetime = mtp_file.last_modified.timetuple() # Remove books in the cache that no longer exist for idx in sorted(relpath_cache.itervalues(), reverse=True): diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index e4ef8ae898..6aab711199 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -14,7 +14,7 @@ from future_builtins import map from datetime import datetime from calibre import human_readable, prints, force_unicode -from calibre.utils.date import local_tz +from calibre.utils.date import local_tz, as_utc from calibre.utils.icu import sort_key, lower from calibre.ebooks import BOOK_EXTENSIONS @@ -40,6 +40,7 @@ class FileOrFolder(object): self.last_modified = datetime.fromtimestamp(md, local_tz) except: self.last_modified = datetime.fromtimestamp(0, local_tz) + self.last_modified = as_utc(self.last_modified) if self.storage_id not in self.all_storage_ids: raise ValueError('Storage id %s not valid for %s, valid values: %s'%(self.storage_id, From 4785359e6ab1993fbc7e7f134e4882b04d82c7ab Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 15:33:33 +0530 Subject: [PATCH 35/77] ... --- src/calibre/devices/mtp/books.py | 1 + src/calibre/devices/mtp/driver.py | 6 +++--- src/calibre/devices/mtp/test.py | 6 +++--- src/calibre/devices/mtp/unix/driver.py | 2 +- src/calibre/devices/mtp/windows/driver.py | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/calibre/devices/mtp/books.py b/src/calibre/devices/mtp/books.py index 2179c49a8a..73e483f19e 100644 --- a/src/calibre/devices/mtp/books.py +++ b/src/calibre/devices/mtp/books.py @@ -30,6 +30,7 @@ class Book(Metadata): self.lpath = self.path = self.lpath.replace(os.sep, '/') self.mtp_relpath = tuple([icu_lower(x) for x in self.lpath.split('/')]) self.datetime = utcnow().timetuple() + self.thumbail = None def matches_file(self, mtp_file): return (self.storage_id == mtp_file.storage_id and diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 4b26e23ef8..13e8394288 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -43,7 +43,7 @@ class MTP_DEVICE(BASE): f = storage.find_path((self.DRIVEINFO,)) dinfo = {} if f is not None: - stream = self.get_file(f) + stream = self.get_mtp_file(f) try: dinfo = json.load(stream, object_hook=from_json) except: @@ -114,7 +114,7 @@ class MTP_DEVICE(BASE): if cache is not None: json_codec = JSONCodec() try: - stream = self.get_file(cache) + stream = self.get_mtp_file(cache) json_codec.decode_from_file(stream, bl, Book, sid) except: need_sync = True @@ -166,7 +166,7 @@ class MTP_DEVICE(BASE): from calibre.ebooks.metadata.meta import get_metadata from calibre.customize.ui import quick_metadata ext = mtp_file.name.rpartition('.')[-1].lower() - stream = self.get_file(mtp_file) + stream = self.get_mtp_file(mtp_file) with quick_metadata: return get_metadata(stream, stream_type=ext, force_read_metadata=True, diff --git a/src/calibre/devices/mtp/test.py b/src/calibre/devices/mtp/test.py index 0563708ea4..c273bac5e0 100644 --- a/src/calibre/devices/mtp/test.py +++ b/src/calibre/devices/mtp/test.py @@ -128,7 +128,7 @@ class TestDeviceInteraction(unittest.TestCase): raw2 = io.BytesIO() pc = ProgressCallback() - self.dev.get_file(f, raw2, callback=pc) + self.dev.get_mtp_file(f, raw2, callback=pc) self.assertEqual(raw.getvalue(), raw2.getvalue()) self.assertTrue(pc.end_called, msg='Progress callback not called with equal values (get_file)') @@ -162,7 +162,7 @@ class TestDeviceInteraction(unittest.TestCase): self.assertEqual(f.storage_id, self.storage.storage_id) raw2 = io.BytesIO() - self.dev.get_file(f, raw2) + self.dev.get_mtp_file(f, raw2) self.assertEqual(raw.getvalue(), raw2.getvalue()) def measure_memory_usage(self, repetitions, func, *args, **kwargs): @@ -226,7 +226,7 @@ class TestDeviceInteraction(unittest.TestCase): def get_file(f): raw = io.BytesIO() pc = ProgressCallback() - self.dev.get_file(f, raw, callback=pc) + self.dev.get_mtp_file(f, raw, callback=pc) raw.truncate(0) del raw del pc diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index b4e8b44407..338913114f 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -273,7 +273,7 @@ class MTP_DEVICE(MTPDeviceBase): return parent.add_child(ans) @synchronous - def get_file(self, f, stream=None, callback=None): + def get_mtp_file(self, f, stream=None, callback=None): if f.is_folder: raise ValueError('%s if a folder'%(f.full_path,)) if stream is None: diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index faa5296547..7c15797ef6 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -293,7 +293,7 @@ class MTP_DEVICE(MTPDeviceBase): return tuple(ans) @same_thread - def get_file(self, f, stream=None, callback=None): + def get_mtp_file(self, f, stream=None, callback=None): if f.is_folder: raise ValueError('%s if a folder'%(f.full_path,)) if stream is None: From b9737b96593e8bef40660cf33889825f24cb63fd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 21:57:09 +0530 Subject: [PATCH 36/77] MTP: Implement get_file() --- src/calibre/devices/mtp/driver.py | 6 ++++++ src/calibre/devices/mtp/filesystem_cache.py | 21 ++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 13e8394288..9ac0c3d31a 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -130,6 +130,7 @@ class MTP_DEVICE(BASE): del relpath_cache[relpath] if cached_metadata.size == mtp_file.size: cached_metadata.datetime = mtp_file.last_modified.timetuple() + cached_metadata.path = mtp_file.mtp_id_path debug('Using cached metadata for', '/'.join(mtp_file.full_path)) continue # No need to update metadata @@ -150,6 +151,7 @@ class MTP_DEVICE(BASE): traceback.print_exc() book.size = mtp_file.size book.datetime = mtp_file.last_modified.timetuple() + book.path = mtp_file.mtp_id_path # Remove books in the cache that no longer exist for idx in sorted(relpath_cache.itervalues(), reverse=True): @@ -197,6 +199,10 @@ class MTP_DEVICE(BASE): # }}} + def get_file(self, path, outfile, end_session=True): + f = self.filesystem_cache.resolve_mtp_id_path(path) + self.get_mtp_file(f, outfile) + def create_upload_path(self, path, mdata, fname): from calibre.devices import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 6aab711199..216e06031f 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import weakref, sys +import weakref, sys, json from collections import deque from operator import attrgetter from future_builtins import map @@ -166,6 +166,10 @@ class FileOrFolder(object): def mtp_relpath(self): return tuple(x.lower() for x in self.full_path[1:]) + @property + def mtp_id_path(self): + return 'mtp:::' + json.dumps(self.object_id) + ':::' + '/'.join(self.full_path) + class FilesystemCache(object): def __init__(self, all_storage, entries): @@ -215,4 +219,19 @@ class FilesystemCache(object): if x.storage_id == storage_id and x.is_ebook: yield x + def resolve_mtp_id_path(self, path): + if not path.startswith('mtp:::'): + raise ValueError('%s is not a valid MTP path'%path) + parts = path.split(':::') + if len(parts) < 3: + raise ValueError('%s is not a valid MTP path'%path) + try: + object_id = json.loads(parts[1]) + except: + raise ValueError('%s is not a valid MTP path'%path) + try: + return self.id_map[object_id] + except KeyError: + raise ValueError('No object found with MTP path: %s'%path) + From 2e66bd1aa58f141e973b7cc350db587ee394d4d2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 23:00:40 +0530 Subject: [PATCH 37/77] MTP: Implement prepare_addable_books and refactor the GUI to run prepare_addable_books in the device thread --- src/calibre/devices/mtp/driver.py | 26 +++++++++++++-- src/calibre/gui2/actions/add.py | 53 +++++++++++++++++++++++++------ src/calibre/gui2/device.py | 8 +++++ 3 files changed, 75 insertions(+), 12 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 9ac0c3d31a..8f8f4d119b 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -7,13 +7,13 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import json, traceback, posixpath, importlib +import json, traceback, posixpath, importlib, os from io import BytesIO from calibre import prints from calibre.constants import iswindows, numeric_version from calibre.devices.mtp.base import debug -from calibre.ptempfile import SpooledTemporaryFile +from calibre.ptempfile import SpooledTemporaryFile, PersistentTemporaryDirectory from calibre.utils.config import from_json, to_json from calibre.utils.date import now, isoformat @@ -199,10 +199,32 @@ class MTP_DEVICE(BASE): # }}} + # Get files from the device {{{ def get_file(self, path, outfile, end_session=True): f = self.filesystem_cache.resolve_mtp_id_path(path) self.get_mtp_file(f, outfile) + def prepare_addable_books(self, paths): + tdir = PersistentTemporaryDirectory('_prepare_mtp') + ans = [] + for path in paths: + try: + f = self.filesystem_cache.resolve_mtp_id_path(path) + except Exception as e: + ans.append((path, e, traceback.format_exc())) + continue + base = os.path.join(tdir, '%s'%f.object_id) + os.mkdir(base) + with open(os.path.join(base, f.name), 'wb') as out: + try: + self.get_mtp_file(f, out) + except Exception as e: + ans.append((path, e, traceback.format_exc())) + else: + ans.append(out.name) + return ans + # }}} + def create_upload_path(self, path, mdata, fname): from calibre.devices import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 9d15fa4ac8..ef7ed7a594 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -10,9 +10,9 @@ from functools import partial from PyQt4.Qt import QPixmap, QTimer - -from calibre.gui2 import error_dialog, choose_files, \ - choose_dir, warning_dialog, info_dialog +from calibre import as_unicode +from calibre.gui2 import (error_dialog, choose_files, choose_dir, + warning_dialog, info_dialog) from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.widgets import IMAGE_EXTENSIONS @@ -400,12 +400,45 @@ class AddAction(InterfaceAction): d = error_dialog(self.gui, _('Add to library'), _('No book files found')) d.exec_() return - paths = self.gui.device_manager.device.prepare_addable_books(paths) - from calibre.gui2.add import Adder - self.__adder_func = partial(self._add_from_device_adder, on_card=None, - model=view.model()) - self._adder = Adder(self.gui, self.gui.library_view.model().db, - self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server) - self._adder.add(paths) + + self.gui.device_manager.prepare_addable_books(self.Dispatcher(partial( + self.books_prepared, view)), paths) + self.bpd = ProgressDialog(_('Downloading books'), + msg=_('Downloading books from device'), parent=self.gui, + cancelable=False) + QTimer.singleShot(1000, self.show_bpd) + + def show_bpd(self): + if self.bpd is not None: + self.bpd.show() + + def books_prepared(self, view, job): + self.bpd.hide() + self.bpd = None + if job.exception is not None: + self.gui.device_job_exception(job) + return + paths = job.result + ok_paths = [x for x in paths if isinstance(x, basestring)] + failed_paths = [x for x in paths if isinstance(x, tuple)] + if failed_paths: + if not ok_paths: + msg = _('Could not download files from the device') + typ = error_dialog + else: + msg = _('Could not download some files from the device') + typ = warning_dialog + det_msg = [x[0]+ '\n ' + as_unicode(x[1]) for x in failed_paths] + det_msg = '\n\n'.join(det_msg) + typ(self.gui, _('Could not download files'), msg, det_msg=det_msg, + show=True) + + if ok_paths: + from calibre.gui2.add import Adder + self.__adder_func = partial(self._add_from_device_adder, on_card=None, + model=view.model()) + self._adder = Adder(self.gui, self.gui.library_view.model().db, + self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server) + self._adder.add(ok_paths) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index d5879042b4..5f9cb3e75c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -443,6 +443,14 @@ class DeviceManager(Thread): # {{{ return self.create_job_step(self._books, done, description=_('Get list of books on device'), to_job=add_as_step_to_job) + def _prepare_addable_books(self, paths): + return self.device.prepare_addable_books(paths) + + def prepare_addable_books(self, done, paths, add_as_step_to_job=None): + return self.create_job_step(self._prepare_addable_books, done, args=[paths], + description=_('Prepare files for transfer from device'), + to_job=add_as_step_to_job) + def _annotations(self, path_map): return self.device.get_annotations(path_map) From 468a47eb5d8f573935ccd4ac7ec6cf256515fbaf Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 23:07:10 +0530 Subject: [PATCH 38/77] ... --- src/calibre/gui2/device.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 5f9cb3e75c..98e42f4178 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -564,9 +564,8 @@ class DeviceManager(Thread): # {{{ to_job=add_as_step_to_job) def _view_book(self, path, target): - f = open(target, 'wb') - self.device.get_file(path, f) - f.close() + with open(target, 'wb') as f: + self.device.get_file(path, f) return target def view_book(self, done, path, target, add_as_step_to_job=None): From e61dbefc7f250edc955f3b15813e548ebf7a4d72 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 1 Sep 2012 23:12:18 +0530 Subject: [PATCH 39/77] ... --- src/calibre/devices/interface.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 4777cafbe9..d0b2611ead 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -543,6 +543,10 @@ class DevicePlugin(Plugin): ''' Given a list of paths, returns another list of paths. These paths point to addable versions of the books. + + If there is an error preparing a book, then instead of a path, the + position in the returned list for that book should be a three tuple: + (original_path, the exception instance, traceback) ''' return paths From 4bc92ec184f0c4430ff73242a17052097228b28d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 00:06:02 +0530 Subject: [PATCH 40/77] PDF Output: Fix page numbers in outline not always correct --- src/calibre/ebooks/pdf/outline_writer.py | 6 +++--- src/calibre/ebooks/pdf/writer.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/calibre/ebooks/pdf/outline_writer.py b/src/calibre/ebooks/pdf/outline_writer.py index 4b2db84f9e..c89f2d9f41 100644 --- a/src/calibre/ebooks/pdf/outline_writer.py +++ b/src/calibre/ebooks/pdf/outline_writer.py @@ -35,12 +35,12 @@ class Outline(object): page, ypos = 0, 0 item = getattr(toc, 'outline_item_', None) if item is not None: + # First use the item URL without fragment + page, ypos = self.pos_map.get(item, {}).get(None, (0, 0)) if toc.fragment: amap = self.pos_map.get(item, None) if amap is not None: - page, ypos = amap.get(toc.fragment, (0, 0)) - else: - page, ypos = self.pos_map.get(item, {}).get(None, (0, 0)) + page, ypos = amap.get(toc.fragment, (page, ypos)) return page, ypos def add_children(self, toc, parent): diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index d37db69a81..92054e2f76 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -196,6 +196,7 @@ class PDFWriter(QObject): # {{{ self.insert_cover() self.render_succeeded = False + self.current_page_num = self.doc.page_count() self.combine_queue.append(os.path.join(self.tmp_path, 'qprinter_out.pdf')) self.first_page = True @@ -283,9 +284,13 @@ class PDFWriter(QObject): # {{{ paged_display.fit_images(); ''') mf = self.view.page().mainFrame() + start_page = self.current_page_num + if not self.first_page: + start_page += 1 while True: if not self.first_page: - self.printer.newPage() + if self.printer.newPage(): + self.current_page_num += 1 self.first_page = False mf.render(self.painter) nsl = evaljs('paged_display.next_screen_location()').toInt() @@ -297,11 +302,10 @@ class PDFWriter(QObject): # {{{ amap = self.bridge_value if not isinstance(amap, dict): amap = {} # Some javascript error occurred - pages = self.doc.page_count() - self.outline.set_pos(self.current_item, None, pages, 0) + self.outline.set_pos(self.current_item, None, start_page, 0) for anchor, x in amap.iteritems(): pagenum, ypos = x - self.outline.set_pos(self.current_item, anchor, pages + pagenum, ypos) + self.outline.set_pos(self.current_item, anchor, start_page + pagenum, ypos) def append_doc(self, outpath): doc = self.podofo.PDFDoc() From 4e20f776bcfc8ebd100a9ed1c4e8bb62148e49e2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 00:06:54 +0530 Subject: [PATCH 41/77] When vieweing a book on the device by clicking in the book details panel, first copy the book off the device, this allows it to work for the smart device driver and the MTP driver --- src/calibre/gui2/actions/view.py | 18 ++++++++++-------- src/calibre/gui2/book_details.py | 7 ++++--- src/calibre/gui2/init.py | 2 ++ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 43e9dad5c4..5a7a991607 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -256,6 +256,15 @@ class ViewAction(InterfaceAction): db.prefs['gui_view_history'] = history[:vh] self.build_menus(db) + def view_device_book(self, path): + pt = PersistentTemporaryFile('_view_device_book'+\ + os.path.splitext(path)[1]) + self.persistent_files.append(pt) + pt.close() + self.gui.device_manager.view_book( + Dispatcher(self.book_downloaded_for_viewing), + path, pt.name) + def _view_books(self, rows): if not rows or len(rows) == 0: self._launch_viewer() @@ -270,12 +279,5 @@ class ViewAction(InterfaceAction): else: paths = self.gui.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.gui.device_manager.view_book(\ - Dispatcher(self.book_downloaded_for_viewing), - path, pt.name) - + self.view_device_book(path) diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index bf5fbe77bd..f03015f4ad 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -19,8 +19,8 @@ from calibre.ebooks.metadata import fmt_sidx from calibre.ebooks.metadata.sources.identify import urls_from_identifiers from calibre.constants import filesystem_encoding from calibre.library.comments import comments_to_html -from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data, - gprefs, rating_font) +from calibre.gui2 import (config, open_url, pixmap_to_data, gprefs, + rating_font) from calibre.utils.icu import sort_key from calibre.utils.formatter import EvalFormatter from calibre.utils.date import is_date_undefined @@ -569,6 +569,7 @@ class BookDetails(QWidget): # {{{ files_dropped = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object) cover_removed = pyqtSignal(object) + view_device_book = pyqtSignal(object) # Drag 'n drop {{{ DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS @@ -643,7 +644,7 @@ class BookDetails(QWidget): # {{{ id_, fmt = val.split(':') self.view_specific_format.emit(int(id_), fmt) elif typ == 'devpath': - open_local_file(val) + self.view_device_book.emit(val) else: try: open_url(QUrl(link, QUrl.TolerantMode)) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index a82dfec7fc..338a558f29 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -269,6 +269,8 @@ class LayoutMixin(object): # {{{ self.iactions['Remove Books'].remove_format_by_id) self.book_details.save_specific_format.connect( self.iactions['Save To Disk'].save_library_format_by_ids) + self.book_details.view_device_book.connect( + self.iactions['View'].view_device_book) m = self.library_view.model() if m.rowCount(None) > 0: From def42a1da369519eafbce574aa9958d6c7420701 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 00:44:18 +0530 Subject: [PATCH 42/77] PDF Output: Use less memory when writing out the PDF file --- src/calibre/ebooks/pdf/writer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 92054e2f76..9ba9b5c20f 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -15,11 +15,12 @@ from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter, QPixmap, QTimer, pyqtProperty, QString, QSize) from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings +from calibre.constants import filesystem_encoding from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ebooks.pdf.pageoptions import (unit, paper_size, orientation) from calibre.ebooks.pdf.outline_writer import Outline from calibre.ebooks.metadata import authors_to_string -from calibre.ptempfile import PersistentTemporaryFile +from calibre.ptempfile import PersistentTemporaryFile, TemporaryFile from calibre import (__appname__, __version__, fit_image, isosx, force_unicode) from calibre.ebooks.oeb.display.webview import load_html @@ -350,8 +351,12 @@ class PDFWriter(QObject): # {{{ if self.metadata.tags: self.doc.keywords = self.metadata.tags self.outline(self.doc) - raw = self.doc.write() - self.out_stream.write(raw) + with TemporaryFile(u'pdf_out.pdf') as tf: + if isinstance(tf, unicode): + tf = tf.encode(filesystem_encoding) + self.doc.save(tf) + with open(tf, 'rb') as src: + shutil.copyfileobj(src, self.out_stream) self.render_succeeded = True finally: self._delete_tmpdir() From 0e6cce50e6ad70fffc4917d2bb32ab273a267077 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 09:12:20 +0530 Subject: [PATCH 43/77] Update Business Week Magazine --- recipes/bwmagazine2.recipe | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/recipes/bwmagazine2.recipe b/recipes/bwmagazine2.recipe index 300d71806a..77143bbefc 100644 --- a/recipes/bwmagazine2.recipe +++ b/recipes/bwmagazine2.recipe @@ -33,38 +33,36 @@ class BusinessWeekMagazine(BasicNewsRecipe): div0 = soup.find ('div', attrs={'class':'column left'}) section_title = '' feeds = OrderedDict() - articles = [] - for div in div0.findAll('a'): + for div in div0.findAll('h4'): + articles = [] section_title = self.tag_to_string(div.findPrevious('h3')).strip() - self.log('Processing section:', section_title) - title=self.tag_to_string(div).strip() - url=div['href'] + title=self.tag_to_string(div.a).strip() + url=div.a['href'] soup0 = self.index_to_soup(url) urlprint=soup0.find('li', attrs={'class':'print'}).a['href'] articles.append({'title':title, 'url':urlprint, 'description':'', 'date':''}) - if articles: - if section_title not in feeds: - feeds[section_title] = [] - feeds[section_title] += articles + if articles: + if section_title not in feeds: + feeds[section_title] = [] + feeds[section_title] += articles + div1 = soup.find ('div', attrs={'class':'column center'}) section_title = '' - articles = [] - for div in div1.findAll('a'): + for div in div1.findAll('h5'): + articles = [] desc=self.tag_to_string(div.findNext('p')).strip() section_title = self.tag_to_string(div.findPrevious('h3')).strip() - self.log('Processing section:', section_title) - title=self.tag_to_string(div).strip() - url=div['href'] + title=self.tag_to_string(div.a).strip() + url=div.a['href'] soup0 = self.index_to_soup(url) urlprint=soup0.find('li', attrs={'class':'print'}).a['href'] articles.append({'title':title, 'url':urlprint, 'description':desc, 'date':''}) - if articles: - if section_title not in feeds: - feeds[section_title] = [] - feeds[section_title] += articles + if articles: + if section_title not in feeds: + feeds[section_title] = [] + feeds[section_title] += articles ans = [(key, val) for key, val in feeds.iteritems()] return ans - From a907f919dab4b6d76a67fc6f9789710076161b64 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 09:33:54 +0530 Subject: [PATCH 44/77] When adding books to calibre and the book does not have a published date, set the published date to undefined rather than todays date --- src/calibre/library/database2.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index f1103f57ee..17c01a6f56 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -32,7 +32,7 @@ from calibre.customize.ui import run_plugins_on_import from calibre import isbytestring from calibre.utils.filenames import ascii_filename, samefile from calibre.utils.date import (utcnow, now as nowf, utcfromtimestamp, - parse_only_date) + parse_only_date, UNDEFINED_DATE) from calibre.utils.config import prefs, tweaks, from_json, to_json from calibre.utils.icu import sort_key, strcmp, lower from calibre.utils.search_query_parser import saved_searches, set_saved_searches @@ -2498,16 +2498,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): self.notify('metadata', [id]) def set_pubdate(self, id, dt, notify=True, commit=True): - if dt: - if isinstance(dt, basestring): - dt = parse_only_date(dt) - self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id)) - self.data.set(id, self.FIELD_MAP['pubdate'], dt, row_is_id=True) - self.dirtied([id], commit=False) - if commit: - self.conn.commit() - if notify: - self.notify('metadata', [id]) + if not dt: + dt = UNDEFINED_DATE + if isinstance(dt, basestring): + dt = parse_only_date(dt) + self.conn.execute('UPDATE books SET pubdate=? WHERE id=?', (dt, id)) + self.data.set(id, self.FIELD_MAP['pubdate'], dt, row_is_id=True) + self.dirtied([id], commit=False) + if commit: + self.conn.commit() + if notify: + self.notify('metadata', [id]) def set_publisher(self, id, publisher, notify=True, commit=True, @@ -3344,7 +3345,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi.timestamp is None: mi.timestamp = utcnow() if mi.pubdate is None: - mi.pubdate = utcnow() + mi.pubdate = UNDEFINED_DATE self.set_metadata(id, mi, ignore_errors=True, commit=True) if cover is not None: try: @@ -3386,7 +3387,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi.timestamp is None: mi.timestamp = utcnow() if mi.pubdate is None: - mi.pubdate = utcnow() + mi.pubdate = UNDEFINED_DATE self.set_metadata(id, mi, commit=True, ignore_errors=True) npath = self.run_import_plugins(path, format) format = os.path.splitext(npath)[-1].lower().replace('.', '').upper() @@ -3426,7 +3427,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): if mi.timestamp is None: mi.timestamp = utcnow() if mi.pubdate is None: - mi.pubdate = utcnow() + mi.pubdate = UNDEFINED_DATE self.set_metadata(id, mi, ignore_errors=True, commit=True) if preserve_uuid and mi.uuid: self.set_uuid(id, mi.uuid, commit=False) From 9360c5833a740336e58991c3d25b71d195e67335 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 09:35:50 +0530 Subject: [PATCH 45/77] Fix Chronicle of Higher Education --- recipes/chronicle_higher_ed.recipe | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/recipes/chronicle_higher_ed.recipe b/recipes/chronicle_higher_ed.recipe index 7ed834a4e5..f0188d4d77 100644 --- a/recipes/chronicle_higher_ed.recipe +++ b/recipes/chronicle_higher_ed.recipe @@ -13,13 +13,13 @@ class Chronicle(BasicNewsRecipe): keep_only_tags = [ dict(name='div', attrs={'class':'article'}), ] - remove_tags = [dict(name='div',attrs={'class':'related module1'})] + remove_tags = [dict(name='div',attrs={'class':['related module1','maintitle']}), + dict(name='div', attrs={'id':['section-nav','icon-row']})] no_javascript = True no_stylesheets = True needs_subscription = True - def get_browser(self): br = BasicNewsRecipe.get_browser() if self.username is not None and self.password is not None: @@ -27,7 +27,7 @@ class Chronicle(BasicNewsRecipe): br.select_form(nr=1) br['username'] = self.username br['password'] = self.password - br.submit() + br.submit() return br def parse_index(self): @@ -47,33 +47,35 @@ class Chronicle(BasicNewsRecipe): #Go to the main body soup = self.index_to_soup(issueurl) - div0 = soup.find ('div', attrs={'id':'article-body'}) + div = soup.find ('div', attrs={'id':'article-body'}) feeds = OrderedDict() - for div in div0.findAll('div',attrs={'class':'module1'}): - section_title = self.tag_to_string(div.find('h3')) - for post in div.findAll('li',attrs={'class':'sub-promo'}): - articles = [] - a=post.find('a', href=True) + section_title = '' + for post in div.findAll('li'): + articles = [] + a=post.find('a', href=True) + if a is not None: title=self.tag_to_string(a) url="http://chronicle.com"+a['href'].strip() + sectiontitle=post.findPrevious('h3') + if sectiontitle is None: + sectiontitle=post.findPrevious('h4') + section_title=self.tag_to_string(sectiontitle) desc=self.tag_to_string(post.find('p')) articles.append({'title':title, 'url':url, 'description':desc, 'date':''}) - if articles: - if section_title not in feeds: - feeds[section_title] = [] - feeds[section_title] += articles + if articles: + if section_title not in feeds: + feeds[section_title] = [] + feeds[section_title] += articles ans = [(key, val) for key, val in feeds.iteritems()] return ans def preprocess_html(self,soup): #process all the images for div in soup.findAll('div', attrs={'class':'tableauPlaceholder'}): - noscripts=div.find('noscript').a div.replaceWith(noscripts) for div0 in soup.findAll('div',text='Powered by Tableau'): div0.extract() return soup - From a4b16899014864d5b403c79a1ae25c0689ca1c8d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 09:37:42 +0530 Subject: [PATCH 46/77] Fix Financial Times (UK) --- recipes/financial_times_uk.recipe | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/recipes/financial_times_uk.recipe b/recipes/financial_times_uk.recipe index 4c331f115f..16295905bc 100644 --- a/recipes/financial_times_uk.recipe +++ b/recipes/financial_times_uk.recipe @@ -10,7 +10,7 @@ from calibre import strftime from calibre.web.feeds.news import BasicNewsRecipe class FinancialTimes(BasicNewsRecipe): - title = 'Financial Times - UK printed edition' + title = 'Financial Times (UK)' __author__ = 'Darko Miletic' description = "The Financial Times (FT) is one of the world's leading business news and information organisations, recognised internationally for its authority, integrity and accuracy." publisher = 'The Financial Times Ltd.' @@ -101,17 +101,19 @@ class FinancialTimes(BasicNewsRecipe): def parse_index(self): feeds = [] soup = self.index_to_soup(self.INDEX) + dates= self.tag_to_string(soup.find('div', attrs={'class':'btm-links'}).find('div')) + self.timefmt = ' [%s]'%dates wide = soup.find('div',attrs={'class':'wide'}) if not wide: return feeds strest = wide.findAll('h3', attrs={'class':'section'}) if not strest: return feeds - st = wide.find('h4',attrs={'class':'section-no-arrow'}) + st = wide.findAll('h4',attrs={'class':'section-no-arrow'}) if st: - strest.insert(0,st) + st.extend(strest) count = 0 - for item in strest: + for item in st: count = count + 1 if self.test and count > 2: return feeds @@ -151,7 +153,7 @@ class FinancialTimes(BasicNewsRecipe): def get_cover_url(self): cdate = datetime.date.today() if cdate.isoweekday() == 7: - cdate -= datetime.timedelta(days=1) + cdate -= datetime.timedelta(days=1) return cdate.strftime('http://specials.ft.com/vtf_pdf/%d%m%y_FRONT1_LON.pdf') def get_obfuscated_article(self, url): @@ -163,9 +165,8 @@ class FinancialTimes(BasicNewsRecipe): count = 10 except: print "Retrying download..." - count += 1 + count += 1 self.temp_files.append(PersistentTemporaryFile('_fa.html')) self.temp_files[-1].write(html) self.temp_files[-1].close() return self.temp_files[-1].name - \ No newline at end of file From b3fd7d3e0153b3aa668e9c473452c8f5cfd5c610 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 09:38:42 +0530 Subject: [PATCH 47/77] ... --- src/calibre/ebooks/pdf/writer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index 9ba9b5c20f..a547d3e650 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -122,7 +122,7 @@ class PDFMetadata(object): # {{{ self.author = force_unicode(self.author) # }}} -class Page(QWebPage): +class Page(QWebPage): # {{{ def __init__(self, opts, log): self.log = log @@ -153,6 +153,7 @@ class Page(QWebPage): def javaScriptAlert(self, frame, msg): self.log(unicode(msg)) +# }}} class PDFWriter(QObject): # {{{ From 5736706846d6bf68723c1a595516f22986ee95c7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 14:59:20 +0530 Subject: [PATCH 48/77] podofo: Implement writing to python file objects --- setup/extensions.py | 1 + src/calibre/ebooks/pdf/writer.py | 10 +- src/calibre/utils/podofo/__init__.py | 15 ++- src/calibre/utils/podofo/doc.cpp | 12 ++ src/calibre/utils/podofo/global.h | 1 + src/calibre/utils/podofo/output.cpp | 172 +++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 src/calibre/utils/podofo/output.cpp diff --git a/setup/extensions.py b/setup/extensions.py index 2f2e2aa9ba..f4ed22687b 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -139,6 +139,7 @@ extensions = [ Extension('podofo', [ 'calibre/utils/podofo/utils.cpp', + 'calibre/utils/podofo/output.cpp', 'calibre/utils/podofo/doc.cpp', 'calibre/utils/podofo/outline.cpp', 'calibre/utils/podofo/podofo.cpp', diff --git a/src/calibre/ebooks/pdf/writer.py b/src/calibre/ebooks/pdf/writer.py index a547d3e650..a9cb951e35 100644 --- a/src/calibre/ebooks/pdf/writer.py +++ b/src/calibre/ebooks/pdf/writer.py @@ -15,12 +15,11 @@ from PyQt4.Qt import (QEventLoop, QObject, QPrinter, QSizeF, Qt, QPainter, QPixmap, QTimer, pyqtProperty, QString, QSize) from PyQt4.QtWebKit import QWebView, QWebPage, QWebSettings -from calibre.constants import filesystem_encoding from calibre.ptempfile import PersistentTemporaryDirectory from calibre.ebooks.pdf.pageoptions import (unit, paper_size, orientation) from calibre.ebooks.pdf.outline_writer import Outline from calibre.ebooks.metadata import authors_to_string -from calibre.ptempfile import PersistentTemporaryFile, TemporaryFile +from calibre.ptempfile import PersistentTemporaryFile from calibre import (__appname__, __version__, fit_image, isosx, force_unicode) from calibre.ebooks.oeb.display.webview import load_html @@ -352,12 +351,7 @@ class PDFWriter(QObject): # {{{ if self.metadata.tags: self.doc.keywords = self.metadata.tags self.outline(self.doc) - with TemporaryFile(u'pdf_out.pdf') as tf: - if isinstance(tf, unicode): - tf = tf.encode(filesystem_encoding) - self.doc.save(tf) - with open(tf, 'rb') as src: - shutil.copyfileobj(src, self.out_stream) + self.doc.save_to_fileobj(self.out_stream) self.render_succeeded = True finally: self._delete_tmpdir() diff --git a/src/calibre/utils/podofo/__init__.py b/src/calibre/utils/podofo/__init__.py index 3134dcd1ba..13c12a9bb3 100644 --- a/src/calibre/utils/podofo/__init__.py +++ b/src/calibre/utils/podofo/__init__.py @@ -94,9 +94,8 @@ def delete_all_but(path, pages): if page not in pages: p.delete_page(page) - raw = p.write() with open(path, 'wb') as f: - f.write(raw) + f.save_to_fileobj(path) def test_outline(src): podofo = get_podofo() @@ -114,7 +113,17 @@ def test_outline(src): f.write(raw) print 'Outlined PDF:', out +def test_save_to(src, dest): + podofo = get_podofo() + p = podofo.PDFDoc() + with open(src, 'rb') as f: + raw = f.read() + p.load(raw) + with open(dest, 'wb') as out: + p.save_to_fileobj(out) + print ('Wrote PDF of size:', out.tell()) + if __name__ == '__main__': import sys - test_outline(sys.argv[-1]) + test_save_to(sys.argv[-2], sys.argv[-1]) diff --git a/src/calibre/utils/podofo/doc.cpp b/src/calibre/utils/podofo/doc.cpp index 7166b2320e..90bfc27921 100644 --- a/src/calibre/utils/podofo/doc.cpp +++ b/src/calibre/utils/podofo/doc.cpp @@ -104,6 +104,15 @@ PDFDoc_write(PDFDoc *self, PyObject *args) { return ans; } + +static PyObject * +PDFDoc_save_to_fileobj(PDFDoc *self, PyObject *args) { + PyObject *f; + + if (!PyArg_ParseTuple(args, "O", &f)) return NULL; + return write_doc(self->doc, f); +} + // }}} // extract_first_page() {{{ @@ -453,6 +462,9 @@ static PyMethodDef PDFDoc_methods[] = { {"write", (PyCFunction)PDFDoc_write, METH_VARARGS, "Return the PDF document as a bytestring." }, + {"save_to_fileobj", (PyCFunction)PDFDoc_save_to_fileobj, METH_VARARGS, + "Write the PDF document to the soecified file-like object." + }, {"extract_first_page", (PyCFunction)PDFDoc_extract_first_page, METH_VARARGS, "extract_first_page() -> Remove all but the first page." }, diff --git a/src/calibre/utils/podofo/global.h b/src/calibre/utils/podofo/global.h index fa9a141b21..4a180d86a0 100644 --- a/src/calibre/utils/podofo/global.h +++ b/src/calibre/utils/podofo/global.h @@ -41,6 +41,7 @@ extern void podofo_set_exception(const PdfError &err); extern PyObject * podofo_convert_pdfstring(const PdfString &s); extern PdfString * podofo_convert_pystring(PyObject *py); extern PdfString * podofo_convert_pystring_single_byte(PyObject *py); +extern PyObject* write_doc(PdfMemDocument *doc, PyObject *f); } diff --git a/src/calibre/utils/podofo/output.cpp b/src/calibre/utils/podofo/output.cpp new file mode 100644 index 0000000000..63e1270af6 --- /dev/null +++ b/src/calibre/utils/podofo/output.cpp @@ -0,0 +1,172 @@ +/* + * output.cpp + * Copyright (C) 2012 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "global.h" + +using namespace PoDoFo; + +class pyerr : public std::exception { +}; + +class OutputDevice : public PdfOutputDevice { + + private: + PyObject *file; + size_t written; + + void update_written() { + size_t pos; + pos = Tell(); + if (pos > written) written = pos; + } + + public: + OutputDevice(PyObject *f) : file(f), written(0) { Py_XINCREF(file); } + ~OutputDevice() { Py_XDECREF(file); file = NULL; } + + size_t GetLength() const { return written; } + + long PrintVLen(const char* pszFormat, va_list args) { + char buf[10]; + int res; + + if( !pszFormat ) { PODOFO_RAISE_ERROR( ePdfError_InvalidHandle ); } + + res = PyOS_vsnprintf(buf, 1, pszFormat, args); + if (res < 0) { + PyErr_SetString(PyExc_Exception, "Something bad happend while calling PyOS_vsnprintf"); + throw pyerr(); + } + return static_cast(res+1); + } + + void PrintV( const char* pszFormat, long lBytes, va_list args ) { + char *buf; + int res; + + if( !pszFormat ) { PODOFO_RAISE_ERROR( ePdfError_InvalidHandle ); } + + buf = new (std::nothrow) char[lBytes+1]; + if (buf == NULL) { PyErr_NoMemory(); throw pyerr(); } + + res = PyOS_vsnprintf(buf, lBytes, pszFormat, args); + + if (res < 0) { + PyErr_SetString(PyExc_Exception, "Something bad happend while calling PyOS_vsnprintf"); + delete[] buf; + throw pyerr(); + } + + Write(buf, static_cast(res)); + delete[] buf; + } + + void Print( const char* pszFormat, ... ) + { + va_list args; + long lBytes; + + va_start( args, pszFormat ); + lBytes = PrintVLen(pszFormat, args); + va_end( args ); + + va_start( args, pszFormat ); + PrintV(pszFormat, lBytes, args); + va_end( args ); + } + + size_t Read( char* pBuffer, size_t lLen ) { + PyObject *ret; + char *buf = NULL; + Py_ssize_t len = 0; + + ret = PyObject_CallMethod(file, (char*)"read", (char*)"n", static_cast(lLen)); + if (ret != NULL) { + if (PyBytes_AsStringAndSize(ret, &buf, &len) != -1) { + memcpy(pBuffer, buf, len); + Py_DECREF(ret); + return static_cast(len); + } + Py_DECREF(ret); + } + + if (PyErr_Occurred() == NULL) + PyErr_SetString(PyExc_Exception, "Failed to read data from python file object"); + + throw pyerr(); + + } + + void Seek(size_t offset) { + PyObject *ret; + ret = PyObject_CallMethod(file, (char*)"seek", (char*)"n", static_cast(offset)); + if (ret == NULL) { + if (PyErr_Occurred() == NULL) + PyErr_SetString(PyExc_Exception, "Failed to seek in python file object"); + throw pyerr(); + } + Py_DECREF(ret); + } + + size_t Tell() const { + PyObject *ret; + unsigned long ans; + + ret = PyObject_CallMethod(file, (char*)"tell", NULL); + if (ret == NULL) { + if (PyErr_Occurred() == NULL) + PyErr_SetString(PyExc_Exception, "Failed to call tell() on python file object"); + throw pyerr(); + } + if (!PyNumber_Check(ret)) { + Py_DECREF(ret); + PyErr_SetString(PyExc_Exception, "tell() method did not return a number"); + throw pyerr(); + } + ans = PyInt_AsUnsignedLongMask(ret); + Py_DECREF(ret); + if (PyErr_Occurred() != NULL) throw pyerr(); + + return static_cast(ans); + } + + void Write(const char* pBuffer, size_t lLen) { + PyObject *ret; + + ret = PyObject_CallMethod(file, (char*)"write", (char*)"s#", pBuffer, (int)lLen); + if (ret == NULL) { + if (PyErr_Occurred() == NULL) + PyErr_SetString(PyExc_Exception, "Failed to call write() on python file object"); + throw pyerr(); + } + Py_DECREF(ret); + update_written(); + } + + void Flush() { + Py_XDECREF(PyObject_CallMethod(file, (char*)"flush", NULL)); + } + +}; + + +PyObject* pdf::write_doc(PdfMemDocument *doc, PyObject *f) { + OutputDevice d(f); + + try { + doc->Write(&d); + } catch(const PdfError & err) { + podofo_set_exception(err); return NULL; + } catch (...) { + if (PyErr_Occurred() == NULL) + PyErr_SetString(PyExc_Exception, "An unknown error occurred while trying to write the pdf to the file object"); + return NULL; + } + + Py_RETURN_NONE; +} + From efac50df463a5081dac925023356c31b6880eaf2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 15:13:12 +0530 Subject: [PATCH 49/77] PDF Output: Do not error out when generating an outline which points to pages that have been removed. Fixes #1044799 (Private bug) --- src/calibre/ebooks/pdf/outline_writer.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/calibre/ebooks/pdf/outline_writer.py b/src/calibre/ebooks/pdf/outline_writer.py index c89f2d9f41..64d11f0208 100644 --- a/src/calibre/ebooks/pdf/outline_writer.py +++ b/src/calibre/ebooks/pdf/outline_writer.py @@ -47,14 +47,19 @@ class Outline(object): for child in toc: page, ypos = self.get_pos(child) text = child.text or _('Page %d')%page + if page >= self.page_count: + page = self.page_count - 1 cn = parent.create(text, page, True) self.add_children(child, cn) def __call__(self, doc): self.pos_map = dict(self.pos_map) + self.page_count = doc.page_count() for child in self.toc: page, ypos = self.get_pos(child) text = child.text or _('Page %d')%page + if page >= self.page_count: + page = self.page_count - 1 node = doc.create_outline(text, page) self.add_children(child, node) From f280dd4de72792658001d2481a64d96370d0cbd7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 17:40:15 +0530 Subject: [PATCH 50/77] Remove PyOS_vsnprintf as it is broken on windows --- src/calibre/utils/podofo/output.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/calibre/utils/podofo/output.cpp b/src/calibre/utils/podofo/output.cpp index 63e1270af6..b0620f7f82 100644 --- a/src/calibre/utils/podofo/output.cpp +++ b/src/calibre/utils/podofo/output.cpp @@ -31,17 +31,21 @@ class OutputDevice : public PdfOutputDevice { size_t GetLength() const { return written; } long PrintVLen(const char* pszFormat, va_list args) { - char buf[10]; - int res; if( !pszFormat ) { PODOFO_RAISE_ERROR( ePdfError_InvalidHandle ); } - res = PyOS_vsnprintf(buf, 1, pszFormat, args); +#ifdef _MSC_VER + return _vscprintf(pszFormat, args); +#else + char buf[10]; + int res; + res = vsnprintf(buf, 1, pszFormat, args); if (res < 0) { - PyErr_SetString(PyExc_Exception, "Something bad happend while calling PyOS_vsnprintf"); + PyErr_SetString(PyExc_Exception, "Something bad happened while calling vsnprintf to get buffer length"); throw pyerr(); } return static_cast(res+1); +#endif } void PrintV( const char* pszFormat, long lBytes, va_list args ) { @@ -53,10 +57,11 @@ class OutputDevice : public PdfOutputDevice { buf = new (std::nothrow) char[lBytes+1]; if (buf == NULL) { PyErr_NoMemory(); throw pyerr(); } - res = PyOS_vsnprintf(buf, lBytes, pszFormat, args); + // Note: PyOS_vsnprintf produces broken output on windows + res = vsnprintf(buf, lBytes, pszFormat, args); if (res < 0) { - PyErr_SetString(PyExc_Exception, "Something bad happend while calling PyOS_vsnprintf"); + PyErr_SetString(PyExc_Exception, "Something bad happened while calling vsnprintf"); delete[] buf; throw pyerr(); } From 94b192b1340658d92df8e5dcf6a48d9f32f5f96e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 18:04:56 +0530 Subject: [PATCH 51/77] ... --- src/calibre/utils/podofo/output.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/calibre/utils/podofo/output.cpp b/src/calibre/utils/podofo/output.cpp index b0620f7f82..015949502a 100644 --- a/src/calibre/utils/podofo/output.cpp +++ b/src/calibre/utils/podofo/output.cpp @@ -37,14 +37,16 @@ class OutputDevice : public PdfOutputDevice { #ifdef _MSC_VER return _vscprintf(pszFormat, args); #else - char buf[10]; - int res; - res = vsnprintf(buf, 1, pszFormat, args); - if (res < 0) { - PyErr_SetString(PyExc_Exception, "Something bad happened while calling vsnprintf to get buffer length"); - throw pyerr(); + char *buf; + int res, len=1024; + while(true) { + buf = new (std::nothrow) char[len]; + if (buf == NULL) { PyErr_NoMemory(); throw pyerr(); } + res = vsnprintf(buf, len, pszFormat, args); + delete[] buf; + if (res >= 0) return res + 1; + len *= 2; } - return static_cast(res+1); #endif } From ae73428839849ba5495754104d738597f9006e4e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 18:06:19 +0530 Subject: [PATCH 52/77] ... --- src/calibre/utils/podofo/output.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/utils/podofo/output.cpp b/src/calibre/utils/podofo/output.cpp index 015949502a..d72fd316fe 100644 --- a/src/calibre/utils/podofo/output.cpp +++ b/src/calibre/utils/podofo/output.cpp @@ -40,7 +40,7 @@ class OutputDevice : public PdfOutputDevice { char *buf; int res, len=1024; while(true) { - buf = new (std::nothrow) char[len]; + buf = new (std::nothrow) char[len+1]; if (buf == NULL) { PyErr_NoMemory(); throw pyerr(); } res = vsnprintf(buf, len, pszFormat, args); delete[] buf; From e6af33d5e911a386046226a6c724224fdcfddc6e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 2 Sep 2012 19:38:46 +0530 Subject: [PATCH 53/77] Disable libusb/libmtp on OS X --- src/calibre/devices/mtp/unix/driver.py | 3 ++- src/calibre/devices/scanner.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 338913114f..2f215f6353 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -27,7 +27,8 @@ def fingerprint(d): class MTP_DEVICE(MTPDeviceBase): - supported_platforms = ['linux', 'osx'] + # libusb(x) does not work on OS X. So no MTP support for OS X + supported_platforms = ['linux'] def __init__(self, *args, **kwargs): MTPDeviceBase.__init__(self, *args, **kwargs) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 6865546a54..e0bb74fa2a 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -292,7 +292,15 @@ if islinux: libusb_scanner = LibUSBScanner() if isosx: - osx_scanner = libusb_scanner + # Apparently libusb causes mem leaks on some Macs and hangs on others and + # works on a few. OS X users will just have to live without MTP support. + # See https://bugs.launchpad.net/calibre/+bug/1044706 + # See https://bugs.launchpad.net/calibre/+bug/1044758 + # osx_scanner = libusb_scanner + usbobserver, usbobserver_err = plugins['usbobserver'] + if usbobserver is None: + raise RuntimeError('Failed to load usbobserver: %s'%usbobserver_err) + osx_scanner = usbobserver.get_usb_devices if isfreebsd: freebsd_scanner = FreeBSDScanner() From fbd7f787c275813099b38239ab724bc6974ec74a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 11:53:40 +0530 Subject: [PATCH 54/77] E-book viewer: Make the detection of full screen layouts like covers a little more robust --- resources/compiled_coffeescript.zip | Bin 57018 -> 56964 bytes src/calibre/ebooks/oeb/display/paged.coffee | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 9f8bccfd5e35a0494759378e0f8a291174a34875..573a8128ac70556e1a7468ad34ee82f7d3157c76 100644 GIT binary patch delta 174 zcmdnBm$_vxvqXS5Gm8iV2&{-xc1-o@Onv6Y!0^m%vRsJd=0`jS_$S|xkz+L9{9Q(p zo6&f)pvJW=8<{rpXfiU1Fk{olz#t7a4Ngo}3=x}Lc-M$Y-hJ}LMA6B+fXs6tK&HZE szI$3slBr-(|9b{Zn#U$L-cz0Y{Ei0Gzw?s??t*KUr51%}rj%qTXe$_*DyXSV<~I|a{E=Ud(P*=PfFw7I5lF02N@26F z^yV#-Rqts`zIjJ{vi=1wod9n}CJ|;41`xos5-bcQHu69mV8p~00%8TYb4+%;tHrc6 r5ybl+!acbc$YUr1@y^{fU=p}I>7J?xD;r2V8xZbiWMFuD2gCyaYEDRu diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 4f912513a9..286945bfb6 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -79,7 +79,7 @@ class PagedDisplay if not this.in_paged_mode # Check if the current document is a full screen layout like # cover, if so we treat it specially. - single_screen = (document.body.scrollWidth < window.innerWidth + 25 and document.body.scrollHeight < window.innerHeight + 25) + single_screen = (document.body.scrollHeight < window.innerHeight + 75) first_layout = true ww = window.innerWidth @@ -149,7 +149,7 @@ class PagedDisplay # current page (when cols_per_screen == 1). Similarly img elements # with height=100% overflow the first column has_svg = document.getElementsByTagName('svg').length > 0 - only_img = document.getElementsByTagName('img').length == 1 and document.getElementsByTagName('div').length < 2 and document.getElementsByTagName('p').length < 2 + only_img = document.getElementsByTagName('img').length == 1 and document.getElementsByTagName('div').length < 3 and document.getElementsByTagName('p').length < 2 this.is_full_screen_layout = (only_img or has_svg) and single_screen and document.body.scrollWidth > document.body.clientWidth this.in_paged_mode = true From 0e8b5d6bb4cc70c8e553086ba74607568d80f2c3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 12:09:18 +0530 Subject: [PATCH 55/77] Updated various Romanian news sources --- recipes/catavencu.recipe | 16 ++--- recipes/dilemaveche.recipe | 112 ++++++++++++++--------------------- recipes/timesnewroman.recipe | 8 ++- 3 files changed, 59 insertions(+), 77 deletions(-) diff --git a/recipes/catavencu.recipe b/recipes/catavencu.recipe index db4057cd6d..d5ba7593e2 100644 --- a/recipes/catavencu.recipe +++ b/recipes/catavencu.recipe @@ -12,7 +12,7 @@ from calibre.web.feeds.news import BasicNewsRecipe class AcademiaCatavencu(BasicNewsRecipe): title = u'Academia Ca\u0163avencu' __author__ = u'Silviu Cotoar\u0103' - description = 'Tagma cum laude' + description = 'Academia Catavencu. Pamflete!' publisher = u'Ca\u0163avencu' oldest_article = 5 language = 'ro' @@ -21,7 +21,7 @@ class AcademiaCatavencu(BasicNewsRecipe): use_embedded_content = False category = 'Ziare' encoding = 'utf-8' - cover_url = 'http://www.academiacatavencu.info/images/logo.png' + cover_url = 'http://www.inpolitics.ro/Uploads/Articles/academia_catavencu.jpg' conversion_options = { 'comments' : description @@ -31,21 +31,21 @@ class AcademiaCatavencu(BasicNewsRecipe): } keep_only_tags = [ - dict(name='h1', attrs={'class':'art_title'}), - dict(name='div', attrs={'class':'art_text'}) + dict(name='h1', attrs={'class':'entry-title'}), + dict(name='div', attrs={'class':'entry-content'}) ] remove_tags = [ - dict(name='div', attrs={'class':['desp_m']}) - , dict(name='div', attrs={'id':['tags']}) + dict(name='div', attrs={'class':['mr_social_sharing_wrapper']}) + , dict(name='div', attrs={'id':['fb_share_1']}) ] remove_tags_after = [ - dict(name='div', attrs={'class':['desp_m']}) + dict(name='div', attrs={'id':['fb_share_1']}) ] feeds = [ - (u'Feeds', u'http://www.academiacatavencu.info/rss.xml') + (u'Feeds', u'http://www.academiacatavencu.info/feed') ] def preprocess_html(self, soup): diff --git a/recipes/dilemaveche.recipe b/recipes/dilemaveche.recipe index 8ba75c4123..72920600f7 100644 --- a/recipes/dilemaveche.recipe +++ b/recipes/dilemaveche.recipe @@ -1,71 +1,51 @@ +# -*- coding: utf-8 -*- +#!/usr/bin/env python + +__license__ = 'GPL v3' +__copyright__ = u'2011, Silviu Cotoar\u0103' +''' +dilemaveche.ro +''' + from calibre.web.feeds.news import BasicNewsRecipe class DilemaVeche(BasicNewsRecipe): - title = u'Dilema Veche' # apare vinerea, mai pe dupa-masa,depinde de Luiza cred (care se semneaza ca fiind creatorul fiecarui articol in feed-ul RSS) - __author__ = 'song2' # inspirat din scriptul pentru Le Monde. Inspired from the Le Monde script - description = '"Sint vechi, domnule!" (I.L. Caragiale)' - publisher = 'Adevarul Holding' - oldest_article = 7 - max_articles_per_feed = 200 - encoding = 'utf8' - language = 'ro' - masthead_url = 'http://www.dilemaveche.ro/sites/all/themes/dilema/theme/dilema_two/layouter/dilema_two_homepage/logo.png' - publication_type = 'magazine' - feeds = [ - ('Editoriale si opinii - Situatiunea', 'http://www.dilemaveche.ro/taxonomy/term/37/0/feed'), - ('Editoriale si opinii - Pe ce lume traim', 'http://www.dilemaveche.ro/taxonomy/term/38/0/feed'), - ('Editoriale si opinii - Bordeie si obiceie', 'http://www.dilemaveche.ro/taxonomy/term/44/0/feed'), - ('Editoriale si opinii - Talc Show', 'http://www.dilemaveche.ro/taxonomy/term/44/0/feed'), - ('Tema saptamanii', 'http://www.dilemaveche.ro/taxonomy/term/19/0/feed'), - ('La zi in cultura - Dilema va recomanda', 'http://www.dilemaveche.ro/taxonomy/term/58/0/feed'), - ('La zi in cultura - Carte', 'http://www.dilemaveche.ro/taxonomy/term/14/0/feed'), - ('La zi in cultura - Film', 'http://www.dilemaveche.ro/taxonomy/term/13/0/feed'), - ('La zi in cultura - Muzica', 'http://www.dilemaveche.ro/taxonomy/term/1341/0/feed'), - ('La zi in cultura - Arte performative', 'http://www.dilemaveche.ro/taxonomy/term/1342/0/feed'), - ('La zi in cultura - Arte vizuale', 'http://www.dilemaveche.ro/taxonomy/term/1512/0/feed'), - ('Societate - Ieri cu vedere spre azi', 'http://www.dilemaveche.ro/taxonomy/term/15/0/feed'), - ('Societate - Din polul opus', 'http://www.dilemaveche.ro/taxonomy/term/41/0/feed'), - ('Societate - Mass comedia', 'http://www.dilemaveche.ro/taxonomy/term/43/0/feed'), - ('Societate - La singular si la plural', 'http://www.dilemaveche.ro/taxonomy/term/42/0/feed'), - ('Oameni si idei - Educatie', 'http://www.dilemaveche.ro/taxonomy/term/46/0/feed'), - ('Oameni si idei - Polemici si dezbateri', 'http://www.dilemaveche.ro/taxonomy/term/48/0/feed'), - ('Oameni si idei - Stiinta si tehnologie', 'http://www.dilemaveche.ro/taxonomy/term/46/0/feed'), - ('Dileme on-line', 'http://www.dilemaveche.ro/taxonomy/term/005/0/feed') - ] - remove_tags_before = dict(name='div',attrs={'class':'spacer_10'}) - remove_tags = [ - dict(name='div', attrs={'class':'art_related_left'}), - dict(name='div', attrs={'class':'controale'}), - dict(name='div', attrs={'class':'simple_overlay'}), - ] - remove_tags_after = [dict(id='facebookLike')] - remove_javascript = True + title = u'Dilema Veche' + __author__ = u'Silviu Cotoar\u0103' + description = 'Sint vechi, domnule! (I.L. Caragiale)' + publisher = u'Adev\u0103rul Holding' + oldest_article = 5 + language = 'ro' + max_articles_per_feed = 100 no_stylesheets = True - remove_empty_feeds = True - extra_css = """ - body{font-family: Georgia,Times,serif } - img{margin-bottom: 0.4em; display:block} - """ - def get_cover_url(self): - cover_url = None - soup = self.index_to_soup('http://dilemaveche.ro') - link_item = soup.find('div',attrs={'class':'box_dr_pdf_picture'}) - if link_item and link_item.a: - cover_url = link_item.a['href'] - br = BasicNewsRecipe.get_browser() - try: - br.open(cover_url) - except: #daca nu gaseste pdf-ul - self.log("\nPDF indisponibil") - link_item = soup.find('div',attrs={'class':'box_dr_pdf_picture'}) - if link_item and link_item.img: - cover_url = link_item.img['src'] - br = BasicNewsRecipe.get_browser() - try: - br.open(cover_url) - except: #daca nu gaseste nici imaginea mica mica - print('Mama lor de nenorociti! nu este nici pdf nici imagine') - cover_url ='http://www.dilemaveche.ro/sites/all/themes/dilema/theme/dilema_two/layouter/dilema_two_homepage/logo.png' - return cover_url - cover_margins = (10, 15, '#ffffff') + use_embedded_content = False + category = 'Ziare' + encoding = 'utf-8' + cover_url = 'http://dilemaveche.ro/sites/all/themes/dilema/theme/dilema_two/layouter/dilema_two_homepage/logo.png' + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [ + dict(name='div', attrs={'class':'c_left_column'}) + ] + + remove_tags = [ + dict(name='div', attrs={'id':['adshop_widget_428x60']}) , + dict(name='div', attrs={'id':['gallery']}) + ] + + remove_tags_after = [ + dict(name='div', attrs={'id':['adshop_widget_428x60']}) + ] + + feeds = [ + (u'Feeds', u'http://dilemaveche.ro/rss.xml') + ] + + def preprocess_html(self, soup): + return self.adeify_images(soup) diff --git a/recipes/timesnewroman.recipe b/recipes/timesnewroman.recipe index 12672aa888..79c2a9628a 100644 --- a/recipes/timesnewroman.recipe +++ b/recipes/timesnewroman.recipe @@ -36,12 +36,14 @@ class TimesNewRoman(BasicNewsRecipe): remove_tags = [ dict(name='p', attrs={'class':['articleinfo']}) - , dict(name='div',attrs={'class':['vergefacebooklike']}) - , dict(name='div', attrs={'class':'cleared'}) + , dict(name='div', attrs={'class':['shareTools']}) + , dict(name='div', attrs={'class':'fb_iframe_widget'}) + , dict(name='div', attrs={'id':'jc'}) ] remove_tags_after = [ - dict(name='div', attrs={'class':'cleared'}) + dict(name='div', attrs={'class':'fb_iframe_widget'}), + dict(name='div', attrs={'id':'jc'}) ] feeds = [ From ed05dbaa2d9696debf0d596720b6fb1e2694a8d2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 13:04:51 +0530 Subject: [PATCH 56/77] Android driver: Add an extra customization option to configure the directory to which eboks are sent on the storage cards. Fixes #1045045 (Android save-to directory list honored only for main memory) --- src/calibre/devices/android/driver.py | 44 +++++++++++++++++++-------- src/calibre/devices/usbms/device.py | 30 ++++++++++-------- 2 files changed, 48 insertions(+), 26 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 9e8aa5fe17..efe979158d 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -186,10 +186,15 @@ class ANDROID(USBMS): } EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks'] - EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to ' - 'send e-books to on the device. The first one that exists will ' + EXTRA_CUSTOMIZATION_MESSAGE = [_('Comma separated list of directories to ' + 'send e-books to on the device\'s main memory. The first one that exists will ' + 'be used'), + _('Comma separated list of directories to ' + 'send e-books to on the device\'s storage cards. The first one that exists will ' 'be used') - EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) + ] + + EXTRA_CUSTOMIZATION_DEFAULT = [', '.join(EBOOK_DIR_MAIN), ''] VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX', 'GOOGLE', 'ARCHOS', @@ -237,23 +242,35 @@ class ANDROID(USBMS): def post_open_callback(self): opts = self.settings() - dirs = opts.extra_customization - if not dirs: - dirs = self.EBOOK_DIR_MAIN - else: - dirs = [x.strip() for x in dirs.split(',')] - self.EBOOK_DIR_MAIN = dirs + opts = opts.extra_customization + if not opts: + opts = [self.EBOOK_DIR_MAIN, ''] + + def strtolist(x): + if isinstance(x, basestring): + x = [y.strip() for y in x.split(',')] + return x or [] + + opts = [strtolist(x) for x in opts] + self._android_main_ebook_dir = opts[0] + self._android_card_ebook_dir = opts[1] def get_main_ebook_dir(self, for_upload=False): - dirs = self.EBOOK_DIR_MAIN + dirs = self._android_main_ebook_dir if not for_upload: def aldiko_tweak(x): return 'eBooks' if x == 'eBooks/import' else x - if isinstance(dirs, basestring): - dirs = [dirs] dirs = list(map(aldiko_tweak, dirs)) return dirs + def get_carda_ebook_dir(self, for_upload=False): + if not for_upload: + return '' + return self._android_card_ebook_dir + + def get_cardb_ebook_dir(self, for_upload=False): + return self.get_carda_ebook_dir() + def windows_sort_drives(self, drives): try: vid, pid, bcd = self.device_being_opened[:3] @@ -271,7 +288,8 @@ class ANDROID(USBMS): proxy = cls._configProxy() proxy['format_map'] = ['mobi', 'azw', 'azw1', 'azw4', 'pdf'] proxy['use_subdirs'] = False - proxy['extra_customization'] = ','.join(['kindle']+cls.EBOOK_DIR_MAIN) + proxy['extra_customization'] = [ + ','.join(['kindle']+cls.EBOOK_DIR_MAIN), ''] @classmethod def configure_for_generic_epub_app(cls): diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 4d4b198de0..025a7e2d95 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -991,24 +991,28 @@ class Device(DeviceConfig, DevicePlugin): elif on_card and on_card not in ('carda', 'cardb'): raise DeviceError(_('Selected slot: %s is not supported.') % on_card) - if on_card == 'carda': - path = os.path.join(self._card_a_prefix, - *(self.get_carda_ebook_dir(for_upload=True).split('/'))) - elif on_card == 'cardb': - path = os.path.join(self._card_b_prefix, - *(self.EBOOK_DIR_CARD_B.split('/'))) - else: - candidates = self.get_main_ebook_dir(for_upload=True) + def get_dest_dir(prefix, candidates): if isinstance(candidates, basestring): candidates = [candidates] + if not candidates: + candidates = [''] candidates = [ - ((os.path.join(self._main_prefix, *(x.split('/')))) if x else - self._main_prefix) for x - in candidates] + ((os.path.join(prefix, *(x.split('/')))) if x else prefix) + for x in candidates] existing = [x for x in candidates if os.path.exists(x)] if not existing: - existing = candidates[:1] - path = existing[0] + existing = candidates + return existing[0] + + if on_card == 'carda': + candidates = self.get_carda_ebook_dir(for_upload=True) + path = get_dest_dir(self._carda_prefix, candidates) + elif on_card == 'cardb': + candidates = self.get_cardb_ebook_dir(for_upload=True) + path = get_dest_dir(self._cardb_prefix, candidates) + else: + candidates = self.get_main_ebook_dir(for_upload=True) + path = get_dest_dir(self._main_prefix, candidates) def get_size(obj): path = getattr(obj, 'name', obj) From f9dec96a168cec5f8cce4d32e85a45d78ba31a0c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 13:08:34 +0530 Subject: [PATCH 57/77] ... --- src/calibre/gui2/device_drivers/configwidget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py index b47a80b6ad..9624efac96 100644 --- a/src/calibre/gui2/device_drivers/configwidget.py +++ b/src/calibre/gui2/device_drivers/configwidget.py @@ -89,6 +89,7 @@ class ConfigWidget(QWidget, Ui_ConfigWidget): l.setBuddy(self.opt_extra_customization[i]) l.setWordWrap(True) self.opt_extra_customization[i].setText(settings.extra_customization[i]) + self.opt_extra_customization[i].setCursorPosition(0) self.extra_layout.addWidget(l, row_func(i, 0), col_func(i)) self.extra_layout.addWidget(self.opt_extra_customization[i], row_func(i, 1), col_func(i)) @@ -101,6 +102,7 @@ class ConfigWidget(QWidget, Ui_ConfigWidget): l.setWordWrap(True) if settings.extra_customization: self.opt_extra_customization.setText(settings.extra_customization) + self.opt_extra_customization.setCursorPosition(0) self.opt_extra_customization.setCursorPosition(0) self.extra_layout.addWidget(l, 0, 0) self.extra_layout.addWidget(self.opt_extra_customization, 1, 0) From bc7de03717b2308916fe988dd38108f1e3ad81f7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 13:44:24 +0530 Subject: [PATCH 58/77] Fix getting device GUI name in the GUI to not use the classmethod, as some drivers (MTP) require it to be an instance method. --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 98e42f4178..8eb4df3fbd 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -977,7 +977,7 @@ class DeviceMixin(object): # {{{ self.set_default_thumbnail(\ self.device_manager.device.THUMBNAIL_HEIGHT) self.status_bar.show_message(_('Device: ')+\ - self.device_manager.device.__class__.get_gui_name()+\ + self.device_manager.device.get_gui_name()+\ _(' detected.'), 3000) self.device_connected = device_kind self.library_view.set_device_connected(self.device_connected) From 4016e852d87da755e3664c778523fc49ece1f031 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 13:58:43 +0530 Subject: [PATCH 59/77] ... --- src/calibre/devices/mtp/driver.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 8f8f4d119b..31679062d2 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -32,6 +32,7 @@ class MTP_DEVICE(BASE): CAN_SET_METADATA = [] BACKLOADING_ERROR_MESSAGE = None MANAGES_DEVICE_PRESENCE = True + FORMATS = ['epub', 'azw3', 'mobi', 'pdf'] def open(self, devices, library_uuid): self.current_library_uuid = library_uuid @@ -225,6 +226,8 @@ class MTP_DEVICE(BASE): return ans # }}} + # Sending files to the device {{{ + def create_upload_path(self, path, mdata, fname): from calibre.devices import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize @@ -237,6 +240,19 @@ class MTP_DEVICE(BASE): ) return tuple(x.lower() for x in filepath.split('/')) + # }}} + + # Settings {{{ + @classmethod + def settings(self): + # TODO: Implement this + class Opts(object): + def __init__(s): + s.format_map = self.FORMATS + return Opts() + + # }}} + if __name__ == '__main__': dev = MTP_DEVICE(None) dev.startup() From e7e3b86573c7c40c9dce2479c53335c3a1945ffa Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 14:40:31 +0530 Subject: [PATCH 60/77] Refactor out sanity_check as a utility method and re-organize utility methods into their own module --- src/calibre/devices/__init__.py | 108 +------------------- src/calibre/devices/mtp/base.py | 2 +- src/calibre/devices/mtp/driver.py | 14 ++- src/calibre/devices/usbms/device.py | 34 +------ src/calibre/devices/usbms/driver.py | 2 +- src/calibre/devices/utils.py | 148 ++++++++++++++++++++++++++++ 6 files changed, 168 insertions(+), 140 deletions(-) create mode 100644 src/calibre/devices/utils.py diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py index 89d0e4e026..2c1d628566 100644 --- a/src/calibre/devices/__init__.py +++ b/src/calibre/devices/__init__.py @@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal ' Device drivers. ''' -import sys, time, pprint, operator, re, os +import sys, time, pprint, operator from functools import partial from StringIO import StringIO @@ -27,112 +27,6 @@ def strftime(epoch, zone=time.gmtime): src[2] = INVERSE_MONTH_MAP[int(src[2])] return ' '.join(src) -def build_template_regexp(template): - from calibre import prints - - def replfunc(match, seen=None): - v = match.group(1) - if v in ['authors', 'author_sort']: - v = 'author' - if v in ('title', 'series', 'series_index', 'isbn', 'author'): - if v not in seen: - seen.add(v) - return '(?P<' + v + '>.+?)' - return '(.+?)' - s = set() - f = partial(replfunc, seen=s) - - try: - template = template.rpartition('/')[2] - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') - except: - prints(u'Failed to parse template: %r'%template) - template = u'{title} - {authors}' - return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') - -def create_upload_path(mdata, fname, template, sanitize, - prefix_path='', - path_type=os.path, - maxlen=250, - use_subdirs=True, - news_in_folder=True, - filename_callback=lambda x, y:x, - sanitize_path_components=lambda x: x - ): - from calibre.library.save_to_disk import get_components, config - from calibre.utils.filenames import shorten_components_to - - special_tag = None - if mdata.tags: - for t in mdata.tags: - if t.startswith(_('News')) or t.startswith('/'): - special_tag = t - break - - if mdata.tags and _('News') in mdata.tags: - try: - p = mdata.pubdate - date = (p.year, p.month, p.day) - except: - today = time.localtime() - date = (today[0], today[1], today[2]) - template = u"{title}_%d-%d-%d" % date - - fname = sanitize(fname) - ext = path_type.splitext(fname)[1] - - opts = config().parse() - if not isinstance(template, unicode): - template = template.decode('utf-8') - app_id = str(getattr(mdata, 'application_id', '')) - id_ = mdata.get('id', fname) - extra_components = get_components(template, mdata, id_, - timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1) - if not extra_components: - extra_components.append(sanitize(filename_callback(fname, - mdata))) - else: - extra_components[-1] = sanitize(filename_callback(extra_components[-1]+ext, mdata)) - - if extra_components[-1] and extra_components[-1][0] in ('.', '_'): - extra_components[-1] = 'x' + extra_components[-1][1:] - - if special_tag is not None: - name = extra_components[-1] - extra_components = [] - tag = special_tag - if tag.startswith(_('News')): - if news_in_folder: - extra_components.append('News') - else: - for c in tag.split('/'): - c = sanitize(c) - if not c: continue - extra_components.append(c) - extra_components.append(name) - - if not use_subdirs: - extra_components = extra_components[-1:] - - def remove_trailing_periods(x): - ans = x - while ans.endswith('.'): - ans = ans[:-1].strip() - if not ans: - ans = 'x' - return ans - - extra_components = list(map(remove_trailing_periods, extra_components)) - components = shorten_components_to(maxlen - len(prefix_path), extra_components) - components = sanitize_path_components(components) - if prefix_path: - filepath = path_type.join(prefix_path, *components) - else: - filepath = path_type.join(*components) - - return filepath - - def get_connected_device(): from calibre.customize.ui import device_plugins from calibre.devices.scanner import DeviceScanner diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 516b68ae1e..a5922a4d21 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -55,7 +55,7 @@ class MTPDeviceBase(DevicePlugin): return False def build_template_regexp(self): - from calibre.devices import build_template_regexp + from calibre.devices.utils import build_template_regexp return build_template_regexp(self.save_template) @property diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 31679062d2..2d414e7e5a 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -33,9 +33,11 @@ class MTP_DEVICE(BASE): BACKLOADING_ERROR_MESSAGE = None MANAGES_DEVICE_PRESENCE = True FORMATS = ['epub', 'azw3', 'mobi', 'pdf'] + DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE' def open(self, devices, library_uuid): self.current_library_uuid = library_uuid + self.plugboards = self.plugboard_func = None BASE.open(self, devices, library_uuid) # Device information {{{ @@ -228,8 +230,12 @@ class MTP_DEVICE(BASE): # Sending files to the device {{{ + def set_plugboards(self, plugboards, pb_func): + self.plugboards = plugboards + self.plugboard_func = pb_func + def create_upload_path(self, path, mdata, fname): - from calibre.devices import create_upload_path + from calibre.devices.utils import create_upload_path from calibre.utils.filenames import ascii_filename as sanitize filepath = create_upload_path(mdata, fname, self.save_template, sanitize, prefix_path=path, @@ -240,6 +246,12 @@ class MTP_DEVICE(BASE): ) return tuple(x.lower() for x in filepath.split('/')) + def upload_books(self, files, names, on_card=None, end_session=True, + metadata=None): + from calibre.devices.utils import sanity_check + sanity_check(on_card, files, self.card_prefix(), self.free_space()) + raise NotImplementedError() + # }}} # Settings {{{ diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 025a7e2d95..795a22888f 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -15,8 +15,7 @@ import os, subprocess, time, re, sys, glob from itertools import repeat from calibre.devices.interface import DevicePlugin -from calibre.devices.errors import (DeviceError, FreeSpaceError, - WrongDestinationError) +from calibre.devices.errors import DeviceError from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.constants import iswindows, islinux, isosx, isfreebsd, plugins from calibre.utils.filenames import ascii_filename as sanitize @@ -976,20 +975,8 @@ class Device(DeviceConfig, DevicePlugin): return self.EBOOK_DIR_CARD_A def _sanity_check(self, on_card, files): - if on_card == 'carda' and not self._card_a_prefix: - raise WrongDestinationError(_( - 'The reader has no storage card %s. You may have changed ' - 'the default send to device action. Right click on the send ' - 'to device button and reset the default action to be ' - '"Send to main memory".')%'A') - elif on_card == 'cardb' and not self._card_b_prefix: - raise WrongDestinationError(_( - 'The reader has no storage card %s. You may have changed ' - 'the default send to device action. Right click on the send ' - 'to device button and reset the default action to be ' - '"Send to main memory".')%'B') - elif on_card and on_card not in ('carda', 'cardb'): - raise DeviceError(_('Selected slot: %s is not supported.') % on_card) + from calibre.devices.utils import sanity_check + sanity_check(on_card, files, self.card_prefix(), self.free_space()) def get_dest_dir(prefix, candidates): if isinstance(candidates, basestring): @@ -1014,19 +1001,6 @@ class Device(DeviceConfig, DevicePlugin): candidates = self.get_main_ebook_dir(for_upload=True) path = get_dest_dir(self._main_prefix, candidates) - def get_size(obj): - path = getattr(obj, 'name', obj) - return os.path.getsize(path) - - sizes = [get_size(f) for f in files] - size = sum(sizes) - - if not on_card and size > self.free_space()[0] - 2*1024*1024: - raise FreeSpaceError(_("There is insufficient free space in main memory")) - if on_card == 'carda' and size > self.free_space()[1] - 1024*1024: - raise FreeSpaceError(_("There is insufficient free space on the storage card")) - if on_card == 'cardb' and size > self.free_space()[2] - 1024*1024: - raise FreeSpaceError(_("There is insufficient free space on the storage card")) return path def filename_callback(self, default, mi): @@ -1056,7 +1030,7 @@ class Device(DeviceConfig, DevicePlugin): pass def create_upload_path(self, path, mdata, fname, create_dirs=True): - from calibre.devices import create_upload_path + from calibre.devices.utils import create_upload_path settings = self.settings() filepath = create_upload_path(mdata, fname, self.save_template(), sanitize, prefix_path=os.path.abspath(path), diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index f6c7556fd8..5f6bdd9402 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -404,7 +404,7 @@ class USBMS(CLI, Device): @classmethod def build_template_regexp(cls): - from calibre.devices import build_template_regexp + from calibre.devices.utils import build_template_regexp return build_template_regexp(cls.save_template()) @classmethod diff --git a/src/calibre/devices/utils.py b/src/calibre/devices/utils.py new file mode 100644 index 0000000000..114e7e4e13 --- /dev/null +++ b/src/calibre/devices/utils.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, time, re +from functools import partial + +from calibre.devices.errors import DeviceError, WrongDestinationError, FreeSpaceError + +def sanity_check(on_card, files, card_prefixes, free_space): + if on_card == 'carda' and not card_prefixes[0]: + raise WrongDestinationError(_( + 'The reader has no storage card %s. You may have changed ' + 'the default send to device action. Right click on the send ' + 'to device button and reset the default action to be ' + '"Send to main memory".')%'A') + elif on_card == 'cardb' and not card_prefixes[1]: + raise WrongDestinationError(_( + 'The reader has no storage card %s. You may have changed ' + 'the default send to device action. Right click on the send ' + 'to device button and reset the default action to be ' + '"Send to main memory".')%'B') + elif on_card and on_card not in ('carda', 'cardb'): + raise DeviceError(_('Selected slot: %s is not supported.') % on_card) + + size = 0 + for f in files: + size += os.path.getsize(getattr(f, 'name', f)) + + if not on_card and size > free_space[0] - 2*1024*1024: + raise FreeSpaceError(_("There is insufficient free space in main memory")) + if on_card == 'carda' and size > free_space[1] - 1024*1024: + raise FreeSpaceError(_("There is insufficient free space on the storage card")) + if on_card == 'cardb' and size > free_space[2] - 1024*1024: + raise FreeSpaceError(_("There is insufficient free space on the storage card")) + +def build_template_regexp(template): + from calibre import prints + + def replfunc(match, seen=None): + v = match.group(1) + if v in ['authors', 'author_sort']: + v = 'author' + if v in ('title', 'series', 'series_index', 'isbn', 'author'): + if v not in seen: + seen.add(v) + return '(?P<' + v + '>.+?)' + return '(.+?)' + s = set() + f = partial(replfunc, seen=s) + + try: + template = template.rpartition('/')[2] + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + except: + prints(u'Failed to parse template: %r'%template) + template = u'{title} - {authors}' + return re.compile(re.sub('{([^}]*)}', f, template) + '([_\d]*$)') + +def create_upload_path(mdata, fname, template, sanitize, + prefix_path='', + path_type=os.path, + maxlen=250, + use_subdirs=True, + news_in_folder=True, + filename_callback=lambda x, y:x, + sanitize_path_components=lambda x: x + ): + from calibre.library.save_to_disk import get_components, config + from calibre.utils.filenames import shorten_components_to + + special_tag = None + if mdata.tags: + for t in mdata.tags: + if t.startswith(_('News')) or t.startswith('/'): + special_tag = t + break + + if mdata.tags and _('News') in mdata.tags: + try: + p = mdata.pubdate + date = (p.year, p.month, p.day) + except: + today = time.localtime() + date = (today[0], today[1], today[2]) + template = u"{title}_%d-%d-%d" % date + + fname = sanitize(fname) + ext = path_type.splitext(fname)[1] + + opts = config().parse() + if not isinstance(template, unicode): + template = template.decode('utf-8') + app_id = str(getattr(mdata, 'application_id', '')) + id_ = mdata.get('id', fname) + extra_components = get_components(template, mdata, id_, + timefmt=opts.send_timefmt, length=maxlen-len(app_id)-1) + if not extra_components: + extra_components.append(sanitize(filename_callback(fname, + mdata))) + else: + extra_components[-1] = sanitize(filename_callback(extra_components[-1]+ext, mdata)) + + if extra_components[-1] and extra_components[-1][0] in ('.', '_'): + extra_components[-1] = 'x' + extra_components[-1][1:] + + if special_tag is not None: + name = extra_components[-1] + extra_components = [] + tag = special_tag + if tag.startswith(_('News')): + if news_in_folder: + extra_components.append('News') + else: + for c in tag.split('/'): + c = sanitize(c) + if not c: continue + extra_components.append(c) + extra_components.append(name) + + if not use_subdirs: + extra_components = extra_components[-1:] + + def remove_trailing_periods(x): + ans = x + while ans.endswith('.'): + ans = ans[:-1].strip() + if not ans: + ans = 'x' + return ans + + extra_components = list(map(remove_trailing_periods, extra_components)) + components = shorten_components_to(maxlen - len(prefix_path), extra_components) + components = sanitize_path_components(components) + if prefix_path: + filepath = path_type.join(prefix_path, *components) + else: + filepath = path_type.join(*components) + + return filepath + + + From 598f3bd17c3caa3200b935edea12811437bae661 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 14:43:32 +0530 Subject: [PATCH 61/77] ... --- src/calibre/devices/mtp/driver.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 2d414e7e5a..2706cba32c 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -77,12 +77,7 @@ class MTP_DEVICE(BASE): return tuple( list(dinfo) + [self.driveinfo] ) def card_prefix(self, end_session=True): - ans = [None, None] - if self._carda_id is not None: - ans[0] = self.filesystem_cache.storage(self._carda_id).storage_prefix - if self._cardb_id is not None: - ans[1] = self.filesystem_cache.storage(self._cardb_id).storage_prefix - return tuple(ans) + return (self._carda_id, self._cardb_id) def set_driveinfo_name(self, location_code, name): sid = {'main':self._main_id, 'A':self._carda_id, From 66d01629c377b171c5261b9dca0ad90ee5686eed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 14:45:59 +0530 Subject: [PATCH 62/77] ... --- src/calibre/devices/mtp/driver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 2706cba32c..8bc2481205 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -35,9 +35,12 @@ class MTP_DEVICE(BASE): FORMATS = ['epub', 'azw3', 'mobi', 'pdf'] DEVICE_PLUGBOARD_NAME = 'MTP_DEVICE' + def __init__(self, *args, **kwargs): + BASE.__init__(self, *args, **kwargs) + self.plugboards = self.plugboard_func = None + def open(self, devices, library_uuid): self.current_library_uuid = library_uuid - self.plugboards = self.plugboard_func = None BASE.open(self, devices, library_uuid) # Device information {{{ From 0b02c1f593a3f0c6abc6f3660cce845f44b10395 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 14:59:31 +0530 Subject: [PATCH 63/77] ... --- src/calibre/devices/mtp/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index a5922a4d21..26523362ed 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -25,7 +25,7 @@ def synchronous(func): return synchronizer class MTPDeviceBase(DevicePlugin): - name = 'SmartDevice App Interface' + name = 'MTP Device Interface' gui_name = _('MTP Device') icon = I('devices/galaxy_s3.png') description = _('Communicate with MTP devices') From 7a1d05199e7813d17ce4f59f38f5ed0fa96f6870 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 15:01:35 +0530 Subject: [PATCH 64/77] ... --- src/calibre/devices/mtp/base.py | 5 +++-- src/calibre/devices/mtp/driver.py | 9 +++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index 26523362ed..bad027baa1 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -45,8 +45,9 @@ class MTPDeviceBase(DevicePlugin): def set_progress_reporter(self, report_progress): self.report_progress = report_progress - def get_gui_name(self): - return self.current_friendly_name or self.name + @classmethod + def get_gui_name(cls): + return getattr(cls, 'current_friendly_name', cls.gui_name) def is_usb_connected(self, devices_on_system, debug=False, only_presence=False): diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 8bc2481205..f36b068a30 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -9,6 +9,7 @@ __docformat__ = 'restructuredtext en' import json, traceback, posixpath, importlib, os from io import BytesIO +from itertools import izip from calibre import prints from calibre.constants import iswindows, numeric_version @@ -244,10 +245,18 @@ class MTP_DEVICE(BASE): ) return tuple(x.lower() for x in filepath.split('/')) + def prefix_for_location(self, on_card): + # TODO: Implement this + return 'calibre' + def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space()) + prefix = self.prefix_for_location(on_card) + for infile, fname, mi in izip(files, names, metadata): + path = self.create_upload_path(prefix, mi, fname) + print (1111111, path) raise NotImplementedError() # }}} From 86726f44d8fcc25ee9a18e0991fedf7871f28c99 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 15:02:05 +0530 Subject: [PATCH 65/77] ... --- src/calibre/devices/mtp/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index f36b068a30..21419960ae 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -243,7 +243,7 @@ class MTP_DEVICE(BASE): use_subdirs = True, news_in_folder = self.NEWS_IN_FOLDER, ) - return tuple(x.lower() for x in filepath.split('/')) + return tuple(x for x in filepath.split('/')) def prefix_for_location(self, on_card): # TODO: Implement this From 4d7ed1b4c6e696d5888dd1fef40aec13c9be1fe4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 17:23:12 +0530 Subject: [PATCH 66/77] ... --- src/calibre/devices/mtp/filesystem_cache.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index 216e06031f..da0445962b 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -80,6 +80,10 @@ class FileOrFolder(object): __str__ = __repr__ __unicode__ = __repr__ + @property + def empty(self): + return not self.files and not self.folders + @property def id_map(self): return self.fs_cache().id_map @@ -217,6 +221,8 @@ class FilesystemCache(object): def iterebooks(self, storage_id): for x in self.id_map.itervalues(): if x.storage_id == storage_id and x.is_ebook: + if x.parent_id == storage_id and x.name.lower().endswith('.txt'): + continue # Ignore .txt files in the root yield x def resolve_mtp_id_path(self, path): From 92c46d2dedc0b335d6b53b17c7fa558672b4859f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 17:36:45 +0530 Subject: [PATCH 67/77] MTP: Sending books to device and deleting books from device implemented --- src/calibre/devices/mtp/books.py | 27 ++++++ src/calibre/devices/mtp/driver.py | 108 +++++++++++++++++++++- src/calibre/devices/mtp/unix/driver.py | 1 + src/calibre/devices/mtp/windows/driver.py | 1 + src/calibre/gui2/device.py | 8 +- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/calibre/devices/mtp/books.py b/src/calibre/devices/mtp/books.py index 73e483f19e..a72fc1f84e 100644 --- a/src/calibre/devices/mtp/books.py +++ b/src/calibre/devices/mtp/books.py @@ -22,6 +22,22 @@ class BookList(BL): def supports_collections(self): return False + def add_book(self, book, replace_metadata=True): + try: + b = self.index(book) + except (ValueError, IndexError): + b = None + if b is None: + self.append(book) + return book + if replace_metadata: + self[b].smart_update(book, replace_metadata=True) + return self[b] + return None + + def remove_book(self, book): + self.remove(book) + class Book(Metadata): def __init__(self, storage_id, lpath, other=None): @@ -36,6 +52,17 @@ class Book(Metadata): return (self.storage_id == mtp_file.storage_id and self.mtp_relpath == mtp_file.mtp_relpath) + def __eq__(self, other): + return (isinstance(other, self.__class__) and (self.storage_id == + other.storage_id and self.mtp_relpath == other.mtp_relpath)) + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash((self.storage_id, self.mtp_relpath)) + + class JSONCodec(JsonCodec): pass diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 21419960ae..68539f334b 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -191,6 +191,7 @@ class MTP_DEVICE(BASE): self.put_file(storage, self.METADATA_CACHE, stream, size) def sync_booklists(self, booklists, end_session=True): + debug('sync_booklists() called') for bl in booklists: if getattr(bl, 'storage_id', None) is None: continue @@ -198,6 +199,7 @@ class MTP_DEVICE(BASE): if storage is None: continue self.write_metadata_cache(storage, bl) + debug('sync_booklists() ended') # }}} @@ -249,15 +251,117 @@ class MTP_DEVICE(BASE): # TODO: Implement this return 'calibre' + def ensure_parent(self, storage, path): + parent = storage + pos = list(path)[:-1] + while pos: + name = pos[0] + pos = pos[1:] + parent = self.create_folder(parent, name) + return parent + def upload_books(self, files, names, on_card=None, end_session=True, metadata=None): + debug('upload_books() called') from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space()) prefix = self.prefix_for_location(on_card) + sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(on_card, + self._main_id) + bl_idx = {'carda':1, 'cardb':2}.get(on_card, 0) + storage = self.filesystem_cache.storage(sid) + + ans = [] + self.report_progress(0, _('Transferring books to device...')) + i, total = 0, len(files) + for infile, fname, mi in izip(files, names, metadata): path = self.create_upload_path(prefix, mi, fname) - print (1111111, path) - raise NotImplementedError() + parent = self.ensure_parent(storage, path) + if hasattr(infile, 'read'): + pos = infile.tell() + infile.seek(0, 2) + sz = infile.tell() + infile.seek(pos) + stream = infile + close = False + else: + sz = os.path.getsize(infile) + stream = lopen(infile, 'rb') + close = True + try: + mtp_file = self.put_file(parent, path[-1], stream, sz) + finally: + if close: + stream.close() + ans.append((mtp_file, bl_idx)) + i += 1 + self.report_progress(i/total, _('Transferred %s to device')%mi.title) + + self.report_progress(1, _('Transfer to device finished...')) + debug('upload_books() ended') + return ans + + def add_books_to_metadata(self, mtp_files, metadata, booklists): + debug('add_books_to_metadata() called') + from calibre.devices.mtp.books import Book + + i, total = 0, len(mtp_files) + self.report_progress(0, _('Adding books to device metadata listing...')) + for x, mi in izip(mtp_files, metadata): + mtp_file, bl_idx = x + bl = booklists[bl_idx] + book = Book(mtp_file.storage_id, '/'.join(mtp_file.mtp_relpath), + other=mi) + book = bl.add_book(book, replace_metadata=True) + if book is not None: + book.size = mtp_file.size + book.datetime = mtp_file.last_modified.timetuple() + book.path = mtp_file.mtp_id_path + i += 1 + self.report_progress(i/total, _('Added %s')%mi.title) + + self.report_progress(1, _('Adding complete')) + debug('add_books_to_metadata() ended') + + # }}} + + # Removing books from the device {{{ + def recursive_delete(self, obj): + parent = self.delete_file_or_folder(obj) + if parent.empty and parent.can_delete and not parent.is_system: + try: + self.recursive_delete(parent) + except: + prints('Failed to delete parent: %s, ignoring'%( + '/'.join(parent.full_path))) + + def delete_books(self, paths, end_session=True): + self.report_progress(0, _('Deleting books from device...')) + + for i, path in enumerate(paths): + f = self.filesystem_cache.resolve_mtp_id_path(path) + self.recursive_delete(f) + self.report_progress((i+1) / float(len(paths)), + _('Deleted %s')%path) + self.report_progress(1, _('All books deleted')) + + def remove_books_from_metadata(self, paths, booklists): + self.report_progress(0, _('Removing books from metadata')) + class NextPath(Exception): pass + + for i, path in enumerate(paths): + try: + for bl in booklists: + for book in bl: + if book.path == path: + bl.remove_book(book) + raise NextPath('') + except NextPath: + pass + self.report_progress((i+1)/len(paths), _('Removed %s')%path) + + self.report_progress(1, _('All books removed')) # }}} diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 2f215f6353..b59ec22110 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -306,6 +306,7 @@ class MTP_DEVICE(MTPDeviceBase): raise DeviceError('Failed to delete %s with error: %s'% (obj.full_path, self.format_errorstack(errs))) parent.remove_child(obj) + return parent def develop(): from calibre.devices.scanner import DeviceScanner diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 7c15797ef6..63fedfaf66 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -338,6 +338,7 @@ class MTP_DEVICE(MTPDeviceBase): parent = obj.parent self.dev.delete_object(obj.object_id) parent.remove_child(obj) + return parent @same_thread def put_file(self, parent, name, stream, size, callback=None, replace=True): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8eb4df3fbd..4cc4c0fb5f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1495,8 +1495,12 @@ class DeviceMixin(object): # {{{ self.device_job_exception(job) return - self.device_manager.add_books_to_metadata(job.result, - metadata, self.booklists()) + try: + self.device_manager.add_books_to_metadata(job.result, + metadata, self.booklists()) + except: + traceback.print_exc() + raise books_to_be_deleted = [] if memory and memory[1]: From 4fcbf630e59d8fd5cb50857f687b89f3c5d6bcf2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 17:56:15 +0530 Subject: [PATCH 68/77] MTP: Print last modified date when dumping filesystem --- src/calibre/devices/mtp/driver.py | 2 +- src/calibre/devices/mtp/filesystem_cache.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 68539f334b..d716d15de5 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -389,7 +389,7 @@ if __name__ == '__main__': raise ValueError('Failed to detect MTP device') dev.set_progress_reporter(prints) dev.open(cd, None) - dev.books() + dev.filesystem_cache.dump() finally: dev.shutdown() diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index da0445962b..d8c5170e59 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -40,6 +40,7 @@ class FileOrFolder(object): self.last_modified = datetime.fromtimestamp(md, local_tz) except: self.last_modified = datetime.fromtimestamp(0, local_tz) + self.last_mod_string = self.last_modified.strftime('%Y/%m/%d %H:%M') self.last_modified = as_utc(self.last_modified) if self.storage_id not in self.all_storage_ids: @@ -74,8 +75,8 @@ class FileOrFolder(object): datum = 'size=%s'%(self.size) if self.is_folder: datum = 'children=%s'%(len(self.files) + len(self.folders)) - return '%s(id=%s, storage_id=%s, %s, path=%s)'%(name, self.object_id, - self.storage_id, datum, path) + return '%s(id=%s, storage_id=%s, %s, path=%s, modified=%s)'%(name, self.object_id, + self.storage_id, datum, path, self.last_mod_string) __str__ = __repr__ __unicode__ = __repr__ @@ -127,6 +128,7 @@ class FileOrFolder(object): c = '+' if self.is_folder else '-' data = ('%s children'%(sum(map(len, (self.files, self.folders)))) if self.is_folder else human_readable(self.size)) + data += ' modified=%s'%self.last_mod_string line = '%s%s %s [id:%s %s]'%(prefix, c, self.name, self.object_id, data) prints(line, file=out) for c in (self.folders, self.files): From 75f127168130f9a452aade714c8754d041b65b82 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 19:57:39 +0530 Subject: [PATCH 69/77] Fix #1045046 (Houston Chronicle news fetch failing) --- recipes/houston_chronicle.recipe | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/recipes/houston_chronicle.recipe b/recipes/houston_chronicle.recipe index b8171467ec..639d5c2042 100644 --- a/recipes/houston_chronicle.recipe +++ b/recipes/houston_chronicle.recipe @@ -7,18 +7,19 @@ class HoustonChronicle(BasicNewsRecipe): title = u'The Houston Chronicle' description = 'News from Houston, Texas' - __author__ = 'Kovid Goyal' + __author__ = 'Kovid Goyal' language = 'en' timefmt = ' [%a, %d %b, %Y]' no_stylesheets = True use_embedded_content = False remove_attributes = ['style'] + auto_cleanup = True oldest_article = 2.0 - keep_only_tags = {'class':lambda x: x and ('hst-articletitle' in x or - 'hst-articletext' in x or 'hst-galleryitem' in x)} - remove_attributes = ['xmlns'] + #keep_only_tags = {'class':lambda x: x and ('hst-articletitle' in x or + #'hst-articletext' in x or 'hst-galleryitem' in x)} + #remove_attributes = ['xmlns'] feeds = [ ('News', "http://www.chron.com/rss/feed/News-270.php"), @@ -37,3 +38,4 @@ class HoustonChronicle(BasicNewsRecipe): ] + From f36feac5baae59d291c1f7a1e19de17795fc70eb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 21:38:08 +0530 Subject: [PATCH 70/77] ... --- session.vim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/session.vim b/session.vim index 1a94d6bf07..4b9dcb72c1 100644 --- a/session.vim +++ b/session.vim @@ -13,6 +13,8 @@ let g:syntastic_cpp_include_dirs = [ \] let g:syntastic_c_include_dirs = g:syntastic_cpp_include_dirs +set wildignore+=resources/viewer/mathjax/** + fun! CalibreLog() " Setup buffers to edit the calibre changelog and version info prior to " making a release. From f07002fdd226fc735e93a6b5979ed0849e1cd89f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 3 Sep 2012 21:42:31 +0530 Subject: [PATCH 71/77] MTP: Fix last modified date not getting read correctly on windows --- setup/extensions.py | 2 +- src/calibre/devices/mtp/filesystem_cache.py | 5 ++++- .../mtp/windows/content_enumeration.cpp | 12 +++++++++-- src/calibre/devices/mtp/windows/remote.py | 20 +++++++++---------- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/setup/extensions.py b/setup/extensions.py index f4ed22687b..f7d40ca72c 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -187,7 +187,7 @@ if iswindows: headers=[ 'calibre/devices/mtp/windows/global.h', ], - libraries=['ole32', 'portabledeviceguids', 'user32'], + libraries=['ole32', 'oleaut32', 'portabledeviceguids', 'user32'], # needs_ddk=True, cflags=['/X'] ), diff --git a/src/calibre/devices/mtp/filesystem_cache.py b/src/calibre/devices/mtp/filesystem_cache.py index d8c5170e59..8f4d20ae18 100644 --- a/src/calibre/devices/mtp/filesystem_cache.py +++ b/src/calibre/devices/mtp/filesystem_cache.py @@ -37,7 +37,10 @@ class FileOrFolder(object): self.size = entry.get('size', 0) md = entry.get('modified', 0) try: - self.last_modified = datetime.fromtimestamp(md, local_tz) + if isinstance(md, tuple): + self.last_modified = datetime(*(list(md)+[local_tz])) + else: + self.last_modified = datetime.fromtimestamp(md, local_tz) except: self.last_modified = datetime.fromtimestamp(0, local_tz) self.last_mod_string = self.last_modified.strftime('%Y/%m/%d %H:%M') diff --git a/src/calibre/devices/mtp/windows/content_enumeration.cpp b/src/calibre/devices/mtp/windows/content_enumeration.cpp index 7186bbdcdb..580f77f9b0 100644 --- a/src/calibre/devices/mtp/windows/content_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/content_enumeration.cpp @@ -84,11 +84,19 @@ static void set_size_property(PyObject *dict, REFPROPERTYKEY key, const char *py static void set_date_property(PyObject *dict, REFPROPERTYKEY key, const char *pykey, IPortableDeviceValues *properties) { FLOAT val = 0; + SYSTEMTIME st; + unsigned int microseconds; PyObject *t; if (SUCCEEDED(properties->GetFloatValue(key, &val))) { - t = Py_BuildValue("d", (double)val); - if (t != NULL) { PyDict_SetItemString(dict, pykey, t); Py_DECREF(t); } + if (VariantTimeToSystemTime(val, &st)) { + microseconds = 1000 * st.wMilliseconds; + t = Py_BuildValue("H H H H H H I", (unsigned short)st.wYear, + (unsigned short)st.wMonth, (unsigned short)st.wDay, + (unsigned short)st.wHour, (unsigned short)st.wMinute, + (unsigned short)st.wSecond, microseconds); + if (t != NULL) { PyDict_SetItemString(dict, pykey, t); Py_DECREF(t); } + } } } diff --git a/src/calibre/devices/mtp/windows/remote.py b/src/calibre/devices/mtp/windows/remote.py index cbc23978d2..f1dfa92767 100644 --- a/src/calibre/devices/mtp/windows/remote.py +++ b/src/calibre/devices/mtp/windows/remote.py @@ -54,9 +54,9 @@ def main(): plugins._plugins['wpd'] = (wpd, '') sys.path.pop(0) - from calibre.devices.mtp.test import run - run() - return + # from calibre.devices.mtp.test import run + # run() + # return from calibre.devices.scanner import win_scanner from calibre.devices.mtp.windows.driver import MTP_DEVICE @@ -81,13 +81,13 @@ def main(): # print ('Fetching file: oFF (198214 bytes)') # stream = dev.get_file('oFF') # print ("Fetched size: ", stream.tell()) - size = 4 - stream = io.BytesIO(b'a'*size) - name = 'zzz-test-file.txt' - stream.seek(0) - f = dev.put_file(dev.filesystem_cache.entries[0], name, stream, size) - print ('Put file:', f) - # dev.filesystem_cache.dump() + # size = 4 + # stream = io.BytesIO(b'a'*size) + # name = 'zzz-test-file.txt' + # stream.seek(0) + # f = dev.put_file(dev.filesystem_cache.entries[0], name, stream, size) + # print ('Put file:', f) + dev.filesystem_cache.dump() finally: dev.shutdown() From 7fb0ef82561cc8f267f66c7ac3efac9481842346 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Sep 2012 09:44:37 +0530 Subject: [PATCH 72/77] ... --- src/calibre/devices/mtp/windows/wpd.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/windows/wpd.cpp b/src/calibre/devices/mtp/windows/wpd.cpp index 15cdd51e22..867ce6bcee 100644 --- a/src/calibre/devices/mtp/windows/wpd.cpp +++ b/src/calibre/devices/mtp/windows/wpd.cpp @@ -120,14 +120,14 @@ wpd_enumerate_devices(PyObject *self, PyObject *args) { hresult_set_exc("Failed to get list of portable devices", hr); } + Py_BEGIN_ALLOW_THREADS; for (i = 0; i < num_of_devices; i++) { - Py_BEGIN_ALLOW_THREADS; CoTaskMemFree(pnp_device_ids[i]); - Py_END_ALLOW_THREADS; pnp_device_ids[i] = NULL; } free(pnp_device_ids); pnp_device_ids = NULL; + Py_END_ALLOW_THREADS; return Py_BuildValue("N", ans); } // }}} From 6ec107a92886635419c64e0bfa65a606a81032e0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 4 Sep 2012 13:22:49 +0530 Subject: [PATCH 73/77] Switch to using psutil to measure memory consumption --- setup/installer/linux/freeze2.py | 3 +- setup/installer/windows/notes.rst | 9 ++ src/calibre/utils/mem.py | 191 ++---------------------------- 3 files changed, 24 insertions(+), 179 deletions(-) diff --git a/setup/installer/linux/freeze2.py b/setup/installer/linux/freeze2.py index 13c02cca12..0ed49c9fef 100644 --- a/setup/installer/linux/freeze2.py +++ b/setup/installer/linux/freeze2.py @@ -15,7 +15,8 @@ from setup import Command, modules, basenames, functions, __version__, \ SITE_PACKAGES = ['PIL', 'dateutil', 'dns', 'PyQt4', 'mechanize', 'sip.so', 'BeautifulSoup.py', 'cssutils', 'encutils', 'lxml', 'sipconfig.py', 'xdg', 'dbus', '_dbus_bindings.so', 'dbus_bindings.py', - '_dbus_glib_bindings.so', 'netifaces.so'] + '_dbus_glib_bindings.so', 'netifaces.so', '_psutil_posix.so', + '_psutil_linux.so', 'psutil'] QTDIR = '/usr/lib/qt4' QTDLLS = ('QtCore', 'QtGui', 'QtNetwork', 'QtSvg', 'QtXml', 'QtWebKit', 'QtDBus') diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index d0f6eb67ba..aa330095d0 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -360,6 +360,15 @@ Run python setup.py build cp build/lib.win32-2.7/netifaces.pyd /cygdrive/c/Python27/Lib/site-packages/ +psutil +-------- + +Download the source tarball + +Run + +Python setup.py build +cp -r build/lib.win32-*/* /cygdrive/c/Python27/Lib/site-packages/ calibre --------- diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py index 4358ec7522..3bc1f29636 100644 --- a/src/calibre/utils/mem.py +++ b/src/calibre/utils/mem.py @@ -13,188 +13,23 @@ You can pass a number to memory and it will be subtracted from the returned value. ''' -import gc, os, re +import gc, os from calibre.constants import iswindows, islinux -if islinux: - # Taken, with thanks, from: - # http://wingolog.org/archives/2007/11/27/reducing-the-footprint-of-python-applications - - def permute(args): - ret = [] - if args: - first = args.pop(0) - for y in permute(args): - for x in first: - ret.append(x + y) - else: - ret.append('') - return ret - - def parsed_groups(match, *types): - groups = match.groups() - assert len(groups) == len(types) - return tuple([type(group) for group, type in zip(groups, types)]) - - class VMA(dict): - def __init__(self, *args): - (self.start, self.end, self.perms, self.offset, - self.major, self.minor, self.inode, self.filename) = args - - def parse_smaps(pid): - with open('/proc/%s/smaps'%pid, 'r') as maps: - hex = lambda s: int(s, 16) - - ret = [] - header = re.compile(r'^([0-9a-f]+)-([0-9a-f]+) (....) ([0-9a-f]+) ' - r'(..):(..) (\d+) *(.*)$') - detail = re.compile(r'^(.*): +(\d+) kB') - for line in maps: - m = header.match(line) - if m: - vma = VMA(*parsed_groups(m, hex, hex, str, hex, str, str, int, str)) - ret.append(vma) - else: - m = detail.match(line) - if m: - k, v = parsed_groups(m, str, int) - assert k not in vma - vma[k] = v - else: - print 'unparseable line:', line - return ret - - perms = permute(['r-', 'w-', 'x-', 'ps']) - - def make_summary_dicts(vmas): - mapped = {} - anon = {} - for d in mapped, anon: - # per-perm - for k in perms: - d[k] = {} - d[k]['Size'] = 0 - for y in 'Shared', 'Private': - d[k][y] = {} - for z in 'Clean', 'Dirty': - d[k][y][z] = 0 - # totals - for y in 'Shared', 'Private': - d[y] = {} - for z in 'Clean', 'Dirty': - d[y][z] = 0 - - for vma in vmas: - if vma.major == '00' and vma.minor == '00': - d = anon - else: - d = mapped - for y in 'Shared', 'Private': - for z in 'Clean', 'Dirty': - d[vma.perms][y][z] += vma.get(y + '_' + z, 0) - d[y][z] += vma.get(y + '_' + z, 0) - d[vma.perms]['Size'] += vma.get('Size', 0) - return mapped, anon - - def values(d, args): - if args: - ret = () - first = args[0] - for k in first: - ret += values(d[k], args[1:]) - return ret - else: - return (d,) - - def print_summary(dicts_and_titles): - def desc(title, perms): - ret = {('Anonymous', 'rw-p'): 'Data (malloc, mmap)', - ('Anonymous', 'rwxp'): 'Writable code (stack)', - ('Mapped', 'r-xp'): 'Code', - ('Mapped', 'rwxp'): 'Writable code (jump tables)', - ('Mapped', 'r--p'): 'Read-only data', - ('Mapped', 'rw-p'): 'Data'}.get((title, perms), None) - if ret: - return ' -- ' + ret - else: - return '' - - for d, title in dicts_and_titles: - print title, 'memory:' - print ' Shared Private' - print ' Clean Dirty Clean Dirty' - for k in perms: - if d[k]['Size']: - print (' %s %7d %7d %7d %7d%s' - % ((k,) - + values(d[k], (('Shared', 'Private'), - ('Clean', 'Dirty'))) - + (desc(title, k),))) - print (' total %7d %7d %7d %7d' - % values(d, (('Shared', 'Private'), - ('Clean', 'Dirty')))) - - print ' ' + '-' * 40 - print (' total %7d %7d %7d %7d' - % tuple(map(sum, zip(*[values(d, (('Shared', 'Private'), - ('Clean', 'Dirty'))) - for d, title in dicts_and_titles])))) - - def print_stats(pid=None): - if pid is None: - pid = os.getpid() - vmas = parse_smaps(pid) - mapped, anon = make_summary_dicts(vmas) - print_summary(((mapped, "Mapped"), (anon, "Anonymous"))) - - def linux_memory(since=0.0): - vmas = parse_smaps(os.getpid()) - mapped, anon = make_summary_dicts(vmas) - dicts_and_titles = ((mapped, "Mapped"), (anon, "Anonymous")) - totals = tuple(map(sum, zip(*[values(d, (('Shared', 'Private'), - ('Clean', 'Dirty'))) - for d, title in dicts_and_titles]))) - return (totals[-1]/1024.) - since - - memory = linux_memory - -elif iswindows: - import win32process - import win32con - import win32api - - # See http://msdn.microsoft.com/en-us/library/ms684877.aspx - # for details on the info returned by get_meminfo - - def get_handle(pid): - return win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, 0, - pid) - - def listprocesses(self): - for process in win32process.EnumProcesses(): - try: - han = get_handle(process) - procmeminfo = meminfo(han) - procmemusage = procmeminfo["WorkingSetSize"] - yield process, procmemusage - except: - pass - - def get_meminfo(pid): - han = win32api.OpenProcess(win32con.PROCESS_QUERY_INFORMATION, 0, - pid) - return meminfo(han) - - def meminfo(handle): - return win32process.GetProcessMemoryInfo(handle) - - def win_memory(since=0.0): - info = meminfo(get_handle(os.getpid())) - return (info['WorkingSetSize']/1024.**2) - since - - memory = win_memory +def get_memory(): + 'Return memory usage in bytes' + import psutil + p = psutil.Process(os.getpid()) + mem = p.get_ext_memory_info() + attr = 'wset' if iswindows else 'data' if islinux else 'rss' + return getattr(mem, attr) +def memory(since=0.0): + 'Return memory used in MB. The value of since is subtracted from the used memory' + ans = get_memory() + ans /= float(1024**2) + return ans - since def gc_histogram(): """Returns per-class counts of existing objects.""" From 206af4c041a905f069835bc7c8bd46cd9848aa10 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Sep 2012 09:28:06 +0530 Subject: [PATCH 74/77] Device drivers: Ignore corrupted metadata.calibre, instead of raising an error --- src/calibre/devices/usbms/driver.py | 2 +- src/calibre/ebooks/metadata/book/json_codec.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/driver.py b/src/calibre/devices/usbms/driver.py index 5f6bdd9402..94309e747e 100644 --- a/src/calibre/devices/usbms/driver.py +++ b/src/calibre/devices/usbms/driver.py @@ -166,7 +166,7 @@ class USBMS(CLI, Device): # make a dict cache of paths so the lookup in the loop below is faster. bl_cache = {} - for idx,b in enumerate(bl): + for idx, b in enumerate(bl): bl_cache[b.lpath] = idx all_formats = self.formats_to_scan_for() diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py index cc9b6f252d..e9cec8acc7 100644 --- a/src/calibre/ebooks/metadata/book/json_codec.py +++ b/src/calibre/ebooks/metadata/book/json_codec.py @@ -161,7 +161,9 @@ class JsonCodec(object): try: js = json.load(file_, encoding='utf-8') for item in js: - booklist.append(self.raw_to_book(item, book_class, prefix)) + entry = self.raw_to_book(item, book_class, prefix) + if entry is not None: + booklist.append(entry) except: print 'exception during JSON decode_from_file' traceback.print_exc() From 288e71de6f581a5fda511ff47de45528aa4e62b9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Sep 2012 09:33:11 +0530 Subject: [PATCH 75/77] ... --- recipes/scmp.recipe | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recipes/scmp.recipe b/recipes/scmp.recipe index 1da7b9e1bc..a4f4bf497c 100644 --- a/recipes/scmp.recipe +++ b/recipes/scmp.recipe @@ -39,10 +39,10 @@ class SCMP(BasicNewsRecipe): #br.set_debug_responses(True) #br.set_debug_redirects(True) if self.username is not None and self.password is not None: - br.open('http://www.scmp.com/portal/site/SCMP/') - br.select_form(name='loginForm') - br['Login' ] = self.username - br['Password'] = self.password + br.open('http://www.scmp.com/') + br.select_form(nr=1) + br['name'] = self.username + br['pass'] = self.password br.submit() return br From 2d23f6d493b15bfc4e7d575c9383fbcccd58503e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Sep 2012 10:05:55 +0530 Subject: [PATCH 76/77] Update published signatures when re-uploading an installer --- setup/upload.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/setup/upload.py b/setup/upload.py index a73d0d2c31..f4e8706d1d 100644 --- a/setup/upload.py +++ b/setup/upload.py @@ -47,6 +47,21 @@ def installer_description(fname): return 'Calibre Portable' return 'Unknown file' +def upload_signatures(): + tdir = mkdtemp() + for installer in installers(): + if not os.path.exists(installer): + continue + with open(installer, 'rb') as f: + raw = f.read() + fingerprint = hashlib.sha512(raw).hexdigest() + fname = os.path.basename(installer+'.sha512') + with open(os.path.join(tdir, fname), 'wb') as f: + f.write(fingerprint) + check_call('scp %s/*.sha512 divok:%s/signatures/' % (tdir, DOWNLOADS), + shell=True) + shutil.rmtree(tdir) + class ReUpload(Command): # {{{ description = 'Re-uplaod any installers present in dist/' @@ -57,6 +72,7 @@ class ReUpload(Command): # {{{ opts.replace = True def run(self, opts): + upload_signatures() for x in installers(): if os.path.exists(x): os.remove(x) @@ -223,19 +239,7 @@ class UploadToServer(Command): # {{{ %(__version__, DOWNLOADS), shell=True) check_call('ssh divok /etc/init.d/apache2 graceful', shell=True) - tdir = mkdtemp() - for installer in installers(): - if not os.path.exists(installer): - continue - with open(installer, 'rb') as f: - raw = f.read() - fingerprint = hashlib.sha512(raw).hexdigest() - fname = os.path.basename(installer+'.sha512') - with open(os.path.join(tdir, fname), 'wb') as f: - f.write(fingerprint) - check_call('scp %s/*.sha512 divok:%s/signatures/' % (tdir, DOWNLOADS), - shell=True) - shutil.rmtree(tdir) + upload_signatures() # }}} # Testing {{{ From a9e104dda625b09b3e1712c54ec1e54ab6ba2ff4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 5 Sep 2012 10:29:13 +0530 Subject: [PATCH 77/77] ... --- src/calibre/devices/android/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index efe979158d..9ae6f4ab9c 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -187,10 +187,10 @@ class ANDROID(USBMS): EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks'] EXTRA_CUSTOMIZATION_MESSAGE = [_('Comma separated list of directories to ' - 'send e-books to on the device\'s main memory. The first one that exists will ' + 'send e-books to on the device\'s main memory. The first one that exists will ' 'be used'), _('Comma separated list of directories to ' - 'send e-books to on the device\'s storage cards. The first one that exists will ' + 'send e-books to on the device\'s storage cards. The first one that exists will ' 'be used') ]