From bbce378f1305470778f2ee84469c732dcac1a8d8 Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 6 Sep 2012 09:49:55 -0600 Subject: [PATCH 01/32] Revised debug catalog generation to use initialize_container, refactored initial database fetch of titles and subsequent access. --- src/calibre/library/catalogs/epub_mobi.py | 9 +- .../library/catalogs/epub_mobi_builder.py | 174 +++++++++--------- 2 files changed, 89 insertions(+), 94 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index f0a4d1cb78..5acb0cfc7a 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -412,10 +412,15 @@ class EPUB_MOBI(CatalogPlugin): pass if GENERATE_DEBUG_EPUB: + from calibre.ebooks.epub import initialize_container from calibre.ebooks.tweak import zip_rebuilder + from calibre.utils.zipfile import ZipFile input_path = os.path.join(catalog_debug_path,'input') - shutil.copy(P('catalog/mimetype'),input_path) - shutil.copytree(P('catalog/META-INF'),os.path.join(input_path,'META-INF')) + epub_shell = os.path.join(catalog_debug_path,'epub_shell.zip') + initialize_container(epub_shell, opf_name='content.opf') + with ZipFile(epub_shell, 'r') as zf: + zf.extractall(path=input_path) + os.remove(epub_shell) zip_rebuilder(input_path, os.path.join(catalog_debug_path,'input.epub')) # returns to gui2.actions.catalog:catalog_generated() diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index a2a22d2c74..6e912f3295 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -5,6 +5,7 @@ __copyright__ = '2010, Greg Riker' import datetime, htmlentitydefs, os, re, shutil, unicodedata, zlib from copy import deepcopy +from operator import itemgetter from xml.sax.saxutils import escape from calibre import (prepare_string_for_xml, strftime, force_unicode) @@ -56,15 +57,6 @@ class CatalogBuilder(object): """ property decorators for attributes """ if True: - - - - - - - - - ''' directory to store cached thumbs ''' @property def cache_dir(self): @@ -102,10 +94,6 @@ class CatalogBuilder(object): def generate_recently_read(self): return self.__generate_recently_read - - - - ''' additional field to include before/after comments ''' @property def merge_comments_rule(self): @@ -128,9 +116,6 @@ class CatalogBuilder(object): def plugin(self): return self.__plugin - - - ''' Progress Reporter for Jobs ''' @property def reporter(self): @@ -199,6 +184,7 @@ class CatalogBuilder(object): self.__stylesheet = stylesheet self.__cache_dir = os.path.join(config_dir, 'caches', 'catalog') self.__catalog_path = PersistentTemporaryDirectory("_epub_mobi_catalog", prefix='') + self.__excluded_tags = self.get_excluded_tags() self.__generate_for_kindle = True if (_opts.fmt == 'mobi' and _opts.output_profile and _opts.output_profile.startswith("kindle")) else False @@ -221,12 +207,13 @@ class CatalogBuilder(object): self.books_by_title = None ''' list of books in series, without series prefix ''' self.books_by_title_no_series_prefix = None + ''' Initial list of books to catalog from which all sections are built ''' + self.books_to_catalog = None self.__content_dir = os.path.join(self.catalog_path, "content") ''' track Job progress ''' self.current_step = 0.0 ''' cumulative error messages to report at conclusion ''' self.error = [] - self.__excluded_tags = self.get_excluded_tags() self.__generate_recently_read = True if (_opts.generate_recently_added and _opts.connected_kindle and self.generate_for_kindle) else False @@ -262,6 +249,7 @@ class CatalogBuilder(object): self.total_steps = 6.0 self.__use_series_prefix_in_titles_section = False + self.books_to_catalog = self.fetch_books_to_catalog() self.compute_total_steps() self.calculate_thumbnail_dimensions() self.confirm_thumbs_archive() @@ -343,6 +331,15 @@ class CatalogBuilder(object): series_index) return key + def _kf_books_by_series_sorter(self, book): + index = book['series_index'] + integer = int(index) + fraction = index-integer + series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0')) + key = '%s %s' % (self.generate_sort_title(book['series']), + series_index) + return key + """ Methods """ def build_sources(self): @@ -614,7 +611,7 @@ class CatalogBuilder(object): annoyance for EPUB. Inputs: - self.books_by_title (list): list of books to catalog + self.books_to_catalog (list): list of books to catalog Output: self.books_by_author (list): sorted by author @@ -623,7 +620,7 @@ class CatalogBuilder(object): AuthorSortMismatchException: author_sort mismatch detected """ - self.books_by_author = sorted(list(self.books_by_title), key=self._kf_books_by_author_sorter_author) + self.books_by_author = sorted(list(self.books_to_catalog), key=self._kf_books_by_author_sorter_author) authors = [(record['author'], record['author_sort']) for record in self.books_by_author] current_author = authors[0] for (i,author) in enumerate(authors): @@ -671,7 +668,7 @@ class CatalogBuilder(object): None: no match """ def _log_prefix_rule_match_info(rule, record): - self.opts.log.info(" %s '%s' by %s (Prefix rule '%s')" % + self.opts.log.info(" %s '%s' by %s (Prefix rule '%s')" % (rule['prefix'],record['title'], record['authors'][0], rule['name'])) @@ -770,7 +767,7 @@ class CatalogBuilder(object): to self.authors. Inputs: - self.books_by_title (list): database, sorted by title + self.books_to_catalog (list): database, sorted by title Outputs: books_by_author: database, sorted by author @@ -790,7 +787,7 @@ class CatalogBuilder(object): # Determine the longest author_sort length before sorting asl = [i['author_sort'] for i in self.books_by_author] las = max(asl, key=len) - self.books_by_author = sorted(self.books_by_author, + self.books_by_author = sorted(self.books_to_catalog, key=lambda x: sort_key(self._kf_books_by_author_sorter_author_sort(x, len(las)))) if self.DEBUG and self.opts.verbose: @@ -843,9 +840,42 @@ class CatalogBuilder(object): return True def fetch_books_by_title(self): - """ Populate self.books_by_title from database + """ Generate a list of books sorted by title. - Create self.books_by_title from filtered database. + Sort the database by title. + + Inputs: + self.books_to_catalog (list): database + + Outputs: + books_by_title: database, sorted by title + + Return: + True: no errors + False: author_sort mismatch detected while building MOBI + """ + self.update_progress_full_step(_("Sorting titles")) + # Re-sort based on title_sort + if len(self.books_to_catalog): + self.books_by_title = sorted(self.books_to_catalog, key=lambda x: sort_key(x['title_sort'].upper())) + + if self.DEBUG and self.opts.verbose: + self.opts.log.info("fetch_books_by_title(): %d books" % len(self.books_by_title)) + self.opts.log.info(" %-40s %-40s" % ('title', 'title_sort')) + for title in self.books_by_title: + self.opts.log.info((u" %-40s %-40s" % (title['title'][0:40], + title['title_sort'][0:40])).encode('utf-8')) + else: + error_msg = _("No books to catalog.\nCheck 'Excluded books' rules in E-book options.\n") + self.opts.log.error('*** ' + error_msg + ' ***') + self.error.append(_('No books available to include in catalog')) + self.error.append(error_msg) + raise EmptyCatalogException, error_msg + + def fetch_books_to_catalog(self): + """ Populate self.books_to_catalog from database + + Create self.books_to_catalog from filtered database. Keys: authors massaged author_sort record['author_sort'] or computed @@ -871,7 +901,7 @@ class CatalogBuilder(object): data (list): filtered list of book metadata dicts Outputs: - (list) books_by_title + (list) books_to_catalog Returns: True: Successful @@ -980,7 +1010,6 @@ class CatalogBuilder(object): return this_title # Entry point - self.update_progress_full_step(_("Fetching database")) self.opts.sort_by = 'title' search_phrase = '' @@ -1003,28 +1032,15 @@ class CatalogBuilder(object): data = self.plugin.search_sort_db(self.db, self.opts) data = self.process_exclusions(data) + if self.opts.verbose and self.prefix_rules: + self.opts.log.info(" Added prefixes:") + # Populate this_title{} from data[{},{}] titles = [] for record in data: this_title = _populate_title(record) titles.append(this_title) - - # Re-sort based on title_sort - if len(titles): - self.books_by_title = sorted(titles, key=lambda x: sort_key(x['title_sort'].upper())) - - if self.DEBUG and self.opts.verbose: - self.opts.log.info("fetch_books_by_title(): %d books" % len(self.books_by_title)) - self.opts.log.info(" %-40s %-40s" % ('title', 'title_sort')) - for title in self.books_by_title: - self.opts.log.info((u" %-40s %-40s" % (title['title'][0:40], - title['title_sort'][0:40])).encode('utf-8')) - else: - error_msg = _("No books to catalog.\nCheck 'Excluded books' rules in E-book options.\n") - self.opts.log.error('*** ' + error_msg + ' ***') - self.error.append(_('No books available to include in catalog')) - self.error.append(error_msg) - raise EmptyCatalogException, error_msg + return titles def fetch_bookmarks(self): """ Interrogate connected Kindle for bookmarks. @@ -1104,7 +1120,7 @@ class CatalogBuilder(object): d.initialize(self.opts.connected_device['save_template']) bookmarks = {} - for book in self.books_by_title: + for book in self.books_to_catalog: if 'formats' in book: path_map = {} id = book['id'] @@ -1148,7 +1164,7 @@ class CatalogBuilder(object): genre_tags_dict (dict): dict of filtered, normalized tags in data set """ - def _format_tag_list(tags, indent=5, line_break=70, header='Tag list'): + def _format_tag_list(tags, indent=2, line_break=70, header='Tag list'): def _next_tag(sorted_tags): for (i, tag) in enumerate(sorted_tags): if i < len(tags) - 1: @@ -1541,7 +1557,7 @@ class CatalogBuilder(object): def generate_html_by_date_added(self): """ Generate content/ByDateAdded.html. - Loop through self.books_by_title sorted by reverse date, generate HTML. + Loop through self.books_to_catalog sorted by reverse date, generate HTML. Input: books_by_title (list): books, sorted by title @@ -1735,10 +1751,10 @@ class CatalogBuilder(object): # >>> Books by date range <<< if self.use_series_prefix_in_titles_section: - self.books_by_date_range = sorted(self.books_by_title, + self.books_by_date_range = sorted(self.books_to_catalog, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) else: - nspt = deepcopy(self.books_by_title) + nspt = deepcopy(self.books_to_catalog) self.books_by_date_range = sorted(nspt, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) date_range_list = [] @@ -1763,7 +1779,7 @@ class CatalogBuilder(object): # >>>> Books by month <<<< # Sort titles case-insensitive for by month using series prefix - self.books_by_month = sorted(self.books_by_title, + self.books_by_month = sorted(self.books_to_catalog, key=lambda x:(x['timestamp'], x['timestamp']),reverse=True) # Loop through books by date @@ -2026,12 +2042,12 @@ class CatalogBuilder(object): if self.opts.verbose: if len(genre_list): - self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" % - (len(genre_list), len(self.books_by_title))) + self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" % + (len(genre_list), len(self.books_to_catalog))) for genre in genre_list: for key in genre: - self.opts.log.info(" %s: %d %s" % (self.get_friendly_genre_tag(key), + self.opts.log.info(" %s: %d %s" % (self.get_friendly_genre_tag(key), len(genre[key]), 'titles' if len(genre[key]) > 1 else 'title')) @@ -2226,48 +2242,28 @@ class CatalogBuilder(object): Output: content/BySeries.html (file) - To do: - self.books_by_series = [i for i in self.books_by_title if i['series']] """ friendly_name = _("Series") self.update_progress_full_step("%s HTML" % friendly_name) self.opts.sort_by = 'series' - # Merge self.excluded_tags with opts.search_text - # Updated to use exact match syntax - - search_phrase = 'series:true ' - if self.excluded_tags: - search_terms = [] - for tag in self.excluded_tags: - search_terms.append("tag:=%s" % tag) - search_phrase += "not (%s)" % " or ".join(search_terms) - - # If a list of ids are provided, don't use search_text - if self.opts.ids: - self.opts.search_text = search_phrase - else: - if self.opts.search_text: - self.opts.search_text += " " + search_phrase - else: - self.opts.search_text = search_phrase - - # Fetch the database as a dictionary - data = self.plugin.search_sort_db(self.db, self.opts) - - # Remove exclusions - self.books_by_series = self.process_exclusions(data, log_exclusion=False) + # *** Convert the existing database, resort by series/index *** + self.books_by_series = [i for i in self.books_to_catalog if i['series']] + self.books_by_series = sorted(self.books_by_series, key=lambda x: sort_key(self._kf_books_by_series_sorter(x))) if not self.books_by_series: self.opts.generate_series = False - self.opts.log(" no series found in selected books, cancelling series generation") + self.opts.log(" no series found in selected books, skipping Series section") return # Generate series_sort for book in self.books_by_series: book['series_sort'] = self.generate_sort_title(book['series']) + # Establish initial letter equivalencies + sort_equivalents = self.establish_equivalencies(self.books_by_series, key='series_sort') + soup = self.generate_html_empty_header(friendly_name) body = soup.find('body') @@ -2277,9 +2273,6 @@ class CatalogBuilder(object): current_letter = "" current_series = None - # Establish initial letter equivalencies - sort_equivalents = self.establish_equivalencies(self.books_by_series, key='series_sort') - # Loop through books_by_series series_count = 0 for idx, book in enumerate(self.books_by_series): @@ -2335,11 +2328,6 @@ class CatalogBuilder(object): # Use series, series index if avail else just title #aTag.insert(0,'%d. %s · %s' % (book['series_index'],escape(book['title']), ' & '.join(book['authors']))) - if is_date_undefined(book['pubdate']): - book['date'] = None - else: - book['date'] = strftime(u'%B %Y', book['pubdate'].timetuple()) - args = self.generate_format_args(book) formatted_title = self.by_series_title_template.format(**args).rstrip() aTag.insert(0,NavigableString(escape(formatted_title))) @@ -2438,7 +2426,7 @@ class CatalogBuilder(object): # Re-sort title list without leading series/series_index # Incoming title : if not self.use_series_prefix_in_titles_section: - nspt = deepcopy(self.books_by_title) + nspt = deepcopy(self.books_to_catalog) nspt = sorted(nspt, key=lambda x: sort_key(x['title_sort'].upper())) self.books_by_title_no_series_prefix = nspt @@ -4339,7 +4327,7 @@ class CatalogBuilder(object): # Report excluded books if self.opts.verbose and excluded_tags: - self.opts.log.info(" Excluded books by Tags:") + self.opts.log.info(" Excluded books:") data = self.db.get_data_as_dict(ids=self.opts.ids) for record in data: matched = list(set(record['tags']) & set(excluded_tags)) @@ -4632,7 +4620,7 @@ class CatalogBuilder(object): normalized += c return normalized - def process_exclusions(self, data_set, log_exclusion=True): + def process_exclusions(self, data_set): """ Filter data_set based on exclusion_rules. Compare each book in data_set to each exclusion_rule. Remove @@ -4666,16 +4654,18 @@ class CatalogBuilder(object): matched = re.search(pat, unicode(field_contents), re.IGNORECASE) if matched is not None: - if self.opts.verbose and log_exclusion: + if self.opts.verbose: field_md = self.db.metadata_for_field(field) for rule in self.opts.exclusion_rules: if rule[1] == '#%s' % field_md['label']: - self.opts.log.info(" - '%s' by %s (Exclusion rule '%s')" % + self.opts.log.info(" - '%s' by %s (Exclusion rule '%s')" % (record['title'], record['authors'][0], rule[0])) exclusion_set.append(record) if record in filtered_data_set: filtered_data_set.remove(record) break + else: + filtered_data_set.append(record) else: if (record not in filtered_data_set and record not in exclusion_set): From 059cd5a161562459d61d757b10164eb527203b49 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 7 Sep 2012 14:07:46 +0530 Subject: [PATCH 02/32] ... --- src/calibre/devices/usbms/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 795a22888f..02991c4f16 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -993,10 +993,10 @@ class Device(DeviceConfig, DevicePlugin): if on_card == 'carda': candidates = self.get_carda_ebook_dir(for_upload=True) - path = get_dest_dir(self._carda_prefix, candidates) + path = get_dest_dir(self._card_a_prefix, candidates) elif on_card == 'cardb': candidates = self.get_cardb_ebook_dir(for_upload=True) - path = get_dest_dir(self._cardb_prefix, candidates) + path = get_dest_dir(self._card_b_prefix, candidates) else: candidates = self.get_main_ebook_dir(for_upload=True) path = get_dest_dir(self._main_prefix, candidates) From 1f9302436ccf67cedf134567e7b6c7c278cbf208 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 7 Sep 2012 17:53:51 +0530 Subject: [PATCH 03/32] ... --- 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 2d5e73bece..10fdb50df9 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -208,7 +208,7 @@ class ANDROID(USBMS): 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0', - 'COBY_MID'] + 'COBY_MID', 'VS'] 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', @@ -227,7 +227,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', '7035', 'VS'] + 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E'] 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', From 6f13b354e48f16e5e58b8f4f356d143071ed2088 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 7 Sep 2012 17:57:35 +0530 Subject: [PATCH 04/32] ... --- Changelog.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changelog.yaml b/Changelog.yaml index d1119e1bbb..b0a9bd68d4 100644 --- a/Changelog.yaml +++ b/Changelog.yaml @@ -35,7 +35,7 @@ - title: "Add an option under Preferences->Look & Feel->Book Details to hide the cover in the book details panel" - - title: "The Calibre Companion Android app that allows wireless connection of Android device to calibre is out of beta. See https://play.google.com/stor/apps/details?id=com.multipie.calibreandroid" + - title: "The Calibre Companion Android app that allows wireless connection of Android device to calibre is out of beta. See https://play.google.com/store/apps/details?id=com.multipie.calibreandroid" bug fixes: - title: "Fix sorting by author not working in the device view in calibre when connected to iTunes" From 3b989ae2a699c7095018dc3ebaab46e0f5cd459b Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Fri, 7 Sep 2012 20:17:02 +0530 Subject: [PATCH 05/32] ... --- recipes/arcamax.recipe | 48 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/recipes/arcamax.recipe b/recipes/arcamax.recipe index 0f144466d7..924f5ad088 100644 --- a/recipes/arcamax.recipe +++ b/recipes/arcamax.recipe @@ -43,38 +43,38 @@ class Arcamax(BasicNewsRecipe): feeds = [] for title, url in [ ######## COMICS - GENERAL ######## - #(u"9 Chickweed Lane", u"http://www.arcamax.com/ninechickweedlane"), - #(u"Agnes", u"http://www.arcamax.com/agnes"), - #(u"Andy Capp", u"http://www.arcamax.com/andycapp"), + #(u"9 Chickweed Lane", #u"http://www.arcamax.com/thefunnies/ninechickweedlane"), + #(u"Agnes", u"http://www.arcamax.com/thefunnies/agnes"), + #(u"Andy Capp", #u"http://www.arcamax.com/thefunnies/andycapp"), (u"BC", u"http://www.arcamax.com/thefunnies/bc"), - #(u"Baby Blues", u"http://www.arcamax.com/babyblues"), - #(u"Beetle Bailey", u"http://www.arcamax.com/beetlebailey"), + #(u"Baby Blues", #u"http://www.arcamax.com/thefunnies/babyblues"), + #(u"Beetle Bailey", #u"http://www.arcamax.com/thefunnies/beetlebailey"), (u"Blondie", u"http://www.arcamax.com/thefunnies/blondie"), - #u"Boondocks", u"http://www.arcamax.com/boondocks"), - #(u"Cathy", u"http://www.arcamax.com/cathy"), - #(u"Daddys Home", u"http://www.arcamax.com/daddyshome"), + #u"Boondocks", u"http://www.arcamax.com/thefunnies/boondocks"), + #(u"Cathy", u"http://www.arcamax.com/thefunnies/cathy"), + #(u"Daddys Home", #u"http://www.arcamax.com/thefunnies/daddyshome"), (u"Dilbert", u"http://www.arcamax.com/thefunnies/dilbert"), - #(u"Dinette Set", u"http://www.arcamax.com/thedinetteset"), + #(u"Dinette Set", #u"http://www.arcamax.com/thefunnies/thedinetteset"), (u"Dog Eat Doug", u"http://www.arcamax.com/thefunnies/dogeatdoug"), (u"Doonesbury", u"http://www.arcamax.com/thefunnies/doonesbury"), - #(u"Dustin", u"http://www.arcamax.com/dustin"), + #(u"Dustin", u"http://www.arcamax.com/thefunnies/dustin"), (u"Family Circus", u"http://www.arcamax.com/thefunnies/familycircus"), (u"Garfield", u"http://www.arcamax.com/thefunnies/garfield"), - #(u"Get Fuzzy", u"http://www.arcamax.com/getfuzzy"), - #(u"Girls and Sports", u"http://www.arcamax.com/girlsandsports"), - #(u"Hagar the Horrible", u"http://www.arcamax.com/hagarthehorrible"), - #(u"Heathcliff", u"http://www.arcamax.com/heathcliff"), - #(u"Jerry King Cartoons", u"http://www.arcamax.com/humorcartoon"), - #(u"Luann", u"http://www.arcamax.com/luann"), - #(u"Momma", u"http://www.arcamax.com/momma"), - #(u"Mother Goose and Grimm", u"http://www.arcamax.com/mothergooseandgrimm"), + #(u"Get Fuzzy", #u"http://www.arcamax.com/thefunnies/getfuzzy"), + #(u"Girls and Sports", #u"http://www.arcamax.com/thefunnies/girlsandsports"), + #(u"Hagar the Horrible", #u"http://www.arcamax.com/thefunnies/hagarthehorrible"), + #(u"Heathcliff", #u"http://www.arcamax.com/thefunnies/heathcliff"), + #(u"Jerry King Cartoons", #u"http://www.arcamax.com/thefunnies/humorcartoon"), + #(u"Luann", u"http://www.arcamax.com/thefunnies/luann"), + #(u"Momma", u"http://www.arcamax.com/thefunnies/momma"), + #(u"Mother Goose and Grimm", #u"http://www.arcamax.com/thefunnies/mothergooseandgrimm"), (u"Mutts", u"http://www.arcamax.com/thefunnies/mutts"), - #(u"Non Sequitur", u"http://www.arcamax.com/nonsequitur"), - #(u"Pearls Before Swine", u"http://www.arcamax.com/pearlsbeforeswine"), - #(u"Pickles", u"http://www.arcamax.com/pickles"), - #(u"Red and Rover", u"http://www.arcamax.com/redandrover"), - #(u"Rubes", u"http://www.arcamax.com/rubes"), - #(u"Rugrats", u"http://www.arcamax.com/rugrats"), + #(u"Non Sequitur", #u"http://www.arcamax.com/thefunnies/nonsequitur"), + #(u"Pearls Before Swine", #u"http://www.arcamax.com/thefunnies/pearlsbeforeswine"), + #(u"Pickles", u"http://www.arcamax.com/thefunnies/pickles"), + #(u"Red and Rover", #u"http://www.arcamax.com/thefunnies/redandrover"), + #(u"Rubes", u"http://www.arcamax.com/thefunnies/rubes"), + #(u"Rugrats", u"http://www.arcamax.com/thefunnies/rugrats"), (u"Speed Bump", u"http://www.arcamax.com/thefunnies/speedbump"), (u"Wizard of Id", u"http://www.arcamax.com/thefunnies/wizardofid"), (u"Zits", u"http://www.arcamax.com/thefunnies/zits"), From 3a584f406e6b3be4d601276da138be78f49d0f02 Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Fri, 7 Sep 2012 14:07:41 -0600 Subject: [PATCH 06/32] Added/fixed translation strings, removed most of the remaining @property decorators. --- src/calibre/gui2/catalog/catalog_epub_mobi.py | 6 +- src/calibre/library/catalogs/epub_mobi.py | 4 +- .../library/catalogs/epub_mobi_builder.py | 214 +++++------------- 3 files changed, 59 insertions(+), 165 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.py b/src/calibre/gui2/catalog/catalog_epub_mobi.py index e0dfffbb35..7f0f7ab8f5 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.py +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.py @@ -87,7 +87,7 @@ class PluginWidget(QWidget,Ui_Form): option_fields += zip(['exclusion_rules_tw'], [{'ordinal':0, 'enabled':True, - 'name':'Catalogs', + 'name':_('Catalogs'), 'field':'Tags', 'pattern':'Catalog'},], ['table_widget']) @@ -96,13 +96,13 @@ class PluginWidget(QWidget,Ui_Form): option_fields += zip(['prefix_rules_tw','prefix_rules_tw'], [{'ordinal':0, 'enabled':True, - 'name':'Read book', + 'name':_('Read book'), 'field':'Tags', 'pattern':'+', 'prefix':u'\u2713'}, {'ordinal':1, 'enabled':True, - 'name':'Wishlist item', + 'name':_('Wishlist item'), 'field':'Tags', 'pattern':'Wishlist', 'prefix':u'\u00d7'},], diff --git a/src/calibre/library/catalogs/epub_mobi.py b/src/calibre/library/catalogs/epub_mobi.py index 5acb0cfc7a..9db05d5076 100644 --- a/src/calibre/library/catalogs/epub_mobi.py +++ b/src/calibre/library/catalogs/epub_mobi.py @@ -59,7 +59,7 @@ class EPUB_MOBI(CatalogPlugin): "Applies to: AZW3, ePub, MOBI output formats")), Option('--exclusion-rules', - default="(('Excluded tags','Tags','Catalog'),)", + default="(('Catalogs','Tags','Catalog'),)", dest='exclusion_rules', action=None, help=_("Specifies the rules used to exclude books from the generated catalog.\n" @@ -139,7 +139,7 @@ class EPUB_MOBI(CatalogPlugin): "Default: '%default'\n" "Applies to: AZW3, ePub, MOBI output formats")), Option('--prefix-rules', - default="(('Read books','tags','+','\u2713'),('Wishlist items','tags','Wishlist','\u00d7'))", + default="(('Read books','tags','+','\u2713'),('Wishlist item','tags','Wishlist','\u00d7'))", dest='prefix_rules', action=None, help=_("Specifies the rules used to include prefixes indicating read books, wishlist items and other user-specified prefixes.\n" diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 6e912f3295..548c8139ef 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -55,199 +55,93 @@ class CatalogBuilder(object): # title dc:title in OPF metadata, NCX periodical # verbosity level of diagnostic printout - """ property decorators for attributes """ - if True: - ''' directory to store cached thumbs ''' - @property - def cache_dir(self): - return self.__cache_dir + ''' device-specific symbol (default empty star) ''' + @property + def SYMBOL_EMPTY_RATING(self): + return self.output_profile.empty_ratings_char - ''' temp dir to store generated catalog ''' - @property - def catalog_path(self): - return self.__catalog_path + ''' device-specific symbol (default filled star) ''' + @property + def SYMBOL_FULL_RATING(self): + return self.output_profile.ratings_char - ''' content dir in generated catalog ''' - @property - def content_dir(self): - return self.__content_dir + ''' device-specific symbol for reading progress ''' + @property + def SYMBOL_PROGRESS_READ(self): + psr = '+' + if self.generate_for_kindle: + psr = '▪' + return psr + ''' device-specific symbol for reading progress ''' + @property + def SYMBOL_PROGRESS_UNREAD(self): + psu = '-' + if self.generate_for_kindle: + psu = '▫' + return psu - ''' active database ''' - @property - def db(self): - return self.__db + ''' device-specific symbol for reading progress ''' + @property + def SYMBOL_READING(self): + if self.generate_for_kindle: + return self.format_prefix('▷') + else: + return self.format_prefix(' ') - ''' tags to exclude as genres ''' - @property - def excluded_tags(self): - return self.__excluded_tags - - ''' True if generating for Kindle in MOBI format ''' - @property - def generate_for_kindle(self): - return self.__generate_for_kindle - - ''' True if connected Kindle and generating for Kindle ''' - @property - def generate_recently_read(self): - return self.__generate_recently_read - - ''' additional field to include before/after comments ''' - @property - def merge_comments_rule(self): - return self.__merge_comments_rule - - - ''' opts passed from gui2.catalog.catalog_epub_mobi.py ''' - @property - def opts(self): - return self.__opts - - ''' output_profile declares special symbols ''' - @property - def output_profile(self): - return self.__output_profile - - - ''' catalog??? device??? ''' - @property - def plugin(self): - return self.__plugin - - ''' Progress Reporter for Jobs ''' - @property - def reporter(self): - return self.__reporter - - ''' stylesheet to include with catalog ''' - @property - def stylesheet(self): - return self.__stylesheet - - ''' device-specific symbol (default empty star) ''' - @property - def SYMBOL_EMPTY_RATING(self): - return self.output_profile.empty_ratings_char - - ''' device-specific symbol (default filled star) ''' - @property - def SYMBOL_FULL_RATING(self): - return self.output_profile.ratings_char - - ''' device-specific symbol for reading progress ''' - @property - def SYMBOL_PROGRESS_READ(self): - psr = '+' - if self.generate_for_kindle: - psr = '▪' - return psr - - ''' device-specific symbol for reading progress ''' - @property - def SYMBOL_PROGRESS_UNREAD(self): - psu = '-' - if self.generate_for_kindle: - psu = '▫' - return psu - - ''' device-specific symbol for reading progress ''' - @property - def SYMBOL_READING(self): - if self.generate_for_kindle: - return self.format_prefix('▷') - else: - return self.format_prefix(' ') - - - ''' full path to thumbs archive ''' - @property - def thumbs_path(self): - return self.__thumbs_path - - - ''' switch controlling format of series books in Titles section ''' - @property - def use_series_prefix_in_titles_section(self): - return self.__use_series_prefix_in_titles_section - def __init__(self, db, _opts, plugin, report_progress=DummyReporter(), stylesheet="content/stylesheet.css", init_resources=True): - self.__db = db - self.__opts = _opts - self.__plugin = plugin - self.__reporter = report_progress - self.__stylesheet = stylesheet - self.__cache_dir = os.path.join(config_dir, 'caches', 'catalog') - self.__catalog_path = PersistentTemporaryDirectory("_epub_mobi_catalog", prefix='') - self.__excluded_tags = self.get_excluded_tags() - self.__generate_for_kindle = True if (_opts.fmt == 'mobi' and + self.db = db + self.opts = _opts + self.plugin = plugin + self.reporter = report_progress + self.stylesheet = stylesheet + self.cache_dir = os.path.join(config_dir, 'caches', 'catalog') + self.catalog_path = PersistentTemporaryDirectory("_epub_mobi_catalog", prefix='') + self.excluded_tags = self.get_excluded_tags() + self.generate_for_kindle = True if (_opts.fmt == 'mobi' and _opts.output_profile and _opts.output_profile.startswith("kindle")) else False - ''' list of unique authors ''' self.authors = None - ''' dict of bookmarked books ''' self.bookmarked_books = None - ''' list of bookmarked books, sorted by date read ''' self.bookmarked_books_by_date_read = None - ''' list of books, sorted by author ''' self.books_by_author = None - ''' list of books, grouped by date range (30 days) ''' self.books_by_date_range = None - ''' list of books, by date added reverse (most recent first) ''' self.books_by_month = None - ''' list of books in series ''' self.books_by_series = None - ''' list of books, sorted by title ''' self.books_by_title = None - ''' list of books in series, without series prefix ''' self.books_by_title_no_series_prefix = None - ''' Initial list of books to catalog from which all sections are built ''' self.books_to_catalog = None - self.__content_dir = os.path.join(self.catalog_path, "content") - ''' track Job progress ''' + self.content_dir = os.path.join(self.catalog_path, "content") self.current_step = 0.0 - ''' cumulative error messages to report at conclusion ''' self.error = [] - self.__generate_recently_read = True if (_opts.generate_recently_added and - _opts.connected_kindle and - self.generate_for_kindle) else False - ''' list of dicts with books by genre ''' + self.generate_recently_read = True if (_opts.generate_recently_added and + _opts.connected_kindle and + self.generate_for_kindle) else False self.genres = [] - ''' dict of enabled genre tags ''' self.genre_tags_dict = None - ''' Author, Title, Series sections ''' self.html_filelist_1 = [] - ''' Date Added, Date Read ''' self.html_filelist_2 = [] - self.__merge_comments_rule = dict(zip(['field','position','hr'],_opts.merge_comments_rule.split(':'))) - ''' cumulative HTML for NCX file ''' + self.merge_comments_rule = dict(zip(['field','position','hr'], + _opts.merge_comments_rule.split(':'))) self.ncx_soup = None - self.__output_profile = None - self.__output_profile = self.get_output_profile(_opts) - ''' playOrder value for building NCX ''' + self.output_profile = None + self.output_profile = self.get_output_profile(_opts) self.play_order = 1 - ''' dict of prefix rules ''' self.prefix_rules = self.get_prefix_rules() - ''' used with ProgressReporter() ''' self.progress_int = 0.0 - ''' used with ProgressReporter() ''' self.progress_string = '' - - self.__thumb_height = 0 - - self.__thumb_width = 0 - ''' list of generated thumbs ''' + self.thumb_height = 0 + self.thumb_width = 0 self.thumbs = None - self.__thumbs_path = os.path.join(self.cache_dir, "thumbs.zip") - ''' used with ProgressReporter() ''' + self.thumbs_path = os.path.join(self.cache_dir, "thumbs.zip") self.total_steps = 6.0 - self.__use_series_prefix_in_titles_section = False + self.use_series_prefix_in_titles_section = False self.books_to_catalog = self.fetch_books_to_catalog() self.compute_total_steps() @@ -3141,8 +3035,8 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(soup, 'navLabel') textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(_(u"Series beginning with %s") % \ - (title_letters[i] if len(title_letters[i])>1 else "'" + title_letters[i] + "'"))) + textTag.insert(0, NavigableString(_(u"Series beginning with '%s'") % \ + (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) contentTag = Tag(soup, 'content') @@ -3262,8 +3156,8 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(soup, 'navLabel') textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(_(u"Titles beginning with %s") % \ - (title_letters[i] if len(title_letters[i])>1 else "'" + title_letters[i] + "'"))) + textTag.insert(0, NavigableString(_(u"Titles beginning with '%s'") % \ + (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) contentTag = Tag(soup, 'content') From fccdb72b8b56558b0fd1cbfb701256d9b7c9dc9d Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Fri, 7 Sep 2012 14:33:34 -0600 Subject: [PATCH 07/32] =?UTF-8?q?Better=20solution=20to=20lp:1047426=20-?= =?UTF-8?q?=20"=E2=80=A6beginning=20with"=20strings=20use=20quotes=20aroun?= =?UTF-8?q?d=20letters,=20but=20not=20'Symbols'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../library/catalogs/epub_mobi_builder.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 548c8139ef..c93b318966 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -3035,7 +3035,11 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(soup, 'navLabel') textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(_(u"Series beginning with '%s'") % \ + if len(title_letters[i])>1: + fmt_string = _(u"Series beginning with %s") + else: + fmt_string = _(u"Series beginning with '%s'") + textTag.insert(0, NavigableString(fmt_string % (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) @@ -3156,7 +3160,11 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(soup, 'navLabel') textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(_(u"Titles beginning with '%s'") % \ + if len(title_letters[i])>1: + fmt_string = _(u"Titles beginning with %s") + else: + fmt_string = _(u"Titles beginning with '%s'") + textTag.insert(0, NavigableString(fmt_string % (title_letters[i] if len(title_letters[i])>1 else title_letters[i]))) navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) @@ -3267,7 +3275,11 @@ class CatalogBuilder(object): self.play_order += 1 navLabelTag = Tag(soup, 'navLabel') textTag = Tag(soup, 'text') - textTag.insert(0, NavigableString(_("Authors beginning with '%s'") % (authors_by_letter[1]))) + if len(authors_by_letter[1])>1: + fmt_string = _(u"Authors beginning with %s") + else: + fmt_string = _(u"Authors beginning with '%s'") + textTag.insert(0, NavigableString(fmt_string % (authors_by_letter[1]))) navLabelTag.insert(0, textTag) navPointByLetterTag.insert(0,navLabelTag) contentTag = Tag(soup, 'content') From 0a3f3682804a6ccbdf6bf1b7de2279fc08d624ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sat, 8 Sep 2012 09:25:59 +0530 Subject: [PATCH 08/32] Fix #1047691 (Drag n drop to Book detail failure) --- src/calibre/library/database2.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 17c01a6f56..0b23e3f0a4 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -1432,6 +1432,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): pdir = os.path.dirname(dest) if not os.path.exists(pdir): os.makedirs(pdir) + size = 0 if copy_function is not None: copy_function(dest) size = os.path.getsize(dest) @@ -1441,6 +1442,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns): with lopen(dest, 'wb') as f: shutil.copyfileobj(stream, f) size = f.tell() + elif os.path.exists(dest): + size = os.path.getsize(dest) self.conn.execute('INSERT OR REPLACE INTO data (book,format,uncompressed_size,name) VALUES (?,?,?,?)', (id, format.upper(), size, name)) self.update_last_modified([id], commit=False) From a06685fd5c304fbb8b8a4ada47ca715f363eb5a8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sat, 8 Sep 2012 09:39:23 +0530 Subject: [PATCH 09/32] Fix #1047721 ([Enhancement] Change new version notification message) --- src/calibre/gui2/update.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/update.py b/src/calibre/gui2/update.py index a7bc341a96..0b685e2fd2 100644 --- a/src/calibre/gui2/update.py +++ b/src/calibre/gui2/update.py @@ -75,7 +75,7 @@ class UpdateNotification(QDialog): self.logo.setPixmap(QPixmap(I('lt.png')).scaled(100, 100, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) self.label = QLabel(('<p>'+ - _('%(app)s has been updated to version <b>%(ver)s</b>. ' + _('New version <b>%(ver)s</b> of %(app)s is available for download. ' 'See the <a href="http://calibre-ebook.com/whats-new' '">new features</a>.'))%dict( app=__appname__, ver=calibre_version)) From 8d30c3434a973674b2b00b952b8f2d65678dfa57 Mon Sep 17 00:00:00 2001 From: GRiker <griker@hotmail.com> Date: Sat, 8 Sep 2012 04:24:20 -0600 Subject: [PATCH 10/32] Handled condition when source book has no tags field. --- src/calibre/library/catalogs/epub_mobi_builder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index c93b318966..3fc0df58b2 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -877,9 +877,11 @@ class CatalogBuilder(object): this_title['prefix'] = self.discover_prefix(record) + this_title['tags'] = [] if record['tags']: this_title['tags'] = self.filter_excluded_genres(record['tags'], self.opts.exclude_genre) + if record['formats']: formats = [] for format in record['formats']: From ceebebf54f38dcfcb0756879cbfca08c7e97ebe8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sat, 8 Sep 2012 17:54:20 +0530 Subject: [PATCH 11/32] MTP: Implement user specified ignoring of devices --- src/calibre/devices/errors.py | 6 ++ src/calibre/devices/mtp/base.py | 3 + src/calibre/devices/mtp/driver.py | 9 +- src/calibre/devices/mtp/unix/driver.py | 42 +++++--- src/calibre/devices/mtp/windows/driver.py | 17 ++- src/calibre/gui2/device.py | 6 +- src/calibre/gui2/device_drivers/mtp_config.py | 102 ++++++++++++++---- 7 files changed, 145 insertions(+), 40 deletions(-) diff --git a/src/calibre/devices/errors.py b/src/calibre/devices/errors.py index d906bb86c8..56f9b1460d 100644 --- a/src/calibre/devices/errors.py +++ b/src/calibre/devices/errors.py @@ -110,3 +110,9 @@ class WrongDestinationError(PathError): trying to send books to a non existant storage card.''' pass +class BlacklistedDevice(OpenFailed): + ''' Raise this error during open() when the device being opened has been + blacklisted by the user. Only used in drivers that manage device presence, + like the MTP driver. ''' + pass + diff --git a/src/calibre/devices/mtp/base.py b/src/calibre/devices/mtp/base.py index a5885ca964..4ada58ecef 100644 --- a/src/calibre/devices/mtp/base.py +++ b/src/calibre/devices/mtp/base.py @@ -59,4 +59,7 @@ class MTPDeviceBase(DevicePlugin): from calibre.devices.utils import build_template_regexp return build_template_regexp(self.save_template) + def is_customizable(self): + return True + diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 92184af8ff..55472d3d44 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -16,7 +16,7 @@ from calibre.constants import iswindows, numeric_version from calibre.devices.mtp.base import debug from calibre.ptempfile import SpooledTemporaryFile, PersistentTemporaryDirectory from calibre.utils.config import from_json, to_json, JSONConfig -from calibre.utils.date import now, isoformat +from calibre.utils.date import now, isoformat, utcnow BASE = importlib.import_module('calibre.devices.mtp.%s.driver'%( 'windows' if iswindows else 'unix')).MTP_DEVICE @@ -51,6 +51,8 @@ class MTP_DEVICE(BASE): 'wordplayer/calibretransfer', 'Books', 'sdcard/ebooks', 'eBooks', 'kindle'] p.defaults['send_template'] = config().parse().send_template + p.defaults['blacklist'] = [] + p.defaults['history'] = {} return self._prefs @@ -74,6 +76,11 @@ class MTP_DEVICE(BASE): self.current_library_uuid = library_uuid self.location_paths = None BASE.open(self, devices, library_uuid) + h = self.prefs['history'] + if self.current_serial_num: + h[self.current_serial_num] = (self.current_friendly_name, + isoformat(utcnow())) + self.prefs['history'] = h # Device information {{{ def _update_drive_info(self, storage, location_code, name=None): diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 3792bb2fcc..31f886b875 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -15,7 +15,7 @@ from functools import partial from calibre import prints, as_unicode from calibre.constants import plugins from calibre.ptempfile import SpooledTemporaryFile -from calibre.devices.errors import OpenFailed, DeviceError +from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.mtp.base import MTPDeviceBase, synchronous MTPDevice = namedtuple('MTPDevice', 'busnum devnum vendor_id product_id ' @@ -99,19 +99,25 @@ class MTP_DEVICE(MTPDeviceBase): return False p('Known MTP devices connected:') for d in devs: p(d) - d = devs[0] - p('\nTrying to open:', d) - try: - self.open(d, 'debug') - except: - p('Opening device failed:') - p(traceback.format_exc()) - return False - p('Opened', self.current_friendly_name, 'successfully') - p('Storage info:') - p(pprint.pformat(self.dev.storage_info)) - self.eject() - return True + + for d in devs: + p('\nTrying to open:', d) + try: + self.open(d, 'debug') + except BlacklistedDevice: + p('This device has been blacklisted by the user') + continue + except: + p('Opening device failed:') + p(traceback.format_exc()) + return False + else: + p('Opened', self.current_friendly_name, 'successfully') + p('Storage info:') + p(pprint.pformat(self.dev.storage_info)) + self.post_yank_cleanup() + return True + return False @synchronous def create_device(self, connected_device): @@ -167,6 +173,12 @@ class MTP_DEVICE(MTPDeviceBase): if not storage: self.blacklisted_devices.add(connected_device) raise OpenFailed('No storage found for device %s'%(connected_device,)) + snum = self.dev.serial_number + if snum in self.prefs.get('blacklist', []): + self.blacklisted_devices.add(connected_device) + self.dev = None + raise BlacklistedDevice( + 'The %s device has been blacklisted by the user'%(connected_device,)) self._main_id = storage[0]['id'] self._carda_id = self._cardb_id = None if len(storage) > 1: @@ -176,7 +188,7 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name = self.dev.friendly_name if not self.current_friendly_name: self.current_friendly_name = self.dev.model_name or _('Unknown MTP device') - self.current_serial_num = self.dev.serial_number + self.current_serial_num = snum @property def filesystem_cache(self): diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 2f606b42d1..50638496d1 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -15,7 +15,7 @@ from itertools import chain from calibre import as_unicode, prints from calibre.constants import plugins, __appname__, numeric_version from calibre.ptempfile import SpooledTemporaryFile -from calibre.devices.errors import OpenFailed, DeviceError +from calibre.devices.errors import OpenFailed, DeviceError, BlacklistedDevice from calibre.devices.mtp.base import MTPDeviceBase class ThreadingViolation(Exception): @@ -163,6 +163,9 @@ class MTP_DEVICE(MTPDeviceBase): p('\nTrying to open:', pnp_id) try: self.open(pnp_id, 'debug-detection') + except BlacklistedDevice: + p('This device has been blacklisted by the user') + continue except: p('Open failed:') p(traceback.format_exc()) @@ -172,7 +175,7 @@ class MTP_DEVICE(MTPDeviceBase): p('Opened', self.current_friendly_name, 'successfully') p('Device info:') p(pprint.pformat(self.dev.data)) - self.eject() + self.post_yank_cleanup() return True p('No suitable MTP devices found') return False @@ -225,7 +228,6 @@ class MTP_DEVICE(MTPDeviceBase): 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 @@ -256,6 +258,13 @@ class MTP_DEVICE(MTPDeviceBase): if not storage: self.blacklisted_devices.add(connected_device) raise OpenFailed('No storage found for device %s'%(connected_device,)) + snum = devdata.get('serial_number', None) + if snum in self.prefs.get('blacklist', []): + self.blacklisted_devices.add(connected_device) + self.dev = None + raise BlacklistedDevice( + 'The %s device has been blacklisted by the user'%(connected_device,)) + self._main_id = storage[0]['id'] if len(storage) > 1: self._carda_id = storage[1]['id'] @@ -266,7 +275,7 @@ class MTP_DEVICE(MTPDeviceBase): self.current_friendly_name = devdata.get('model_name', _('Unknown MTP device')) self.currently_connected_pnp_id = connected_device - self.current_serial_num = devdata.get('serial_number', None) + self.current_serial_num = snum @same_thread def get_basic_device_information(self): diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 6d638ef9c2..8466fe9320 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -24,7 +24,8 @@ from calibre.gui2 import (config, error_dialog, Dispatcher, dynamic, from calibre.ebooks.metadata import authors_to_string from calibre import preferred_encoding, prints, force_unicode, as_unicode from calibre.utils.filenames import ascii_filename -from calibre.devices.errors import FreeSpaceError, WrongDestinationError +from calibre.devices.errors import (FreeSpaceError, WrongDestinationError, + BlacklistedDevice) from calibre.devices.apple.driver import ITUNES_ASYNC from calibre.devices.folder_device.driver import FOLDER_DEVICE from calibre.devices.bambook.driver import BAMBOOK, BAMBOOKWifi @@ -252,6 +253,9 @@ class DeviceManager(Thread): # {{{ if cd is not None: try: dev.open(cd, self.current_library_uuid) + except BlacklistedDevice as e: + prints('Ignoring blacklisted device: %s'% + as_unicode(e)) except: prints('Error while trying to open %s (Driver: %s)'% (cd, dev)) diff --git a/src/calibre/gui2/device_drivers/mtp_config.py b/src/calibre/gui2/device_drivers/mtp_config.py index b6628f4e65..261fea3df2 100644 --- a/src/calibre/gui2/device_drivers/mtp_config.py +++ b/src/calibre/gui2/device_drivers/mtp_config.py @@ -16,6 +16,7 @@ from PyQt4.Qt import (QWidget, QListWidgetItem, Qt, QToolButton, QLabel, from calibre.ebooks import BOOK_EXTENSIONS from calibre.gui2 import error_dialog from calibre.gui2.dialogs.template_dialog import TemplateDialog +from calibre.utils.date import parse_date class FormatsConfig(QWidget): # {{{ @@ -136,6 +137,45 @@ class SendToConfig(QWidget): # {{{ # }}} +class IgnoredDevices(QWidget): # {{{ + + def __init__(self, devs, blacklist): + QWidget.__init__(self) + self.l = l = QVBoxLayout() + self.setLayout(l) + self.la = la = QLabel('<p>'+_( + '''Select the devices to be <b>ignored</b>. calibre will not + connect to devices with a checkmark next to their names.''')) + la.setWordWrap(True) + l.addWidget(la) + self.f = f = QListWidget(self) + l.addWidget(f) + + devs = [(snum, (x[0], parse_date(x[1]))) for snum, x in + devs.iteritems()] + for dev, x in sorted(devs, key=lambda x:x[1][1], reverse=True): + name = x[0] + name = '%s [%s]'%(name, dev) + item = QListWidgetItem(name, f) + item.setData(Qt.UserRole, dev) + item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) + item.setCheckState(Qt.Checked if dev in blacklist else Qt.Unchecked) + + @property + def blacklist(self): + return [unicode(self.f.item(i).data(Qt.UserRole).toString()) for i in + xrange(self.f.count()) if self.f.item(i).checkState()==Qt.Checked] + + def ignore_device(self, snum): + for i in xrange(self.f.count()): + i = self.f.item(i) + c = unicode(i.data(Qt.UserRole).toString()) + if c == snum: + i.setCheckState(Qt.Checked) + break + +# }}} + class MTPConfig(QTabWidget): def __init__(self, device, parent=None): @@ -162,6 +202,8 @@ class MTPConfig(QTabWidget): l = QLabel(msg) l.setWordWrap(True) l.setStyleSheet('QLabel { margin-left: 2em }') + l.setMinimumWidth(500) + l.setMinimumHeight(400) self.insertTab(0, l, _('Cannot configure')) else: self.base = QWidget(self) @@ -173,16 +215,34 @@ class MTPConfig(QTabWidget): self.get_pref('format_map')) self.send_to = SendToConfig(self.get_pref('send_to')) self.template = TemplateConfig(self.get_pref('send_template')) - self.base.la = la = QLabel(_('Choose the formats to send to the %s')%self.device.current_friendly_name) + self.base.la = la = QLabel(_( + 'Choose the formats to send to the %s')%self.device.current_friendly_name) la.setWordWrap(True) l.addWidget(la, 0, 0, 1, 1) - l.addWidget(self.formats, 1, 0, 3, 1) + l.addWidget(self.formats, 1, 0, 2, 1) l.addWidget(self.send_to, 1, 1, 1, 1) l.addWidget(self.template, 2, 1, 1, 1) l.setRowStretch(2, 10) + self.base.b = b = QPushButton(QIcon(I('minus.png')), + _('Ignore the %s in calibre')%device.current_friendly_name, + self.base) + l.addWidget(b, 3, 0, 1, 2) + b.clicked.connect(self.ignore_device) + + self.igntab = IgnoredDevices(self.device.prefs['history'], + self.device.prefs['blacklist']) + self.addTab(self.igntab, _('Ignored devices')) self.setCurrentIndex(0) + def ignore_device(self): + self.igntab.ignore_device(self.device.current_serial_num) + self.base.b.setEnabled(False) + self.base.b.setText(_('The %s will be ignored in calibre')% + self.device.current_friendly_name) + self.base.b.setStyleSheet('QPushButton { font-weight: bold }') + self.base.setEnabled(False) + def get_pref(self, key): p = self.device.prefs.get(self.current_device_key, {}) if not p: @@ -194,31 +254,35 @@ class MTPConfig(QTabWidget): return self._device() def validate(self): - if not self.formats.validate(): - return False - if not self.template.validate(): - return False + if hasattr(self, 'formats'): + if not self.formats.validate(): + return False + if not self.template.validate(): + return False return True def commit(self): p = self.device.prefs.get(self.current_device_key, {}) - p.pop('format_map', None) - f = self.formats.format_map - if f and f != self.device.prefs['format_map']: - p['format_map'] = f + if hasattr(self, 'formats'): + p.pop('format_map', None) + f = self.formats.format_map + if f and f != self.device.prefs['format_map']: + p['format_map'] = f - p.pop('send_template', None) - t = self.template.template - if t and t != self.device.prefs['send_template']: - p['send_template'] = t + p.pop('send_template', None) + t = self.template.template + if t and t != self.device.prefs['send_template']: + p['send_template'] = t - p.pop('send_to', None) - s = self.send_to.value - if s and s != self.device.prefs['send_to']: - p['send_to'] = s + p.pop('send_to', None) + s = self.send_to.value + if s and s != self.device.prefs['send_to']: + p['send_to'] = s - self.device.prefs[self.current_device_key] = p + self.device.prefs[self.current_device_key] = p + + self.device.prefs['blacklist'] = self.igntab.blacklist if __name__ == '__main__': from calibre.gui2 import Application From 00c5e5e2f2be0b81e875c6db28e5493842090af6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sat, 8 Sep 2012 20:19:32 +0530 Subject: [PATCH 12/32] Fix #1047865 (Calibre not connecting showing android device) --- src/calibre/devices/android/driver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 10fdb50df9..e261fe71da 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -208,7 +208,7 @@ class ANDROID(USBMS): 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0', - 'COBY_MID', 'VS'] + 'COBY_MID', 'VS', 'AINOL'] 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', @@ -227,7 +227,8 @@ 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', '7035', 'VIEWPAD_7E'] + 'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E', + 'NOVO7'] 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', @@ -237,7 +238,8 @@ 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', '7035', 'VIEWPAD_7E'] + 'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E', + 'NOVO7'] OSX_MAIN_MEM = 'Android Device Main Memory' From 88821514f95d167448fe33d9481367b296a4f0ec Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sat, 8 Sep 2012 21:51:42 +0530 Subject: [PATCH 13/32] Update Birmingham post --- recipes/birmingham_post.recipe | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/recipes/birmingham_post.recipe b/recipes/birmingham_post.recipe index ae5d2c9ce9..b9b3c3fc57 100644 --- a/recipes/birmingham_post.recipe +++ b/recipes/birmingham_post.recipe @@ -1,14 +1,17 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1306097511(BasicNewsRecipe): title = u'Birmingham post' - description = 'News for Birmingham UK' - timefmt = '' + description = 'Author D.Asbury. News for Birmingham UK' + #timefmt = '' + # last update 8/9/12 __author__ = 'Dave Asbury' - cover_url = 'http://1.bp.blogspot.com/_GwWyq5eGw9M/S9BHPHxW55I/AAAAAAAAB6Q/iGCWl0egGzg/s320/Birmingham+post+Lite+front.JPG' + cover_url = 'http://profile.ak.fbcdn.net/hprofile-ak-snc4/161987_9010212100_2035706408_n.jpg' oldest_article = 2 max_articles_per_feed = 12 + linearize_tables = True remove_empty_feeds = True remove_javascript = True + no_stylesheets = True #auto_cleanup = True language = 'en_GB' @@ -17,11 +20,12 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): keep_only_tags = [ - dict(name='h1',attrs={'id' : 'article-headline'}), + dict(attrs={'id' : 'article-header'}), + #dict(name='h1',attrs={'id' : 'article-header'}), dict(attrs={'class':['article-meta-author','article-meta-date','article main','art-o art-align-center otm-1 ']}), - dict(name='div',attrs={'class' : 'article-image full'}), - dict(attrs={'clas' : 'art-o art-align-center otm-1 '}), - dict(name='div',attrs={'class' : 'article main'}), + dict(name='div',attrs={'class' : 'article-image full'}), + dict(attrs={'clas' : 'art-o art-align-center otm-1 '}), + dict(name='div',attrs={'class' : 'article main'}), #dict(name='p') #dict(attrs={'id' : 'three-col'}) ] @@ -37,11 +41,9 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): (u'Bloggs & Comments',u'http://www.birminghampost.net/comment/rss.xml') ] - extra_css = ''' - body {font: sans-serif medium;}' - h1 {text-align : center; font-family:Arial,Helvetica,sans-serif; font-size:20px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold;} - h2 {text-align : center;color:#4D4D4D;font-family:Arial,Helvetica,sans-serif; font-size:15px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:bold; } - span{ font-size:9.5px; font-weight:bold;font-style:italic} - p { text-align: justify; font-family:Arial,Helvetica,sans-serif; font-size:11px; font-size-adjust:none; font-stretch:normal; font-style:normal; font-variant:normal; font-weight:normal;} - - ''' + extra_css = ''' + h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;text-align:center;} + h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;} + p{font-family:Arial,Helvetica,sans-serif;font-size:small;} + body{font-family:Helvetica,Arial,sans-serif;font-size:small;} + ''' From 90be6ee35d59b3ca9aafdac1f0e615b3ecc0097e Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 00:47:28 +0530 Subject: [PATCH 14/32] Fix #1047947 (recipe for Die Zeit (subscription only) doesn't work after URL change) --- recipes/zeitde_sub.recipe | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recipes/zeitde_sub.recipe b/recipes/zeitde_sub.recipe index dfa52e8504..b22e9793ed 100644 --- a/recipes/zeitde_sub.recipe +++ b/recipes/zeitde_sub.recipe @@ -118,13 +118,13 @@ class ZeitEPUBAbo(BasicNewsRecipe): def build_index(self): domain = "https://premium.zeit.de" - url = domain + "/abo/zeit_digital" + url = domain + "/abo/digitalpaket" browser = self.get_browser() # new login process response = browser.open(url) # Get rid of nested form - response.set_data(response.get_data().replace('<div><form action="/abo/zeit_digital?destination=node%2F94" accept-charset="UTF-8" method="post" id="user-login-form" class="zol_inlinelabel">', '')) + response.set_data(response.get_data().replace('<div><form action="/abo/digitalpaket?destination=node%2F94" accept-charset="UTF-8" method="post" id="user-login-form" class="zol_inlinelabel">', '')) browser.set_response(response) browser.select_form(nr=2) browser.form['name']=self.username @@ -177,13 +177,13 @@ class ZeitEPUBAbo(BasicNewsRecipe): try: self.log.warning('Trying PDF-based cover') domain = "https://premium.zeit.de" - url = domain + "/abo/zeit_digital" + url = domain + "/abo/digitalpaket" browser = self.get_browser() # new login process response=browser.open(url) # Get rid of nested form - response.set_data(response.get_data().replace('<div><form action="/abo/zeit_digital?destination=node%2F94" accept-charset="UTF-8" method="post" id="user-login-form" class="zol_inlinelabel">', '')) + response.set_data(response.get_data().replace('<div><form action="/abo/digitalpaket?destination=node%2F94" accept-charset="UTF-8" method="post" id="user-login-form" class="zol_inlinelabel">', '')) browser.set_response(response) browser.select_form(nr=2) From 65f9b902fdbc4db09427adfa9112981c89d38a26 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 09:10:42 +0530 Subject: [PATCH 15/32] Fix #1047992 (Sony problem sending to SD card) --- src/calibre/devices/usbms/device.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index 02991c4f16..49d766c67f 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -974,6 +974,9 @@ class Device(DeviceConfig, DevicePlugin): def get_carda_ebook_dir(self, for_upload=False): return self.EBOOK_DIR_CARD_A + def get_cardb_ebook_dir(self, for_upload=False): + return self.EBOOK_DIR_CARD_B + def _sanity_check(self, on_card, files): from calibre.devices.utils import sanity_check sanity_check(on_card, files, self.card_prefix(), self.free_space()) From 2bafa6a5116f189629bbfc2a2c64719e630d6348 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 09:51:51 +0530 Subject: [PATCH 16/32] ... --- resources/compiled_coffeescript.zip | Bin 56964 -> 57017 bytes src/calibre/ebooks/oeb/display/mathjax.coffee | 1 + 2 files changed, 1 insertion(+) diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 573a8128ac70556e1a7468ad34ee82f7d3157c76..0f6b8c3e7a522839ce146a7c6211d81b23512310 100644 GIT binary patch delta 218 zcmZqK%e-?hvrK?DGm8iV2srv_I#$^&_FK)vzyQLs43qAOZq~mrlT}Y&Um-ZmUBODB zS|KMhFEyzsH8Hyw%uOsU$<IwJ$xH_FN{UKT6>1e~Hy^zEfo<}I5RQ#p-_A=jGKnzb zuwe3;J1Ud)FK|s>e@Bby84u99$sg_*F|me#SOM-FlO6ABF>Of%@&1Q!PwoZs{uhFH e=k6LX@n4>FPgR7K4diq-Al%Q$!0_Y_hz9_&qDUS9 delta 191 zcmdnFm$_vx^WtYb7bKZkL>NGzIzYm4v%uuEG9Cs75SE?H$Sbzda3<^K|Ew?BCL7+> zn9O%qd~(5EBPMzG$%-MelMmk2VmcQBWQtB^x@XEHnF?ka6tPVXxu?aXc?`^TxXh;$ w;LXS+!i>Wju;nn~-+7QGh1|rFjI6{8y{zH@Z&o&tBpVR!XJlYle+R?^07IHG^Z)<= diff --git a/src/calibre/ebooks/oeb/display/mathjax.coffee b/src/calibre/ebooks/oeb/display/mathjax.coffee index ad893baa7e..cd130c85c8 100644 --- a/src/calibre/ebooks/oeb/display/mathjax.coffee +++ b/src/calibre/ebooks/oeb/display/mathjax.coffee @@ -39,6 +39,7 @@ class MathJax showMathMenu: false, extensions: ["tex2jax.js", "asciimath2jax.js", "mml2jax.js"], jax: ["input/TeX","input/MathML","input/AsciiMath","output/SVG"], + // SVG : { linebreaks : { automatic : true } }, TeX: { extensions: ["AMSmath.js","AMSsymbols.js","noErrors.js","noUndefined.js"] } From 8a5ca1df87e3a37af06c20ad10a9f1c180786eac Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 10:41:32 +0530 Subject: [PATCH 17/32] History Today by Rick Shang --- recipes/history_today.recipe | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 recipes/history_today.recipe diff --git a/recipes/history_today.recipe b/recipes/history_today.recipe new file mode 100644 index 0000000000..43adf7a358 --- /dev/null +++ b/recipes/history_today.recipe @@ -0,0 +1,87 @@ +import re +from calibre.web.feeds.recipes import BasicNewsRecipe +from collections import OrderedDict + +class HistoryToday(BasicNewsRecipe): + + title = 'History Today' + __author__ = 'Rick Shang' + + description = 'UK-based magazine, publishing articles and book reviews covering all types and periods of history.' + language = 'en' + category = 'news' + encoding = 'UTF-8' + + remove_tags = [dict(name='div',attrs={'class':['print-logo','print-site_name','print-breadcrumb']}), + dict(name='div', attrs={'id':['ht-tools','ht-tools2','ht-tags']})] + 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: + br.open('http://www.historytoday.com/user/login') + br.select_form(nr=1) + br['name'] = self.username + br['pass'] = self.password + res = br.submit() + raw = res.read() + if 'Session limit exceeded' in raw: + br.select_form(nr=1) + control=br.find_control('sid').items[1] + sid = [] + br['sid']=sid.join(control) + br.submit() + return br + + def parse_index(self): + + #Find date + soup0 = self.index_to_soup('http://www.historytoday.com/') + dates = self.tag_to_string(soup0.find('div',attrs={'id':'block-block-226'}).span) + self.timefmt = u' [%s]'%dates + + #Go to issue + soup = self.index_to_soup('http://www.historytoday.com/contents') + cover = soup.find('div',attrs={'id':'content-area'}).find('img')['src'] + self.cover_url=cover + + #Go to the main body + + div = soup.find ('div', attrs={'class':'region region-content-bottom'}) + + feeds = OrderedDict() + section_title = '' + for section in div.findAll('div', attrs={'id':re.compile("block\-views\-contents.*")}): + section_title = self.tag_to_string(section.find('h2',attrs={'class':'title'})) + sectionbody=section.find('div', attrs={'class':'view-content'}) + for article in sectionbody.findAll('div',attrs={'class':re.compile("views\-row.*")}): + articles = [] + subarticle = [] + subarticle = article.findAll('div') + if len(subarticle) < 2: + continue + title=self.tag_to_string(subarticle[0]) + originalurl="http://www.historytoday.com" + subarticle[0].span.a['href'].strip() + originalpage=self.index_to_soup(originalurl) + printurl=originalpage.find('div',attrs = {'id':'ht-tools'}).a['href'].strip() + url="http://www.historytoday.com" + printurl + desc=self.tag_to_string(subarticle[1]) + articles.append({'title':title, 'url':url, 'description':desc, 'date':''}) + + 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 cleanup(self): + self.browser.open('http://www.historytoday.com/logout') + From e72a0f2482cce158fdcefa8f770cb0e92aabf023 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 12:46:36 +0530 Subject: [PATCH 18/32] Update countryfile --- recipes/countryfile.recipe | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/countryfile.recipe b/recipes/countryfile.recipe index 0502129791..71977048c7 100644 --- a/recipes/countryfile.recipe +++ b/recipes/countryfile.recipe @@ -1,12 +1,11 @@ from calibre import browser from calibre.web.feeds.news import BasicNewsRecipe - class AdvancedUserRecipe1325006965(BasicNewsRecipe): title = u'Countryfile.com' #cover_url = 'http://www.countryfile.com/sites/default/files/imagecache/160px_wide/cover/2_1.jpg' __author__ = 'Dave Asbury' description = 'The official website of Countryfile Magazine' - # last updated 15/4/12 + # last updated 9/9//12 language = 'en_GB' oldest_article = 30 max_articles_per_feed = 25 @@ -17,13 +16,14 @@ class AdvancedUserRecipe1325006965(BasicNewsRecipe): def get_cover_url(self): soup = self.index_to_soup('http://www.countryfile.com/') cov = soup.find(attrs={'class' : 'imagecache imagecache-160px_wide imagecache-linked imagecache-160px_wide_linked'}) - #print '******** ',cov,' ***' + print '******** ',cov,' ***' cov2 = str(cov) - cov2=cov2[124:-90] - #print '******** ',cov2,' ***' - + cov2=cov2[140:223] + print '******** ',cov2,' ***' + #cov2='http://www.countryfile.com/sites/default/files/imagecache/160px_wide/cover/1b_0.jpg' # try to get cover - if can't get known cover br = browser() + br.set_handle_redirect(False) try: br.open_novisit(cov2) From b5109cbdf06ff0194a12b11547595c59a75edf56 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 17:44:29 +0530 Subject: [PATCH 19/32] Possibly fix a mem leak in device scanning on windows --- src/calibre/utils/windows/winutil.c | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/calibre/utils/windows/winutil.c b/src/calibre/utils/windows/winutil.c index 42b6462313..0134bf21ed 100644 --- a/src/calibre/utils/windows/winutil.c +++ b/src/calibre/utils/windows/winutil.c @@ -168,9 +168,9 @@ winutil_set_debug(PyObject *self, PyObject *args) { return Py_None; } -static LPTSTR +static LPWSTR get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iterate) { - /* Get a the property specified by `property` from the registry for the + /* Get the property specified by `property` from the registry for the * device enumerated by `index` in the collection `hDevInfo`. `iterate` * will be set to `FALSE` if `index` points outside `hDevInfo`. * :return: A string allocated on the heap containing the property or @@ -178,7 +178,7 @@ get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iter */ SP_DEVINFO_DATA DeviceInfoData; DWORD DataT; - LPTSTR buffer = NULL; + LPWSTR buffer = NULL; DWORD buffersize = 0; DeviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA); @@ -187,7 +187,7 @@ get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iter return NULL; } - while(!SetupDiGetDeviceRegistryProperty( + while(!SetupDiGetDeviceRegistryPropertyW( hDevInfo, &DeviceInfoData, property, @@ -209,7 +209,7 @@ get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iter } static BOOL -check_device_id(LPTSTR buffer, unsigned int vid, unsigned int pid) { +check_device_id(LPWSTR buffer, unsigned int vid, unsigned int pid) { WCHAR xVid[9], dVid[9], xPid[9], dPid[9]; unsigned int j; _snwprintf_s(xVid, 9, _TRUNCATE, L"vid_%4.4x", vid); @@ -672,7 +672,8 @@ winutil_get_usb_devices(PyObject *self, PyObject *args) { HDEVINFO hDevInfo; DWORD i; BOOL iterate = TRUE; PyObject *devices, *temp = (PyObject *)1; - LPTSTR buffer; + LPWSTR buffer; + BOOL ok = 1; if (!PyArg_ParseTuple(args, "")) return NULL; @@ -691,16 +692,17 @@ winutil_get_usb_devices(PyObject *self, PyObject *args) { PyErr_Print(); continue; } buffersize = wcslen(buffer); - for (j = 0; j < buffersize; j++) buffer[j] = tolower(buffer[j]); + for (j = 0; j < buffersize; j++) buffer[j] = towlower(buffer[j]); temp = PyUnicode_FromWideChar(buffer, buffersize); PyMem_Free(buffer); if (temp == NULL) { PyErr_NoMemory(); + ok = 0; break; } - PyList_Append(devices, temp); + PyList_Append(devices, temp); Py_DECREF(temp); temp = NULL; } //for - if (temp == NULL) { Py_DECREF(devices); devices = NULL; } + if (!ok) { Py_DECREF(devices); devices = NULL; } SetupDiDestroyDeviceInfoList(hDevInfo); return devices; } @@ -711,7 +713,7 @@ winutil_is_usb_device_connected(PyObject *self, PyObject *args) { unsigned int vid, pid; HDEVINFO hDevInfo; DWORD i; BOOL iterate = TRUE; - LPTSTR buffer; + LPWSTR buffer; int found = FALSE; PyObject *ans; From 113269d00c6e76725a69d354c9c0359eaf032eb1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 20:38:14 +0530 Subject: [PATCH 20/32] Another leak? --- src/calibre/utils/windows/winutil.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/utils/windows/winutil.c b/src/calibre/utils/windows/winutil.c index 0134bf21ed..c00bcc4b41 100644 --- a/src/calibre/utils/windows/winutil.c +++ b/src/calibre/utils/windows/winutil.c @@ -196,7 +196,8 @@ get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iter buffersize, &buffersize)) { if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) { - buffer = (LPTSTR)PyMem_Malloc(2*buffersize); // Twice for bug in Win2k + if (buffer != NULL) { PyMem_Free(buffer); buffer = NULL; } + buffer = (LPWSTR)PyMem_Malloc(2*buffersize); // Twice for bug in Win2k } else { PyMem_Free(buffer); PyErr_SetFromWindowsErr(0); From 814d92d56ca5ac08eeca285c9fe5cbc4c8a6f68b Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 20:39:53 +0530 Subject: [PATCH 21/32] ... --- src/calibre/utils/windows/winutil.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/calibre/utils/windows/winutil.c b/src/calibre/utils/windows/winutil.c index c00bcc4b41..384e2d1e28 100644 --- a/src/calibre/utils/windows/winutil.c +++ b/src/calibre/utils/windows/winutil.c @@ -199,9 +199,8 @@ get_registry_property(HDEVINFO hDevInfo, DWORD index, DWORD property, BOOL *iter if (buffer != NULL) { PyMem_Free(buffer); buffer = NULL; } buffer = (LPWSTR)PyMem_Malloc(2*buffersize); // Twice for bug in Win2k } else { - PyMem_Free(buffer); + if (buffer != NULL) { PyMem_Free(buffer); buffer = NULL; } PyErr_SetFromWindowsErr(0); - buffer = NULL; break; } } //while From 8f095aa63b5e7a2b99f8d42b6af06ea23fcfca81 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 20:45:47 +0530 Subject: [PATCH 22/32] And another potential leak --- src/calibre/utils/windows/winutil.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/utils/windows/winutil.c b/src/calibre/utils/windows/winutil.c index 384e2d1e28..242fe99fa1 100644 --- a/src/calibre/utils/windows/winutil.c +++ b/src/calibre/utils/windows/winutil.c @@ -683,8 +683,10 @@ winutil_get_usb_devices(PyObject *self, PyObject *args) { // Create a Device information set with all USB devices hDevInfo = create_device_info_set(NULL, L"USB", 0, DIGCF_PRESENT | DIGCF_ALLCLASSES); - if (hDevInfo == INVALID_HANDLE_VALUE) + if (hDevInfo == INVALID_HANDLE_VALUE) { + Py_DECREF(devices); return NULL; + } // Enumerate through the set for (i=0; iterate; i++) { buffer = get_registry_property(hDevInfo, i, SPDRP_HARDWAREID, &iterate); From ae34d417f6bd827a2ec6ff72f9c579398900aefa Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 21:10:30 +0530 Subject: [PATCH 23/32] Add function to test for mem leaks in the scanner --- src/calibre/devices/scanner.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index e0bb74fa2a..00eae7b09f 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -329,8 +329,30 @@ class DeviceScanner(object): return device.is_usb_connected(self.devices, debug=debug, only_presence=only_presence) +def test_for_mem_leak(): + from calibre.utils.mem import memory, gc_histogram, diff_hists + import gc + gc.disable() + scanner = DeviceScanner() + scanner.scan() + for i in xrange(3): gc.collect() + + for reps in (10, 100, 1000, 10000): + for i in xrange(3): gc.collect() + h1 = gc_histogram() + startmem = memory() + for i in xrange(reps): + scanner.scan() + for i in xrange(3): gc.collect() + usedmem = memory(startmem) + prints('Memory used in %d repetitions of scan(): %.6f KB'%(reps, + 1024*usedmem)) + prints('Differences in python object counts:') + diff_hists(h1, gc_histogram()) + def main(args=sys.argv): + test_for_mem_leak() return 0 if __name__ == '__main__': From b9f1084b5478c57b78cf524250fa2faca45fa8d6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 21:17:30 +0530 Subject: [PATCH 24/32] ... --- src/calibre/devices/scanner.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 00eae7b09f..aea0c94740 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -335,9 +335,10 @@ def test_for_mem_leak(): gc.disable() scanner = DeviceScanner() scanner.scan() + memory() # load the psutil library for i in xrange(3): gc.collect() - for reps in (10, 100, 1000, 10000): + for reps in (1, 10, 100, 1000, 10000): for i in xrange(3): gc.collect() h1 = gc_histogram() startmem = memory() @@ -345,10 +346,11 @@ def test_for_mem_leak(): scanner.scan() for i in xrange(3): gc.collect() usedmem = memory(startmem) - prints('Memory used in %d repetitions of scan(): %.6f KB'%(reps, + prints('Memory used in %d repetitions of scan(): %.10f KB'%(reps, 1024*usedmem)) prints('Differences in python object counts:') diff_hists(h1, gc_histogram()) + prints() def main(args=sys.argv): From bb16a23be84033924fa400bc98d0143fb874e612 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 21:19:26 +0530 Subject: [PATCH 25/32] ... --- src/calibre/devices/scanner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index aea0c94740..288e9a77a1 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -346,7 +346,7 @@ def test_for_mem_leak(): scanner.scan() for i in xrange(3): gc.collect() usedmem = memory(startmem) - prints('Memory used in %d repetitions of scan(): %.10f KB'%(reps, + prints('Memory used in %d repetitions of scan(): %.5f KB'%(reps, 1024*usedmem)) prints('Differences in python object counts:') diff_hists(h1, gc_histogram()) From bcf5a4acfbad6546fa1885db2dec2708548a05ce Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 21:26:25 +0530 Subject: [PATCH 26/32] Update Metro UK --- recipes/metro_uk.recipe | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/recipes/metro_uk.recipe b/recipes/metro_uk.recipe index 5b7b3a64ed..fcceba4ce7 100644 --- a/recipes/metro_uk.recipe +++ b/recipes/metro_uk.recipe @@ -1,10 +1,10 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1306097511(BasicNewsRecipe): title = u'Metro UK' - description = 'Author Dave Asbury : News as provide by The Metro -UK' + description = 'Author Dave Asbury : News from The Metro - UK' #timefmt = '' __author__ = 'Dave Asbury' - #last update 4/8/12 + #last update 9/9/12 cover_url = 'http://profile.ak.fbcdn.net/hprofile-ak-snc4/276636_117118184990145_2132092232_n.jpg' no_stylesheets = True oldest_article = 1 @@ -17,23 +17,24 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe): language = 'en_GB' masthead_url = 'http://e-edition.metro.co.uk/images/metro_logo.gif' extra_css = ''' - h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:1.6em;} + h1{font-family:Arial,Helvetica,sans-serif; font-weight:900;font-size:1.6em;} h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:1.2em;} p{font-family:Arial,Helvetica,sans-serif;font-size:1.0em;} body{font-family:Helvetica,Arial,sans-serif;font-size:1.0em;} - ''' + ''' keep_only_tags = [ - #dict(name='h1'), - #dict(name='h2'), - #dict(name='div', attrs={'class' : ['row','article','img-cnt figure','clrd']}) - #dict(name='h3'), - #dict(attrs={'class' : 'BText'}), - ] + #dict(name='h1'), + #dict(name='h2'), + #dict(name='div', attrs={'class' : ['row','article','img-cnt figure','clrd']}) + #dict(name='h3'), + #dict(attrs={'class' : 'BText'}), + ] remove_tags = [ + dict(name='div',attrs={'class' : 'art-fd fd-gr1-b clrd'}), dict(name='span',attrs={'class' : 'share'}), - dict(name='li'), - dict(attrs={'class' : ['twitter-share-button','header-forms','hdr-lnks','close','art-rgt','fd-gr1-b clrd google-article','news m12 clrd clr-b p5t shareBtm','item-ds csl-3-img news','c-1of3 c-last','c-1of1','pd','item-ds csl-3-img sport']}), - dict(attrs={'id' : ['','sky-left','sky-right','ftr-nav','and-ftr','notificationList','logo','miniLogo','comments-news','metro_extras']}) + dict(name='li'), + dict(attrs={'class' : ['twitter-share-button','header-forms','hdr-lnks','close','art-rgt','fd-gr1-b clrd google-article','news m12 clrd clr-b p5t shareBtm','item-ds csl-3-img news','c-1of3 c-last','c-1of1','pd','item-ds csl-3-img sport']}), + dict(attrs={'id' : ['','sky-left','sky-right','ftr-nav','and-ftr','notificationList','logo','miniLogo','comments-news','metro_extras']}) ] remove_tags_before = dict(name='h1') #remove_tags_after = dict(attrs={'id':['topic-buttons']}) From 7bba2267b0a299ccbc7586fd0f7b8c908fa80255 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 22:19:39 +0530 Subject: [PATCH 27/32] Add testing for leaks in the windows pnp scanner --- src/calibre/devices/scanner.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/scanner.py b/src/calibre/devices/scanner.py index 288e9a77a1..156ed981cb 100644 --- a/src/calibre/devices/scanner.py +++ b/src/calibre/devices/scanner.py @@ -338,7 +338,7 @@ def test_for_mem_leak(): memory() # load the psutil library for i in xrange(3): gc.collect() - for reps in (1, 10, 100, 1000, 10000): + for reps in (1, 10, 100, 1000): for i in xrange(3): gc.collect() h1 = gc_histogram() startmem = memory() @@ -352,6 +352,22 @@ def test_for_mem_leak(): diff_hists(h1, gc_histogram()) prints() + if not iswindows: + return + + for reps in (1, 10, 100, 1000): + for i in xrange(3): gc.collect() + h1 = gc_histogram() + startmem = memory() + for i in xrange(reps): + win_pnp_drives() + for i in xrange(3): gc.collect() + usedmem = memory(startmem) + prints('Memory used in %d repetitions of pnp_scan(): %.5f KB'%(reps, + 1024*usedmem)) + prints('Differences in python object counts:') + diff_hists(h1, gc_histogram()) + prints() def main(args=sys.argv): test_for_mem_leak() From d638319b307d97fdef2ccb8a0c31152db2e8c84f Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Sun, 9 Sep 2012 22:48:22 +0530 Subject: [PATCH 28/32] Fix a mem leak in the windows PNP drive scanner --- src/calibre/utils/windows/winutil.c | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/calibre/utils/windows/winutil.c b/src/calibre/utils/windows/winutil.c index 242fe99fa1..64c943ffa5 100644 --- a/src/calibre/utils/windows/winutil.c +++ b/src/calibre/utils/windows/winutil.c @@ -607,31 +607,28 @@ winutil_get_removable_drives(PyObject *self, PyObject *args) { return NULL; } - ddebug = PyObject_IsTrue(pdebug); + // Find all removable drives + for (j = 0; j < MAX_DRIVES; j++) g_drives[j].letter = 0; + if (!get_all_removable_disks(g_drives)) return NULL; volumes = PyDict_New(); - if (volumes == NULL) return NULL; - - - for (j = 0; j < MAX_DRIVES; j++) g_drives[j].letter = 0; - - // Find all removable drives - if (!get_all_removable_disks(g_drives)) { - return NULL; - } + if (volumes == NULL) return PyErr_NoMemory(); + ddebug = PyObject_IsTrue(pdebug); hDevInfo = create_device_info_set((LPGUID)&GUID_DEVINTERFACE_VOLUME, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); - if (hDevInfo == INVALID_HANDLE_VALUE) return NULL; + if (hDevInfo == INVALID_HANDLE_VALUE) { Py_DECREF(volumes); return NULL; } // Enumerate through the set for (i=0; iterate; i++) { candidates = PyList_New(0); - if (candidates == NULL) return PyErr_NoMemory(); + if (candidates == NULL) { Py_DECREF(volumes); return PyErr_NoMemory();} interfaceDetailData = get_device_ancestors(hDevInfo, i, candidates, &iterate, ddebug); if (interfaceDetailData == NULL) { - PyErr_Print(); continue; + PyErr_Print(); + Py_DECREF(candidates); candidates = NULL; + continue; } length = wcslen(interfaceDetailData->DevicePath); @@ -653,12 +650,13 @@ winutil_get_removable_drives(PyObject *self, PyObject *args) { key = PyBytes_FromFormat("%c", (char)g_drives[j].letter); if (key == NULL) return PyErr_NoMemory(); PyDict_SetItem(volumes, key, candidates); - Py_DECREF(candidates); + Py_DECREF(key); key = NULL; break; } } } + Py_XDECREF(candidates); candidates = NULL; PyMem_Free(interfaceDetailData); } //for From 2e262a144939cdecb3075167f8f920b7e35cc773 Mon Sep 17 00:00:00 2001 From: Eric Lammerts <eric@lammerts.org> Date: Sun, 9 Sep 2012 15:47:17 -0400 Subject: [PATCH 29/32] Update RSS feeds for De Volkskrant --- recipes/volksrant.recipe | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/recipes/volksrant.recipe b/recipes/volksrant.recipe index 386cb1e729..b3629ee4e0 100644 --- a/recipes/volksrant.recipe +++ b/recipes/volksrant.recipe @@ -73,14 +73,20 @@ class AdvancedUserRecipe1249039563(BasicNewsRecipe): Change Log: Date: 10/15/2010 Feeds updated by Martin Tarenskeen + Date: 09/09/2012 + Feeds updated by Eric Lammerts ''' feeds = [ - (u'Laatste Nieuws', u'http://www.volkskrant.nl/rss/laatstenieuws.rss'), - (u'Binnenland', u'http://www.volkskrant.nl/rss/nederland.rss'), - (u'Buitenland', u'http://www.volkskrant.nl/rss/internationaal.rss'), - (u'Economie', u'http://www.volkskrant.nl/rss/economie.rss'), - (u'Sport', u'http://www.volkskrant.nl/rss/sport.rss'), - (u'Cultuur', u'http://www.volkskrant.nl/rss/kunst.rss'), - (u'Gezondheid & Wetenschap', u'http://www.volkskrant.nl/rss/wetenschap.rss'), - (u'Internet & Media', u'http://www.volkskrant.nl/rss/media.rss') ] + (u'Nieuws', u'http://www.volkskrant.nl/nieuws/rss.xml'), + (u'Binnenland', u'http://www.volkskrant.nl/nieuws/binnenland/rss.xml'), + (u'Buitenland', u'http://www.volkskrant.nl/buitenland/rss.xml'), + (u'Economie', u'http://www.volkskrant.nl/nieuws/economie/rss.xml'), + (u'Politiek', u'http://www.volkskrant.nl/politiek/rss.xml'), + (u'Sport', u'http://www.volkskrant.nl/sport/rss.xml'), + (u'Cultuur', u'http://www.volkskrant.nl/nieuws/cultuur/rss.xml'), + (u'Gezondheid & wetenschap', u'http://www.volkskrant.nl/nieuws/gezondheid--wetenschap/rss.xml'), + (u'Tech & Media', u'http://www.volkskrant.nl/tech-media/rss.xml'), + (u'Reizen', u'http://www.volkskrant.nl/nieuws/reizen/rss.xml'), + (u'Opinie', u'http://www.volkskrant.nl/opinie/rss.xml'), + (u'Opmerkelijk', u'http://www.volkskrant.nl/nieuws/opmerkelijk/rss.xml') ] From 61e13950a36568fe52ed18aee1596ca6659fc849 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 10 Sep 2012 08:55:39 +0530 Subject: [PATCH 30/32] Fix ebook catalog generation on linux systems where the encoding is not UTF-8. Fixes #1048404 (Catalog generation fails) --- src/calibre/library/catalogs/epub_mobi_builder.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 3fc0df58b2..f592437916 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -211,15 +211,15 @@ class CatalogBuilder(object): (str): sort key """ if not book['series']: - fs = '{:<%d}!{!s}' % longest_author_sort + fs = u'{:<%d}!{!s}' % longest_author_sort key = fs.format(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')) - fs = '{:<%d}~{!s}{!s}' % longest_author_sort + series_index = u'%04d%s' % (integer, str(u'%0.4f' % fraction).lstrip(u'0')) + fs = u'{:<%d}~{!s}{!s}' % longest_author_sort key = fs.format(capitalize(book['author_sort']), self.generate_sort_title(book['series']), series_index) @@ -2464,7 +2464,9 @@ class CatalogBuilder(object): title_str=title_str, xmlns=XHTML_NS, ) - + for k, v in args.iteritems(): + if isbytestring(v): + args[k] = v.decode('utf-8') generated_html = P('catalog/template.xhtml', data=True).decode('utf-8').format(**args) generated_html = substitute_entites(generated_html) From 9aef5216e55e784addc51ad067709c855f6293f4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 10 Sep 2012 09:26:34 +0530 Subject: [PATCH 31/32] ... --- src/calibre/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/constants.py b/src/calibre/constants.py index 2308abb567..d70c186502 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -57,7 +57,7 @@ else: # On linux, unicode arguments to os file functions are coerced to an ascii # bytestring if sys.getfilesystemencoding() == 'ascii', which is # just plain dumb. So issue a warning. - print ('WARNING: You do not have the LANG environment variable set. ' + print ('WARNING: You do not have the LANG environment variable set correctly. ' 'This will cause problems with non-ascii filenames. ' 'Set it to something like en_US.UTF-8.\n') except: From 90f7eb8eec2679c3d8b62c56932b94bdfb981592 Mon Sep 17 00:00:00 2001 From: Kovid Goyal <kovid@kovidgoyal.net> Date: Mon, 10 Sep 2012 10:09:14 +0530 Subject: [PATCH 32/32] When downloading metadata for many books, if some of them fail, add an option to the downloaded message to show the failed books in the main book list, so that they can be individually processed easily --- src/calibre/gui2/actions/edit_metadata.py | 36 +++++++++++----- src/calibre/gui2/proceed.py | 52 +++++++++++++++++------ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py index c2558d56ae..26d15d0a83 100644 --- a/src/calibre/gui2/actions/edit_metadata.py +++ b/src/calibre/gui2/actions/edit_metadata.py @@ -80,7 +80,7 @@ class EditMetadataAction(InterfaceAction): Dispatcher(self.metadata_downloaded), ensure_fields=ensure_fields) - def cleanup_bulk_download(self, tdir): + def cleanup_bulk_download(self, tdir, *args): try: shutil.rmtree(tdir, ignore_errors=True) except: @@ -108,22 +108,26 @@ class EditMetadataAction(InterfaceAction): 'Proceed with updating the metadata in your library?')%len(id_map) show_copy_button = False + checkbox_msg = None if failed_ids or failed_covers: show_copy_button = True num = len(failed_ids.union(failed_covers)) msg += '<p>'+_('Could not download metadata and/or covers for %d of the books. Click' ' "Show details" to see which books.')%num + checkbox_msg = _('Show the &failed books in the main book list ' + 'after updating metadata') - payload = (id_map, tdir, log_file, lm_map) - self.gui.proceed_question(self.apply_downloaded_metadata, - payload, log_file, - _('Download log'), _('Download complete'), msg, + payload = (id_map, tdir, log_file, lm_map, + failed_ids.union(failed_covers)) + self.gui.proceed_question(self.apply_downloaded_metadata, payload, + log_file, _('Download log'), _('Download complete'), msg, det_msg=det_msg, show_copy_button=show_copy_button, - cancel_callback=lambda x:self.cleanup_bulk_download(tdir), - log_is_file=True) + cancel_callback=partial(self.cleanup_bulk_download, tdir), + log_is_file=True, checkbox_msg=checkbox_msg, + checkbox_checked=False) - def apply_downloaded_metadata(self, payload): - good_ids, tdir, log_file, lm_map = payload + def apply_downloaded_metadata(self, payload, *args): + good_ids, tdir, log_file, lm_map, failed_ids = payload if not good_ids: return @@ -162,8 +166,18 @@ class EditMetadataAction(InterfaceAction): cov = None id_map[bid] = (opf, cov) - self.apply_metadata_changes(id_map, callback=lambda x: - self.cleanup_bulk_download(tdir)) + restrict_to_failed = bool(args and args[0]) + if restrict_to_failed: + db.data.set_marked_ids(failed_ids) + + self.apply_metadata_changes(id_map, + callback=partial(self.downloaded_metadata_applied, tdir, + restrict_to_failed)) + + def downloaded_metadata_applied(self, tdir, restrict_to_failed, *args): + if restrict_to_failed: + self.gui.search.set_search_string('marked:true') + self.cleanup_bulk_download(tdir) # }}} diff --git a/src/calibre/gui2/proceed.py b/src/calibre/gui2/proceed.py index 1074792096..9bdf48e086 100644 --- a/src/calibre/gui2/proceed.py +++ b/src/calibre/gui2/proceed.py @@ -11,18 +11,18 @@ from collections import namedtuple from PyQt4.Qt import (QDialog, Qt, QLabel, QGridLayout, QPixmap, QDialogButtonBox, QApplication, QSize, pyqtSignal, QIcon, - QPlainTextEdit) + QPlainTextEdit, QCheckBox) from calibre.constants import __version__ from calibre.gui2.dialogs.message_box import ViewLog Question = namedtuple('Question', 'payload callback cancel_callback ' 'title msg html_log log_viewer_title log_is_file det_msg ' - 'show_copy_button') + 'show_copy_button checkbox_msg checkbox_checked') class ProceedQuestion(QDialog): - ask_question = pyqtSignal(object, object) + ask_question = pyqtSignal(object, object, object) def __init__(self, parent): QDialog.__init__(self, parent) @@ -62,10 +62,13 @@ class ProceedQuestion(QDialog): self.bb.setStandardButtons(self.bb.Yes|self.bb.No) self.bb.button(self.bb.Yes).setDefault(True) + self.checkbox = QCheckBox('', self) + l.addWidget(ic, 0, 0, 1, 1) l.addWidget(msg, 0, 1, 1, 1) - l.addWidget(self.det_msg, 1, 0, 1, 2) - l.addWidget(self.bb, 2, 0, 1, 2) + l.addWidget(self.checkbox, 1, 0, 1, 2) + l.addWidget(self.det_msg, 2, 0, 1, 2) + l.addWidget(self.bb, 3, 0, 1, 2) self.ask_question.connect(self.do_ask_question, type=Qt.QueuedConnection) @@ -82,19 +85,28 @@ class ProceedQuestion(QDialog): if self.questions: payload, callback, cancel_callback = self.questions[0][:3] self.questions = self.questions[1:] - self.ask_question.emit(callback, payload) + cb = None + if self.checkbox.isVisible(): + cb = bool(self.checkbox.isChecked()) + self.ask_question.emit(callback, payload, cb) self.hide() def reject(self): if self.questions: payload, callback, cancel_callback = self.questions[0][:3] self.questions = self.questions[1:] - self.ask_question.emit(cancel_callback, payload) + cb = None + if self.checkbox.isVisible(): + cb = bool(self.checkbox.isChecked()) + self.ask_question.emit(cancel_callback, payload, cb) self.hide() - def do_ask_question(self, callback, payload): + def do_ask_question(self, callback, payload, checkbox_checked): if callable(callback): - callback(payload) + args = [payload] + if checkbox_checked is not None: + args.append(checkbox_checked) + callback(*args) self.show_question() def toggle_det_msg(self, *args): @@ -122,6 +134,10 @@ class ProceedQuestion(QDialog): self.det_msg.setVisible(False) self.det_msg_toggle.setVisible(bool(question.det_msg)) self.det_msg_toggle.setText(self.show_det_msg) + self.checkbox.setVisible(question.checkbox_msg is not None) + if question.checkbox_msg is not None: + self.checkbox.setText(question.checkbox_msg) + self.checkbox.setChecked(question.checkbox_checked) self.do_resize() self.show() self.bb.button(self.bb.Yes).setDefault(True) @@ -129,10 +145,10 @@ class ProceedQuestion(QDialog): def __call__(self, callback, payload, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, cancel_callback=None, - log_is_file=False): + log_is_file=False, checkbox_msg=None, checkbox_checked=False): ''' A non modal popup that notifies the user that a background task has - been completed. This class guarantees that onlya single popup is + been completed. This class guarantees that only a single popup is visible at any one time. Other requests are queued and displayed after the user dismisses the current popup. @@ -147,11 +163,18 @@ class ProceedQuestion(QDialog): :param msg: The msg to display :param det_msg: Detailed message :param log_is_file: If True the html_log parameter is interpreted as - the path to a file on disk containing the log encoded with utf-8 + the path to a file on disk containing the log + encoded with utf-8 + :param checkbox_msg: If not None, a checkbox is displayed in the + dialog, showing this message. The callback is + called with both the payload and the state of the + checkbox as arguments. + :param checkbox_checked: If True the checkbox is checked by default. + ''' question = Question(payload, callback, cancel_callback, title, msg, html_log, log_viewer_title, log_is_file, det_msg, - show_copy_button) + show_copy_button, checkbox_msg, checkbox_checked) self.questions.append(question) self.show_question() @@ -169,7 +192,8 @@ def main(): from calibre.gui2 import Application app = Application([]) p = ProceedQuestion(None) - p(lambda p:None, None, 'ass', 'ass', 'testing', 'testing') + p(lambda p:None, None, 'ass', 'ass', 'testing', 'testing', + checkbox_msg='testing the ruddy checkbox', det_msg='details') p.exec_() app