From 06cbaca2e6160b83467c16c5737462a4c312816a Mon Sep 17 00:00:00 2001 From: ldolse Date: Wed, 19 Jan 2011 22:21:11 +0800 Subject: [PATCH 01/39] start at enabling some heuristics options by default --- src/calibre/ebooks/conversion/cli.py | 1 + src/calibre/ebooks/conversion/plumber.py | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index b5c057b0f9..8cd4f124d5 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -69,6 +69,7 @@ def option_recommendation_to_cli_option(add_option, rec): opt = rec.option switches = ['-'+opt.short_switch] if opt.short_switch else [] switches.append('--'+opt.long_switch) + flip_switches = ['italicize_common_cases', 'markup_chapter_headings', 'unwrap_lines', 'dehyphenate', 'fix_indents'] attrs = dict(dest=opt.name, help=opt.help, choices=opt.choices, default=rec.recommended_value) if isinstance(rec.recommended_value, type(True)): diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 04ee892c19..7dd977cd7b 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -490,19 +490,19 @@ OptionRecommendation(name='enable_heuristics', 'heuristic processing to take place.')), OptionRecommendation(name='markup_chapter_headings', - recommended_value=False, level=OptionRecommendation.LOW, + recommended_value=True, level=OptionRecommendation.LOW, help=_('Detect unformatted chapter headings and sub headings. Change ' 'them to h2 and h3 tags. This setting will not create a TOC, ' 'but can be used in conjunction with structure detection to create ' 'one.')), OptionRecommendation(name='italicize_common_cases', - recommended_value=False, level=OptionRecommendation.LOW, + recommended_value=True, level=OptionRecommendation.LOW, help=_('Look for common words and patterns that denote ' 'italics and italicize them.')), OptionRecommendation(name='fix_indents', - recommended_value=False, level=OptionRecommendation.LOW, + recommended_value=True, level=OptionRecommendation.LOW, help=_('Turn indentation created from multiple non-breaking space entities ' 'into CSS indents.')), @@ -515,7 +515,7 @@ OptionRecommendation(name='html_unwrap_factor', 'be reduced')), OptionRecommendation(name='unwrap_lines', - recommended_value=False, level=OptionRecommendation.LOW, + recommended_value=True, level=OptionRecommendation.LOW, help=_('Unwrap lines using punctuation and other formatting clues.')), OptionRecommendation(name='delete_blank_paragraphs', @@ -530,7 +530,7 @@ OptionRecommendation(name='format_scene_breaks', 'horizontal rules.')), OptionRecommendation(name='dehyphenate', - recommended_value=False, level=OptionRecommendation.LOW, + recommended_value=True, level=OptionRecommendation.LOW, help=_('Analyze hyphenated words throughout the document. The ' 'document itself is used as a dictionary to determine whether hyphens ' 'should be retained or removed.')), From 27b83959356754d8c0516d9d1da174acb3b0cf2f Mon Sep 17 00:00:00 2001 From: ldolse Date: Wed, 19 Jan 2011 23:09:18 +0800 Subject: [PATCH 02/39] document updates --- src/calibre/manual/conversion.rst | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/calibre/manual/conversion.rst b/src/calibre/manual/conversion.rst index de27a5f5bb..6ec986f26a 100644 --- a/src/calibre/manual/conversion.rst +++ b/src/calibre/manual/conversion.rst @@ -260,11 +260,11 @@ The Output profile also controls the screen size. This will cause, for example, Heuristic Processing --------------------- -Heuristic Processing provides a variety of functions which can be used that try to detect and correct +Heuristic Processing provides a variety of functions which can be used to try and detect and correct common problems in poorly formatted input documents. Use these functions if your input document suffers -from bad formatting. Because these functions rely on common patterns, be aware that in some cases an +from poor formatting. Because these functions rely on common patterns, be aware that in some cases an option may lead to worse results, so use with care. As an example, several of these options will -remove all non-breaking-space entities. +remove all non-breaking-space entities, or may include false positive matches relating to the function. :guilabel:`Enable heuristic processing` This option activates |app|'s Heuristic Processing stage of the conversion pipeline. @@ -283,7 +283,7 @@ remove all non-breaking-space entities. correction, then this value should be reduced to somewhere between 0.1 and 0.2. :guilabel:`Detect and markup unformatted chapter headings and sub headings` - If your document does not have Chapter Markers and titles formatted differently from the rest of the text, + If your document does not have chapter headings and titles formatted differently from the rest of the text, |app| can use this option to attempt detection them and surround them with heading tags.

tags are used for chapter headings;

tags are used for any titles that are detected. @@ -331,21 +331,23 @@ remove all non-breaking-space entities. Some documents use a convention of defining text indents using non-breaking space entities. When this option is enabled |app| will attempt to detect this sort of formatting and convert them to a 3% text indent using css. -.. search-replace: +.. _search-replace: Search & Replace --------------------- -These options are useful primarily for conversion of PDF documents. Often, the conversion leaves -behind page headers and footers in the text. These options use regular expressions to try and detect -the headers and footers and remove them. Remember that they operate on the intermediate XHTML produced -by the conversion pipeline. There is also a wizard to help you customize the regular expressions for -your document. These options can also be used for generic search and replace of any content by additionally -specifying a replacement expression. +These options are useful primarily for conversion of PDF documents or OCR conversions, though they can +also be used to fix many document specific problems. As an example, some conversions can leaves behind page +headers and footers in the text. These options use regular expressions to try and detect headers, footers, +or other arbitrary text and remove or replace them. Remember that they operate on the intermediate XHTML produced +by the conversion pipeline. There is a wizard to help you customize the regular expressions for +your document. Click the magic wand beside the expression box, and click the 'Test' button after composing +your search expression. Successful matches will be highlighted in Yellow. -The search works by using a python regular expression. All matched text is simply removed from -the document or replaced using the replacement pattern. You can learn more about regular expressions and -their syntax at :ref:`regexptutorial`. +The search works by using a python regular expression. All matched text is simply removed from +the document or replaced using the replacement pattern. The replacement pattern is optional, if left blank +then text matching the search pattern will be deleted from the document. You can learn more about regular expressions +and their syntax at :ref:`regexptutorial`. .. _structure-detection: From dd30aa375ae5468f4961c7c88718db503d1aee5d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 13:18:51 -0700 Subject: [PATCH 03/39] Fix #8458 (Las Vegas Review Journal: Extra "stuff" in each article) --- resources/recipes/las_vegas_review.recipe | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/resources/recipes/las_vegas_review.recipe b/resources/recipes/las_vegas_review.recipe index 9292c105a4..ea51c2cf78 100644 --- a/resources/recipes/las_vegas_review.recipe +++ b/resources/recipes/las_vegas_review.recipe @@ -3,12 +3,17 @@ from calibre.web.feeds.news import BasicNewsRecipe class AdvancedUserRecipe1274742400(BasicNewsRecipe): title = u'Las Vegas Review Journal' - __author__ = 'Joel' + __author__ = 'Kovid Goyal' language = 'en' oldest_article = 7 max_articles_per_feed = 100 + keep_only_tags = [dict(id='content-main')] + remove_tags = [dict(id=['right-col-content', 'trending-topics']), + {'class':['ppy-outer']} + ] + no_stylesheets = True feeds = [ (u'News', u'http://www.lvrj.com/news.rss'), From 33f524d985a2358bca7b9a5d0bb42d4978dec9cc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 16:59:20 -0700 Subject: [PATCH 04/39] ... --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 4ca2072317..a2ced18e0f 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -724,7 +724,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): au = _('Unknown') au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')]) self.authors.setEditText(au) - + self.authors.set_separator('&') self.authors.set_space_before_sep(True) self.authors.update_items_cache(self.db.all_author_names()) From add04ff930f0725bc4af6dfa4eacc0c01ebb98f1 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 19 Jan 2011 17:01:34 -0700 Subject: [PATCH 05/39] GwR revisions to catalog, making 'By Authors' optional --- src/calibre/gui2/catalog/catalog_epub_mobi.ui | 6 +- src/calibre/library/catalog.py | 122 +++++++++++++----- 2 files changed, 92 insertions(+), 36 deletions(-) diff --git a/src/calibre/gui2/catalog/catalog_epub_mobi.ui b/src/calibre/gui2/catalog/catalog_epub_mobi.ui index 6e057a6481..5136f86fae 100644 --- a/src/calibre/gui2/catalog/catalog_epub_mobi.ui +++ b/src/calibre/gui2/catalog/catalog_epub_mobi.ui @@ -35,7 +35,7 @@ - Sections to include in catalog. All catalogs include 'Books by Author'. + Sections to include in catalog. Included sections @@ -79,13 +79,13 @@ - false + true Books by Author - true + false diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index ae600a29f9..6b607b7c1f 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -29,7 +29,6 @@ FIELDS = ['all', 'author_sort', 'authors', 'comments', 'series_index', 'series', 'size', 'tags', 'timestamp', 'title', 'uuid'] - #Allowed fields for template TEMPLATE_ALLOWED_FIELDS = [ 'author_sort', 'authors', 'id', 'isbn', 'pubdate', 'publisher', 'series_index', 'series', 'tags', 'timestamp', 'title', 'uuid' ] @@ -605,43 +604,42 @@ class EPUB_MOBI(CatalogPlugin): "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-authors', - default=True, + default=False, dest='generate_authors', action = 'store_true', - help=_("Include 'Authors' section in catalog." - "This switch is ignored - Books By Author section is always generated." + help=_("Include 'Authors' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-descriptions', - default=True, + default=False, dest='generate_descriptions', action = 'store_true', - help=_("Include book descriptions in catalog.\n" + help=_("Include 'Descriptions' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-genres', - default=True, + default=False, dest='generate_genres', action = 'store_true', help=_("Include 'Genres' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-titles', - default=True, + default=False, dest='generate_titles', action = 'store_true', help=_("Include 'Titles' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-series', - default=True, + default=False, dest='generate_series', action = 'store_true', help=_("Include 'Series' section in catalog.\n" "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--generate-recently-added', - default=True, + default=False, dest='generate_recently_added', action = 'store_true', help=_("Include 'Recently Added' section in catalog.\n" @@ -976,7 +974,7 @@ class EPUB_MOBI(CatalogPlugin): self.__thumbWidth = 0 self.__thumbHeight = 0 self.__title = opts.catalog_title - self.__totalSteps = 8.0 + self.__totalSteps = 6.0 self.__useSeriesPrefixInTitlesSection = False self.__verbose = opts.verbose @@ -1014,17 +1012,21 @@ class EPUB_MOBI(CatalogPlugin): (self.__archive_path, float(cached_thumb_width))) # Tweak build steps based on optional sections: 1 call for HTML, 1 for NCX + incremental_jobs = 0 + if self.opts.generate_authors: + incremental_jobs += 2 if self.opts.generate_titles: - self.__totalSteps += 2 + incremental_jobs += 2 if self.opts.generate_recently_added: - self.__totalSteps += 2 + incremental_jobs += 2 if self.generateRecentlyRead: - self.__totalSteps += 2 + incremental_jobs += 2 if self.opts.generate_series: - self.__totalSteps += 2 + incremental_jobs += 2 if self.opts.generate_descriptions: # +1 thumbs - self.__totalSteps += 3 + incremental_jobs += 3 + self.__totalSteps += incremental_jobs # Load section list templates templates = [] @@ -1358,13 +1360,21 @@ class EPUB_MOBI(CatalogPlugin): if self.opts.generate_descriptions: self.generateThumbnails() self.generateHTMLDescriptions() - self.generateHTMLByAuthor() + if self.opts.generate_authors: + self.generateHTMLByAuthor() if self.opts.generate_titles: self.generateHTMLByTitle() if self.opts.generate_series: self.generateHTMLBySeries() if self.opts.generate_genres: self.generateHTMLByTags() + # If this is the only Section, and there are no genres, bail + if self.opts.section_list == ['Genres'] and not self.genres: + error_msg = _("No Genres found to catalog.\nCheck 'Excluded genres'\nin E-book options.\n") + self.opts.log.error(error_msg) + self.error.append(_('No books available to catalog')) + self.error.append(error_msg) + return False if self.opts.generate_recently_added: self.generateHTMLByDateAdded() if self.generateRecentlyRead: @@ -1372,7 +1382,8 @@ class EPUB_MOBI(CatalogPlugin): self.generateOPF() self.generateNCXHeader() - self.generateNCXByAuthor("Authors") + if self.opts.generate_authors: + self.generateNCXByAuthor("Authors") if self.opts.generate_titles: self.generateNCXByTitle("Titles") if self.opts.generate_series: @@ -1879,7 +1890,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Link to author emTag = Tag(soup, "em") aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) aTag.insert(0, NavigableString(book['author'])) emTag.insert(0,aTag) pBookTag.insert(ptc, emTag) @@ -2149,7 +2161,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) pAuthorTag = Tag(soup, "p") pAuthorTag['class'] = "author_index" aTag = Tag(soup, "a") - aTag['name'] = "%s" % self.generateAuthorAnchor(current_author) + if self.opts.generate_authors: + aTag['name'] = "%s" % self.generateAuthorAnchor(current_author) aTag.insert(0,NavigableString(current_author)) pAuthorTag.insert(0,aTag) divTag.insert(dtc,pAuthorTag) @@ -2276,7 +2289,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Link to author emTag = Tag(soup, "em") aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) emTag.insert(0,aTag) pBookTag.insert(ptc, emTag) @@ -2425,7 +2439,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Link to author emTag = Tag(soup, "em") aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) emTag.insert(0,aTag) pBookTag.insert(ptc, emTag) @@ -2473,7 +2488,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Link to author emTag = Tag(soup, "em") aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(new_entry['author'])) aTag.insert(0, NavigableString(new_entry['author'])) emTag.insert(0,aTag) pBookTag.insert(ptc, emTag) @@ -2692,7 +2708,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Link to author aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(escape(' & '.join(book['authors'])))) aTag.insert(0, NavigableString(' & '.join(book['authors']))) pBookTag.insert(ptc, aTag) @@ -3074,10 +3091,34 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) textTag.insert(0, NavigableString(self.title)) navLabelTag.insert(0, textTag) navPointTag.insert(0, navLabelTag) - contentTag = Tag(soup, 'content') - #contentTag['src'] = "content/book_%d.html" % int(self.booksByTitle[0]['id']) - contentTag['src'] = "content/ByAlphaAuthor.html" - navPointTag.insert(1, contentTag) + + if self.opts.generate_authors: + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/ByAlphaAuthor.html" + navPointTag.insert(1, contentTag) + elif self.opts.generate_titles: + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/ByAlphaTitle.html" + navPointTag.insert(1, contentTag) + elif self.opts.generate_series: + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/BySeries.html" + navPointTag.insert(1, contentTag) + elif self.opts.generate_genres: + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/ByGenres.html" + navPointTag.insert(1, contentTag) + elif self.opts.generate_recently_added: + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/ByDateAdded.html" + navPointTag.insert(1, contentTag) + else: + sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \ + else self.booksByTitle + contentTag = Tag(soup, 'content') + contentTag['src'] = "content/book_%d.html" % int(sort_descriptions_by[0]['id']) + navPointTag.insert(1, contentTag) + cmiTag = Tag(soup, '%s' % 'calibre:meta-img') cmiTag['name'] = "mastheadImage" cmiTag['src'] = "images/mastheadImage.gif" @@ -4140,7 +4181,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) pAuthorTag = Tag(soup, "p") pAuthorTag['class'] = "author_index" aTag = Tag(soup, "a") - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) aTag.insert(0, book['author']) pAuthorTag.insert(0,aTag) divTag.insert(dtc,pAuthorTag) @@ -4371,7 +4413,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Insert the author link (always) aTag = body.find('a', attrs={'class':'author'}) - aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", + if self.opts.generate_authors: + aTag['href'] = "%s.html#%s" % ("ByAlphaAuthor", self.generateAuthorAnchor(book['author'])) if publisher == ' ': @@ -4860,6 +4903,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) opts.basename = "Catalog" opts.cli_environment = not hasattr(opts,'sync') + + # Hard-wired to always sort descriptions by author, with series after non-series opts.sort_descriptions_by_author = True build_log = [] @@ -4898,14 +4943,13 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) if opts_dict['ids']: build_log.append(" book count: %d" % len(opts_dict['ids'])) - ''' sections_list = [] if opts.generate_authors: sections_list.append('Authors') - ''' - sections_list = ['Authors'] if opts.generate_titles: sections_list.append('Titles') + if opts.generate_series: + sections_list.append('Series') if opts.generate_genres: sections_list.append('Genres') if opts.generate_recently_added: @@ -4913,7 +4957,18 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) if opts.generate_descriptions: sections_list.append('Descriptions') + if not sections_list: + opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***') + opts.log.warn('When invoking from the CLI, add one or more of the Section switches:\n' + ' --generate-authors\n' + ' --generate-titles\n' + ' --generate-series\n' + ' --generate-genres\n' + ' --generate-recently-added\n' + ' --generate-descriptions') + return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] build_log.append(u" Sections: %s" % ', '.join(sections_list)) + opts.section_list = sections_list # Limit thumb_width to 1.0" - 2.0" try: @@ -4948,6 +5003,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Launch the Catalog builder catalog = self.CatalogBuilder(db, opts, self, report_progress=notification) + if opts.verbose: log.info(" Begin catalog source generation") catalog.createDirectoryStructure() From 6d8f1fc1854b7a68f0cae03e72e06426f97feb97 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 19 Jan 2011 17:41:09 -0700 Subject: [PATCH 06/39] GwR revisions to catalog generator --- src/calibre/library/catalog.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 6b607b7c1f..b52e3785bd 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -1519,7 +1519,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) for tag in exclude_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 @@ -4958,15 +4957,18 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) sections_list.append('Descriptions') if not sections_list: - opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***') - opts.log.warn('When invoking from the CLI, add one or more of the Section switches:\n' - ' --generate-authors\n' - ' --generate-titles\n' - ' --generate-series\n' - ' --generate-genres\n' - ' --generate-recently-added\n' - ' --generate-descriptions') - return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] + if opts.cli_environment: + opts.log.warn('*** No Section switches specified, enabling all Sections ***') + opts.generate_authors = True + opts.generate_titles = True + opts.generate_series = True + opts.generate_genres = True + opts.generate_recently_added = True + opts.generate_descriptions = True + sections_list = ['Authors','Titles','Series','Genres','Recently Added','Descriptions'] + else: + opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***') + return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] build_log.append(u" Sections: %s" % ', '.join(sections_list)) opts.section_list = sections_list From 9b80ef4b50597d61f07956b7da656e74ae6016e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 17:44:16 -0700 Subject: [PATCH 07/39] Gulf News by DM. Fixes #8466 (New recipe for UAE Gulf News) --- resources/recipes/gulfnews.recipe | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 resources/recipes/gulfnews.recipe diff --git a/resources/recipes/gulfnews.recipe b/resources/recipes/gulfnews.recipe new file mode 100644 index 0000000000..4c40aa3f37 --- /dev/null +++ b/resources/recipes/gulfnews.recipe @@ -0,0 +1,64 @@ +__license__ = 'GPL v3' +__copyright__ = '2011, Darko Miletic ' +''' +gulfnews.com +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class GulfNews(BasicNewsRecipe): + title = 'Gulf News' + __author__ = 'Darko Miletic' + description = 'News from United Arab Emirrates, persian gulf and rest of the world' + publisher = 'Al Nisr Publishing LLC' + category = 'news, politics, UAE, world' + oldest_article = 2 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = False + language = 'en' + remove_empty_feeds = True + publication_type = 'newsportal' + masthead_url = 'http://gulfnews.com/media/img/gulf_news_logo.jpg' + extra_css = """ + body{font-family: Arial,Helvetica,sans-serif } + img{margin-bottom: 0.4em; display:block} + h1{font-family: Georgia, 'Times New Roman', Times, serif} + ol,ul{list-style: none} + .synopsis{font-size: small} + .details{font-size: x-small} + .image{font-size: xx-small} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + + remove_tags = [ + dict(name=['meta','link','object','embed']) + ,dict(attrs={'class':['quickLinks','ratings']}) + ,dict(attrs={'id':'imageSelector'}) + ] + remove_attributes=['lang'] + keep_only_tags=[ + dict(name='h1') + ,dict(attrs={'class':['synopsis','details','image','article']}) + ] + + + feeds = [ + (u'UAE News' , u'http://gulfnews.com/cmlink/1.446094') + ,(u'Business' , u'http://gulfnews.com/cmlink/1.446098') + ,(u'Entertainment' , u'http://gulfnews.com/cmlink/1.446095') + ,(u'Sport' , u'http://gulfnews.com/cmlink/1.446096') + ,(u'Life' , u'http://gulfnews.com/cmlink/1.446097') + ] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return soup From 260484f5152e0892a393be74dff1f2cdee891d36 Mon Sep 17 00:00:00 2001 From: ldolse Date: Thu, 20 Jan 2011 10:06:28 +0800 Subject: [PATCH 08/39] reduce false positives in dehyphenate --- src/calibre/ebooks/conversion/preprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index f728bec52b..5fceeb7aed 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -201,7 +201,7 @@ class Dehyphenator(object): lookupword = self.removesuffixes.sub('', dehyphenated) else: lookupword = dehyphenated - if len(firsthalf) > 3 and self.prefixes.match(firsthalf) is None: + if len(firsthalf) > 4 and self.prefixes.match(firsthalf) is None: lookupword = self.removeprefix.sub('', lookupword) if self.verbose > 2: self.log("lookup word is: "+str(lookupword)+", orig is: " + str(hyphenated)) From 111b4b12a415972bc910b87591f47864b78003f7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 19:38:26 -0700 Subject: [PATCH 09/39] ... --- src/calibre/manual/faq.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 37d18ea329..3e382c8f10 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -108,8 +108,8 @@ Follow these steps to find the problem: * Make sure that you are connecting only a single device to your computer at a time. Do not have another |app| supported device like an iPhone/iPad etc. at the same time. * Make sure you are running the latest version of |app|. The latest version can always be downloaded from `the calibre website `_. - * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is - * In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled. + * Ensure your operating system is seeing the device. That is, the device should be mounted as a disk that you can access using Windows explorer or whatever the file management program on your computer is. + * In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled. * If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker `_. How does |app| manage collections on my SONY reader? From 3e5a31683931769dfcf90ceea500d0e5a413f180 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 20:08:39 -0700 Subject: [PATCH 10/39] PD Novel driver: Put books on the SD card into the eBoks folder --- src/calibre/devices/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py index 9f8dbcb379..e549a4a9fd 100644 --- a/src/calibre/devices/misc.py +++ b/src/calibre/devices/misc.py @@ -106,7 +106,7 @@ class PDNOVEL(USBMS): WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE' THUMBNAIL_HEIGHT = 130 - EBOOK_DIR_MAIN = 'eBooks' + EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'eBooks' SUPPORTS_SUB_DIRS = False DELETE_EXTS = ['.jpg', '.jpeg', '.png'] From dd976263248a97e72a9208d2e4e05d13d65eb17b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 20:43:47 -0700 Subject: [PATCH 11/39] Start work on refactored Edit Metadata dialog --- src/calibre/gui2/metadata/single.py | 535 ++++++++++++++++++++++++++++ src/calibre/gui2/widgets.py | 7 +- 2 files changed, 539 insertions(+), 3 deletions(-) create mode 100644 src/calibre/gui2/metadata/single.py diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py new file mode 100644 index 0000000000..eff6f97e7d --- /dev/null +++ b/src/calibre/gui2/metadata/single.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap, re + +from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ + QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ + QDoubleSpinBox + +from calibre.gui2 import ResizableDialog +from calibre.utils.icu import sort_key +from calibre.utils.config import tweaks +from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ + EnComboBox +from calibre.ebooks.metadata import title_sort, authors_to_string, \ + string_to_authors + +''' +The interface common to all widgets used to set basic metadata +class BasicMetadataWidget(object): + + LABEL = "label text" + + def initialize(self, db, id_): + pass + + def commit(self, db, id_): + return True + + @dynamic_property + def current_val(self): + def fget(self): + return None + def fset(self, val): + pass + return property(fget=fget, fset=fset) +''' + +# Title {{{ +class TitleEdit(EnLineEdit): + + TITLE_ATTR = 'title' + COMMIT = True + TOOLTIP = _('Change the title of this book') + LABEL = _('&Title:') + + def __init__(self, parent): + self.dialog = parent + EnLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True) + self.current_val = title + self.original_val = self.current_val + + def commit(self, db, id_): + title = self.current_val + if self.COMMIT: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) + else: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, + commit=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + title = unicode(self.text()).strip() + if not title: + title = self.get_default() + return title + + def fset(self, val): + if hasattr(val, 'strip'): + val = val.strip() + if not val: + val = self.get_default() + self.setText(val) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + +class TitleSortEdit(TitleEdit): + + TITLE_ATTR = 'title_sort' + COMMIT = False + TOOLTIP = _('Specify how this book should be sorted when by title.' + ' For example, The Exorcist might be sorted as Exorcist, The.') + LABEL = _('Title &sort:') + + def __init__(self, parent, title_edit, autogen_button): + TitleEdit.__init__(self, parent) + self.title_edit = title_edit + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'title sort matches the current title')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color warns that the current ' + 'title sort does not match the current title. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.title_edit.textChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + def update_state(self, *args): + ts = title_sort(self.title_edit.current_val) + normal = ts == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + self.current_val = title_sort(self.title_edit.current_val) + +# }}} + +# Authors {{{ +class AuthorsEdit(CompleteComboBox): + + TOOLTIP = '' + LABEL = _('&Author(s):') + + def __init__(self, parent): + self.dialog = parent + CompleteComboBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + all_authors = db.all_authors() + all_authors.sort(key=lambda x : sort_key(x[1])) + for i in all_authors: + id, name = i + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.addItem(authors_to_string(name)) + + self.set_separator('&') + self.set_space_before_sep(True) + self.update_items_cache(db.all_author_names()) + + au = db.authors(id_, index_is_id=True) + if not au: + au = _('Unknown') + self.current_val = [a.strip().replace('|', ',') for a in au.split(',')] + self.original_val = self.current_val + + def commit(self, db, id_): + authors = self.current_val + db.set_authors(id_, authors, notify=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + au = unicode(self.text()).strip() + if not au: + au = self.get_default() + return string_to_authors(au) + + def fset(self, val): + if not val: + val = [self.get_default()] + self.setEditText(' & '.join([x.strip() for x in val])) + self.lineEdit().setCursorPosition(0) + + + return property(fget=fget, fset=fset) + +class AuthorSortEdit(EnLineEdit): + + TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' + 'For example Charles Dickens should be sorted as Dickens, ' + 'Charles.\nIf the box is colored green, then text matches ' + 'the individual author\'s sort strings. If it is colored ' + 'red, then the authors and this text do not match.') + LABEL = _('Author s&ort:') + + def __init__(self, parent, authors_edit, autogen_button, db): + EnLineEdit.__init__(self, parent) + self.authors_edit = authors_edit + self.db = db + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'author sort matches the current author')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color indicates that the current ' + 'author sort does not match the current author. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.authors_edit.editTextChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.text()).strip() + + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def update_state(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + au = self.db.author_sort_from_authors(string_to_authors(au)) + + normal = au == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + authors = string_to_authors(au) + self.current_val = self.db.author_sort_from_authors(authors) + + def initialize(self, db, id_): + self.current_val = db.author_sort(id_, index_is_id=True) + + def commit(self, db, id_): + aus = self.current_val + db.set_author_sort(id_, aus, notify=False, commit=False) + return True + +# }}} + +# Series {{{ +class SeriesEdit(EnComboBox): + + TOOLTIP = _('List of known series. You can add new series.') + LABEL = _('&Series:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.dialog = parent + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_series = db.all_series() + all_series.sort(key=lambda x : sort_key(x[1])) + series_id = db.series_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_series: + id, name = i + if id == series_id: + idx = c + self.addItem(name) + c += 1 + + self.lineEdit().setText('') + if idx is not None: + self.setCurrentIndex(idx) + self.original_val = self.current_val + + def commit(self, db, id_): + series = self.current_val + db.set_series(id_, series, notify=False, commit=True) + return True + +class SeriesIndexEdit(QDoubleSpinBox): + + TOOLTIP = '' + LABEL = _('&Number:') + + def __init__(self, parent, series_edit): + QDoubleSpinBox.__init__(self, parent) + self.dialog = parent + self.db = self.original_series_name = None + self.setMaximum(1000000) + self.series_edit = series_edit + series_edit.currentIndexChanged.connect(self.enable) + series_edit.editTextChanged.connect(self.enable) + series_edit.lineEdit().editingFinished.connect(self.increment) + self.enable() + + def enable(self, *args): + self.setEnabled(bool(self.series_edit.current_val)) + + @dynamic_property + def current_val(self): + + def fget(self): + return self.value() + + def fset(self, val): + if val is None: + val = 1.0 + val = float(val) + self.setValue(val) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.db = db + if self.series_edit.current_val: + val = db.series_index(id_, index_is_id=True) + else: + val = 1.0 + self.current_val = val + self.original_val = self.current_val + self.original_series_name = self.series_edit.original_val + + def commit(self, db, id_): + db.set_series_index(id_, self.current_val, notify=False, commit=False) + return True + + def increment(self): + if self.db is not None: + try: + series = self.series_edit.current_val + if series and series != self.original_series_name: + ns = 1.0 + if tweaks['series_index_auto_increment'] != 'const': + ns = self.db.get_next_series_num_for(series) + self.current_val = ns + self.original_series_name = series + except: + import traceback + traceback.print_exc() + + +# }}} + +class BuddyLabel(QLabel): + + def __init__(self, buddy): + QLabel.__init__(self, buddy.LABEL) + self.setBuddy(buddy) + self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) + +class MetadataSingleDialog(ResizableDialog): + + def __init__(self, db, parent=None): + self.db = db + ResizableDialog.__init__(self, parent) + + def setupUi(self, *args): # {{{ + self.resize(990, 650) + + self.button_box = QDialogButtonBox( + QDialogButtonBox.Ok|QDialogButtonBox.Cancel, Qt.Horizontal, + self) + self.button_box.accepted.connect(self.accept) + self.button_box.rejected.connect(self.reject) + + self.scroll_area = QScrollArea(self) + self.scroll_area.setFrameShape(QScrollArea.NoFrame) + self.scroll_area.setWidgetResizable(True) + self.central_widget = QTabWidget(self) + self.scroll_area.setWidget(self.central_widget) + + self.l = QVBoxLayout(self) + self.setLayout(self.l) + self.l.setMargin(0) + self.l.addWidget(self.scroll_area) + self.l.addWidget(self.button_box) + + self.setWindowIcon(QIcon(I('edit_input.png'))) + self.setWindowTitle(_('Edit Meta Information')) + + self.create_basic_metadata_widgets() + + self.do_layout() + # }}} + + def create_basic_metadata_widgets(self): + self.basic_metadata_widgets = [] + # Title + self.title = TitleEdit(self) + self.deduce_title_sort_button = QToolButton(self) + self.deduce_title_sort_button.setToolTip( + _('Automatically create the title sort entry based on the current ' + 'title entry.\nUsing this button to create title sort will ' + 'change title sort from red to green.')) + self.deduce_title_sort_button.setWhatsThis( + self.deduce_title_sort_button.toolTip()) + self.title_sort = TitleSortEdit(self, self.title, + self.deduce_title_sort_button) + self.basic_metadata_widgets.extend([self.title, self.title_sort]) + + # Authors + self.authors = AuthorsEdit(self) + self.deduce_author_sort_button = QToolButton(self) + self.deduce_author_sort_button.setToolTip(_( + 'Automatically create the author sort entry based on the current' + ' author entry.\n' + 'Using this button to create author sort will change author sort from' + ' red to green.')) + self.author_sort = AuthorSortEdit(self, self.authors, + self.deduce_author_sort_button, db) + self.basic_metadata_widgets.extend([self.authors, self.author_sort]) + + self.swap_title_author_button = QToolButton(self) + self.swap_title_author_button.setIcon(QIcon(I('swap.png'))) + self.swap_title_author_button.setToolTip(_( + 'Swap the author and title')) + self.swap_title_author_button.clicked.connect(self.swap_title_author) + + self.series = SeriesEdit(self) + self.remove_unused_series_button = QToolButton(self) + self.remove_unused_series_button.setToolTip( + _('Remove unused series (Series that have no books)') ) + self.remove_unused_series_button.clicked.connect(self.remove_unused_series) + self.series_index = SeriesIndexEdit(self, self.series) + self.basic_metadata_widgets.extend([self.series, self.series_index]) + + def do_layout(self): + self.central_widget.clear() + self.tabs = [] + self.labels = [] + self.tabs.append(QWidget(self)) + self.central_widget.addTab(self.tabs[0], _("&Basic metadata")) + self.tabs[0].l = l = QVBoxLayout() + self.tabs[0].tl = tl = QGridLayout() + self.tabs[0].setLayout(l) + l.addLayout(tl) + + def create_row(row, one, two, three, col=1, icon='forward.png'): + ql = BuddyLabel(one) + tl.addWidget(ql, row, col+0, 1, 1) + self.labels.append(ql) + tl.addWidget(one, row, col+1, 1, 1) + if two is not None: + tl.addWidget(two, row, col+2, 1, 1) + two.setIcon(QIcon(I(icon))) + ql = BuddyLabel(three) + tl.addWidget(ql, row, col+3, 1, 1) + self.labels.append(ql) + tl.addWidget(three, row, col+4, 1, 1) + + tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1) + + create_row(0, self.title, self.deduce_title_sort_button, self.title_sort) + create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort) + create_row(2, self.series, self.remove_unused_series_button, + self.series_index, icon='trash.png') + + + def __call__(self, id_, has_next=False, has_previous=False): + self.book_id = id_ + for widget in self.basic_metadata_widgets: + widget.initialize(self.db, id_) + + def swap_title_author(self, *args): + title = self.title.current_val + self.title.current_val = authors_to_string(self.authors.current_val) + self.authors.current_val = string_to_authors(title) + self.title_sort.auto_generate() + self.author_sort.auto_generate() + + def remove_unused_series(self, *args): + self.db.remove_unused_series() + idx = self.series.current_val + self.series.clear() + self.series.initialize(self.db, self.book_id) + if idx: + for i in range(self.series.count()): + if unicode(self.series.itemText(i)) == idx: + self.series.setCurrentIndex(i) + break + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + from calibre.library import db + db = db() + d = MetadataSingleDialog(db) + d(db.data[0][0]) + d.exec_() + diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index dd1121c725..fc21d9a3b3 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -123,6 +123,8 @@ IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp'] class FormatList(QListWidget): DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS + formats_dropped = pyqtSignal(object, object) + delete_format = pyqtSignal() @classmethod def paths_from_event(cls, event): @@ -146,15 +148,14 @@ class FormatList(QListWidget): def dropEvent(self, event): paths = self.paths_from_event(event) event.setDropAction(Qt.CopyAction) - self.emit(SIGNAL('formats_dropped(PyQt_PyObject,PyQt_PyObject)'), - event, paths) + self.formats_dropped.emit(event, paths) def dragMoveEvent(self, event): event.acceptProposedAction() def keyPressEvent(self, event): if event.key() == Qt.Key_Delete: - self.emit(SIGNAL('delete_format()')) + self.delete_format.emit() else: return QListWidget.keyPressEvent(self, event) From 3a063ee644de603d3e3c8c327c58b1f95abfc416 Mon Sep 17 00:00:00 2001 From: ldolse Date: Thu, 20 Jan 2011 12:10:34 +0800 Subject: [PATCH 12/39] updated text input heuristics option to preserve text indents, fixed false positive case in dehyphenate --- src/calibre/ebooks/txt/input.py | 51 +++++++++++++++++---------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/calibre/ebooks/txt/input.py b/src/calibre/ebooks/txt/input.py index 5b99b19e74..6ec1edb65c 100644 --- a/src/calibre/ebooks/txt/input.py +++ b/src/calibre/ebooks/txt/input.py @@ -71,21 +71,41 @@ class TXTInput(InputFormatPlugin): txt = txt.decode(ienc, 'replace') txt = _ent_pat.sub(xml_entity_to_unicode, txt) + + # Normalize line endings + txt = normalize_line_endings(txt) + + if options.formatting_type == 'auto': + options.formatting_type = detect_formatting_type(txt) + + if options.formatting_type == 'heuristic': + setattr(options, 'enable_heuristics', True) + setattr(options, 'markup_chapter_headings', True) + setattr(options, 'italicize_common_cases', True) + setattr(options, 'fix_indents', True) + setattr(options, 'preserve_spaces', True) + setattr(options, 'delete_blank_paragraphs', True) + setattr(options, 'format_scene_breaks', True) + setattr(options, 'dehyphenate', True) + + # Determine the paragraph type of the document. + if options.paragraph_type == 'auto': + options.paragraph_type = detect_paragraph_type(txt) + if options.paragraph_type == 'unknown': + log.debug('Could not reliably determine paragraph type using block') + options.paragraph_type = 'block' + else: + log.debug('Auto detected paragraph type as %s' % options.paragraph_type) + # Preserve spaces will replace multiple spaces to a space # followed by the   entity. if options.preserve_spaces: txt = preserve_spaces(txt) - # Normalize line endings - txt = normalize_line_endings(txt) - # Get length for hyphen removal and punctuation unwrap docanalysis = DocAnalysis('txt', txt) length = docanalysis.line_length(.5) - if options.formatting_type == 'auto': - options.formatting_type = detect_formatting_type(txt) - if options.formatting_type == 'markdown': log.debug('Running text though markdown conversion...') try: @@ -96,16 +116,8 @@ class TXTInput(InputFormatPlugin): elif options.formatting_type == 'textile': log.debug('Running text though textile conversion...') html = convert_textile(txt) - else: - # Determine the paragraph type of the document. - if options.paragraph_type == 'auto': - options.paragraph_type = detect_paragraph_type(txt) - if options.paragraph_type == 'unknown': - log.debug('Could not reliably determine paragraph type using block') - options.paragraph_type = 'block' - else: - log.debug('Auto detected paragraph type as %s' % options.paragraph_type) + else: # Dehyphenate dehyphenator = Dehyphenator(options.verbose, log=self.log) txt = dehyphenator(txt,'txt', length) @@ -129,15 +141,6 @@ class TXTInput(InputFormatPlugin): flow_size = getattr(options, 'flow_size', 0) html = convert_basic(txt, epub_split_size_kb=flow_size) - if options.formatting_type == 'heuristic': - setattr(options, 'enable_heuristics', True) - setattr(options, 'markup_chapter_headings', True) - setattr(options, 'italicize_common_cases', True) - setattr(options, 'fix_indents', True) - setattr(options, 'delete_blank_paragraphs', True) - setattr(options, 'format_scene_breaks', True) - setattr(options, 'dehyphenate', True) - from calibre.customize.ui import plugin_for_input_format html_input = plugin_for_input_format('html') for opt in html_input.options: From 2f5fa30b8680ef69608552aa38190beb71cfaa35 Mon Sep 17 00:00:00 2001 From: ldolse Date: Thu, 20 Jan 2011 12:17:25 +0800 Subject: [PATCH 13/39] ... --- src/calibre/ebooks/conversion/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/cli.py b/src/calibre/ebooks/conversion/cli.py index a95bb23126..33ae61f16a 100644 --- a/src/calibre/ebooks/conversion/cli.py +++ b/src/calibre/ebooks/conversion/cli.py @@ -75,7 +75,6 @@ def option_recommendation_to_cli_option(add_option, rec): opt = rec.option switches = ['-'+opt.short_switch] if opt.short_switch else [] switches.append('--'+opt.long_switch) - flip_switches = ['italicize_common_cases', 'markup_chapter_headings', 'unwrap_lines', 'dehyphenate', 'fix_indents'] attrs = dict(dest=opt.name, help=opt.help, choices=opt.choices, default=rec.recommended_value) if isinstance(rec.recommended_value, type(True)): From f6404d19d856b35085a3f337eb303872bd5d638c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 Jan 2011 22:29:11 -0700 Subject: [PATCH 14/39] More work on the new metadata dialog --- src/calibre/gui2/metadata/single.py | 194 +++++++++++++++++++++++++++- 1 file changed, 187 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index eff6f97e7d..be4170b43f 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,19 +5,26 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import textwrap, re +import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ - QDoubleSpinBox + QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal -from calibre.gui2 import ResizableDialog +from calibre.gui2 import ResizableDialog, file_icon_provider, \ + choose_files, error_dialog from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox + EnComboBox, FormatList from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors +from calibre.utils.date import local_tz +from calibre import strftime +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.customize.ui import run_plugins_on_import +from calibre.utils.date import utcfromtimestamp + ''' The interface common to all widgets used to set basic metadata @@ -385,15 +392,181 @@ class SeriesIndexEdit(QDoubleSpinBox): # }}} -class BuddyLabel(QLabel): +class BuddyLabel(QLabel): # {{{ def __init__(self, buddy): QLabel.__init__(self, buddy.LABEL) self.setBuddy(buddy) self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) +# }}} + +class Format(QListWidgetItem): # {{{ + + def __init__(self, parent, ext, size, path=None, timestamp=None): + self.path = path + self.ext = ext + self.size = float(size)/(1024*1024) + text = '%s (%.2f MB)'%(self.ext.upper(), self.size) + QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), + text, parent, QListWidgetItem.UserType) + if timestamp is not None: + ts = timestamp.astimezone(local_tz) + t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) + text = _('Last modified: %s')%t + self.setToolTip(text) + self.setStatusTip(text) + +# }}} + +class FormatsManager(QWidget): # {{{ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.dialog = parent + self.changed = False + + self.l = l = QGridLayout() + self.setLayout(l) + self.cover_from_format_button = QToolButton(self) + self.cover_from_format_button.setToolTip( + _('Set the cover for the book from the selected format')) + self.cover_from_format_button.setIcon(QIcon(I('book.png'))) + self.cover_from_format_button.setIconSize(QSize(32, 32)) + + self.metadata_from_format_button = QToolButton(self) + self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) + self.metadata_from_format_button.setIconSize(QSize(32, 32)) + # TODO: Implement the *_from_format buttons + + self.add_format_button = QToolButton(self) + self.add_format_button.setIcon(QIcon(I('add_book.png'))) + self.add_format_button.setIconSize(QSize(32, 32)) + self.add_format_button.clicked.connect(self.add_format) + + self.remove_format_button = QToolButton(self) + self.remove_format_button.setIcon(QIcon(I('trash.png'))) + self.remove_format_button.setIconSize(QSize(32, 32)) + self.remove_format_button.clicked.connect(self.remove_format) + + self.formats = FormatList(self) + self.formats.setAcceptDrops(True) + self.formats.formats_dropped.connect(self.formats_dropped) + self.formats.delete_format.connect(self.remove_format) + self.formats.itemDoubleClicked.connect(self.show_format) + self.formats.setDragDropMode(self.formats.DropOnly) + self.formats.setIconSize(QSize(32, 32)) + self.formats.setMaximumWidth(200) + + l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) + l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) + l.addWidget(self.add_format_button, 0, 2, 1, 1) + l.addWidget(self.remove_format_button, 2, 2, 1, 1) + l.addWidget(self.formats, 0, 1, 3, 1) + + + + def initialize(self, db, id_): + self.changed = False + exts = db.formats(id_, index_is_id=True) + if exts: + exts = exts.split(',') + for ext in exts: + if not ext: + ext = '' + size = db.sizeof_format(id_, ext, index_is_id=True) + timestamp = db.format_last_modified(id_, ext) + if size is None: + continue + Format(self.formats, ext, size, timestamp=timestamp) + + def commit(self, db, id_): + if not self.changed: + return True + old_extensions, new_extensions, paths = set(), set(), {} + for row in range(self.formats.count()): + fmt = self.formats.item(row) + ext, path = fmt.ext.lower(), fmt.path + if 'unknown' in ext.lower(): + ext = None + if path: + new_extensions.add(ext) + paths[ext] = path + else: + old_extensions.add(ext) + for ext in new_extensions: + db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, + index_is_id=True) + db_extensions = set([f.lower() for f in db.formats(id_, + index_is_id=True).split(',')]) + extensions = new_extensions.union(old_extensions) + for ext in db_extensions: + if ext not in extensions: + db.remove_format(id_, ext, notify=False, index_is_id=True) + + self.changed = False + return True + + def add_format(self, *args): + files = choose_files(self, 'add formats dialog', + _("Choose formats for ") + + self.dialog.title.current_val, + [(_('Books'), BOOK_EXTENSIONS)]) + self._add_formats(files) + + def _add_formats(self, paths): + added = False + if not paths: + return added + bad_perms = [] + for _file in paths: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + bad_perms.append(_file) + continue + + nfile = run_plugins_on_import(_file) + if nfile is not None: + _file = nfile + stat = os.stat(_file) + size = stat.st_size + ext = os.path.splitext(_file)[1].lower().replace('.', '') + timestamp = utcfromtimestamp(stat.st_mtime) + for row in range(self.formats.count()): + fmt = self.formats.item(row) + if fmt.ext.lower() == ext: + self.formats.takeItem(row) + break + Format(self.formats, ext, size, path=_file, timestamp=timestamp) + self.changed = True + added = True + if bad_perms: + error_dialog(self, _('No permission'), + _('You do not have ' + 'permission to read the following files:'), + det_msg='\n'.join(bad_perms), show=True) + + return added + + def formats_dropped(self, event, paths): + if self._add_formats(paths): + event.accept() + + def remove_format(self, *args): + rows = self.formats.selectionModel().selectedRows(0) + for row in rows: + self.formats.takeItem(row.row()) + self.changed = True + + def show_format(self, item, *args): + fmt = item.ext + self.dialog.view_format.emit(fmt) + +# }}} class MetadataSingleDialog(ResizableDialog): + view_format = pyqtSignal(object) + def __init__(self, db, parent=None): self.db = db ResizableDialog.__init__(self, parent) @@ -427,7 +600,7 @@ class MetadataSingleDialog(ResizableDialog): self.do_layout() # }}} - def create_basic_metadata_widgets(self): + def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] # Title self.title = TitleEdit(self) @@ -468,7 +641,12 @@ class MetadataSingleDialog(ResizableDialog): self.series_index = SeriesIndexEdit(self, self.series) self.basic_metadata_widgets.extend([self.series, self.series_index]) - def do_layout(self): + self.formats_manager = FormatsManager(self) + self.basic_metadata_widgets.append(self.formats_manager) + + # }}} + + def do_layout(self): # {{{ self.central_widget.clear() self.tabs = [] self.labels = [] @@ -499,6 +677,8 @@ class MetadataSingleDialog(ResizableDialog): create_row(2, self.series, self.remove_unused_series_button, self.series_index, icon='trash.png') + tl.addWidget(self.formats_manager, 0, 6, 3, 1) + # }}} def __call__(self, id_, has_next=False, has_previous=False): self.book_id = id_ From 2216de65f07d11a02cbfab0f6c461d7d13427de8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 00:40:36 -0700 Subject: [PATCH 15/39] ... --- src/calibre/gui2/metadata/single.py | 74 ++++++++++++++++++++++++++++- src/calibre/gui2/widgets.py | 6 +-- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index be4170b43f..b9fae51789 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -9,14 +9,15 @@ import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ - QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal + QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ + QSplitter from calibre.gui2 import ResizableDialog, file_icon_provider, \ choose_files, error_dialog from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList + EnComboBox, FormatList, ImageView from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors from calibre.utils.date import local_tz @@ -40,6 +41,7 @@ class BasicMetadataWidget(object): @dynamic_property def current_val(self): + # Present in most but not all basic metadata widgets def fget(self): return None def fset(self, val): @@ -563,6 +565,66 @@ class FormatsManager(QWidget): # {{{ # }}} +class Cover(ImageView): + + def __init__(self, parent): + ImageView.__init__(self, parent) + self._cdata = None + self.cover_changed.connect(self.set_pixmap_from_data) + + def set_pixmap_from_data(self, data): + if not data: + self.current_val = None + return + orig = self.current_val + self.current_val = data + if self.current_val is None: + error_dialog(self, _('Invalid cover'), + _('Could not change cover as the image is invalid.'), + show=True) + self.current_val = orig + + def initialize(self, db, id_): + self._cdata = None + self.current_val = db.cover(id_, index_is_id=True) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + @dynamic_property + def current_val(self): + def fget(self): + return self._cdata + def fset(self, cdata): + self._cdata = None + pm = QPixmap() + if cdata: + pm.loadFromData(cdata) + if pm.isNull(): + pm = QPixmap(I('default_cover.png')) + else: + self._cdata = cdata + self.setPixmap(pm) + tt = _('This book has no cover') + if self._cdata: + tt = _('Cover size: %dx%d pixels') % \ + (pm.width(), pm.height()) + self.setToolTip(tt) + + return property(fget=fget, fset=fset) + + def commit(self, db, id_): + if self.changed: + if self.current_val: + db.set_cover(id_, self.current_val, notify=False, commit=False) + else: + db.remove_cover(id_, notify=False, commit=False) + return True + + + class MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -644,6 +706,9 @@ class MetadataSingleDialog(ResizableDialog): self.formats_manager = FormatsManager(self) self.basic_metadata_widgets.append(self.formats_manager) + self.cover = Cover(self) + self.basic_metadata_widgets.append(self.cover) + # }}} def do_layout(self): # {{{ @@ -678,9 +743,14 @@ class MetadataSingleDialog(ResizableDialog): self.series_index, icon='trash.png') tl.addWidget(self.formats_manager, 0, 6, 3, 1) + + self.splitter = QSplitter(Qt.Horizontal, self) + self.splitter.addWidget(self.cover) + l.addWidget(self.splitter) # }}} def __call__(self, id_, has_next=False, has_previous=False): + # TODO: Next and previous buttons self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index fc21d9a3b3..9e117822e4 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -163,6 +163,7 @@ class FormatList(QListWidget): class ImageView(QWidget): BORDER_WIDTH = 1 + cover_changed = pyqtSignal(object) def __init__(self, parent=None): QWidget.__init__(self, parent) @@ -202,8 +203,7 @@ class ImageView(QWidget): if not pmap.isNull(): self.setPixmap(pmap) event.accept() - self.emit(SIGNAL('cover_changed(PyQt_PyObject)'), open(path, - 'rb').read()) + self.cover_changed.emit(open(path, 'rb').read()) break def dragMoveEvent(self, event): @@ -272,7 +272,7 @@ class ImageView(QWidget): pmap = cb.pixmap(cb.Selection) if not pmap.isNull(): self.setPixmap(pmap) - self.emit(SIGNAL('cover_changed(PyQt_PyObject)'), + self.cover_changed.emit( pixmap_to_data(pmap)) # }}} From 6942bf950c88fe17f61f60cf355103d221e9cfae Mon Sep 17 00:00:00 2001 From: GRiker Date: Thu, 20 Jan 2011 07:03:19 -0700 Subject: [PATCH 16/39] GwR bug fixes for single-section MOBI output --- src/calibre/library/catalog.py | 88 ++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index b52e3785bd..95e738dd58 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -580,7 +580,7 @@ class EPUB_MOBI(CatalogPlugin): "pipeline to the specified " "directory. Useful if you are unsure at which stage " "of the conversion process a bug is occurring.\n" - "Default: '%default'None\n" + "Default: '%default'\n" "Applies to: ePub, MOBI output formats")), Option('--exclude-book-marker', default=':', @@ -1370,7 +1370,9 @@ class EPUB_MOBI(CatalogPlugin): self.generateHTMLByTags() # If this is the only Section, and there are no genres, bail if self.opts.section_list == ['Genres'] and not self.genres: - error_msg = _("No Genres found to catalog.\nCheck 'Excluded genres'\nin E-book options.\n") + error_msg = _("No enabled genres found to catalog.\n") + if not self.opts.cli_environment: + error_msg += "Check 'Excluded genres'\nin E-book options.\n" self.opts.log.error(error_msg) self.error.append(_('No books available to catalog')) self.error.append(error_msg) @@ -2792,14 +2794,16 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) genre_list.append(tag_list) if self.opts.verbose: - self.opts.log.info(" Genre summary: %d active genre tags used in generating catalog with %d titles" % + 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.booksByTitle))) - for genre in genre_list: - for key in genre: - self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key), - len(genre[key]), - 'titles' if len(genre[key]) > 1 else 'title')) + for genre in genre_list: + for key in genre: + self.opts.log.info(" %s: %d %s" % (self.getFriendlyGenreTag(key), + len(genre[key]), + 'titles' if len(genre[key]) > 1 else 'title')) + # Write the results # genre_list = [ {friendly_tag:[{book},{book}]}, {friendly_tag:[{book},{book}]}, ...] @@ -3105,13 +3109,15 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) navPointTag.insert(1, contentTag) elif self.opts.generate_genres: contentTag = Tag(soup, 'content') - contentTag['src'] = "content/ByGenres.html" + #contentTag['src'] = "content/ByGenres.html" + contentTag['src'] = "%s" % self.genres[0]['file'] navPointTag.insert(1, contentTag) elif self.opts.generate_recently_added: contentTag = Tag(soup, 'content') contentTag['src'] = "content/ByDateAdded.html" navPointTag.insert(1, contentTag) else: + # Descriptions only sort_descriptions_by = self.booksByAuthor if self.opts.sort_descriptions_by_author \ else self.booksByTitle contentTag = Tag(soup, 'content') @@ -3125,7 +3131,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) navMapTag.insert(0,navPointTag) ncx.insert(0,navMapTag) - self.ncxSoup = soup def generateNCXDescriptions(self, tocTitle): @@ -3911,7 +3916,6 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Add this section to the body body.insert(btc, navPointTag) btc += 1 - self.ncxSoup = ncx_soup def writeNCX(self): @@ -4055,12 +4059,34 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) # Remove the special marker tags from the database's tag list, # return sorted list of normalized genre tags + def format_tag_list(tags, indent=5, line_break=70, header='Tag list'): + def next_tag(sorted_tags): + for (i, tag) in enumerate(sorted_tags): + if i < len(tags) - 1: + yield tag + ", " + else: + yield tag + + ans = '%s%d %s:\n' % (' ' * indent, len(tags), header) + ans += ' ' * (indent + 1) + out_str = '' + sorted_tags = sorted(tags) + for tag in next_tag(sorted_tags): + out_str += tag + if len(out_str) >= line_break: + ans += out_str + '\n' + out_str = ' ' * (indent + 1) + return ans + out_str + normalized_tags = [] friendly_tags = [] + excluded_tags = [] for tag in tags: - if tag[0] in self.markerTags: + if tag in self.markerTags: + excluded_tags.append(tag) continue if re.search(self.opts.exclude_genre, tag): + excluded_tags.append(tag) continue if tag == ' ': continue @@ -4079,32 +4105,8 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) if genre_tags_dict[key] == normalized: self.opts.log.warn(" %s" % key) if self.verbose: - def next_tag(tags): - for (i, tag) in enumerate(tags): - if i < len(tags) - 1: - yield tag + ", " - else: - yield tag - - self.opts.log.info(u' %d genre tags in database (excluding genres matching %s):' % \ - (len(genre_tags_dict), self.opts.exclude_genre)) - - # Display friendly/normalized genres - # friendly => normalized - if False: - sorted_tags = ['%s => %s' % (key, genre_tags_dict[key]) for key in sorted(genre_tags_dict.keys())] - for tag in next_tag(sorted_tags): - self.opts.log(u' %s' % tag) - else: - sorted_tags = ['%s' % (key) for key in sorted(genre_tags_dict.keys())] - out_str = '' - line_break = 70 - for tag in next_tag(sorted_tags): - out_str += tag - if len(out_str) >= line_break: - self.opts.log.info(' %s' % out_str) - out_str = '' - self.opts.log.info(' %s' % out_str) + self.opts.log.info('%s' % format_tag_list(genre_tags_dict, header="enabled genre tags in database")) + self.opts.log.info('%s' % format_tag_list(excluded_tags, header="excluded genre tags")) return genre_tags_dict @@ -4969,7 +4971,13 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) else: opts.log.warn('\n*** No enabled Sections, terminating catalog generation ***') return ["No Included Sections","No enabled Sections.\nCheck E-book options tab\n'Included sections'\n"] - build_log.append(u" Sections: %s" % ', '.join(sections_list)) + if opts.fmt == 'mobi' and sections_list == ['Descriptions']: + warning = _("\n*** Adding 'By Authors' Section required for MOBI output ***") + opts.log.warn(warning) + sections_list.insert(0,'Authors') + opts.generate_authors = True + + opts.log(u" Sections: %s" % ', '.join(sections_list)) opts.section_list = sections_list # Limit thumb_width to 1.0" - 2.0" @@ -5017,7 +5025,7 @@ then rebuild the catalog.\n''').format(author[0],author[1],current_author[1]) if catalog_source_built: log.info(" Completed catalog source generation\n") else: - log.warn(" *** Errors during catalog generation, check log for details ***") + log.error(" *** Terminated catalog generation, check log for details ***") if catalog_source_built: recommendations = [] From 9f133b37ccf098b4ddb79e17ca04d1311f175526 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 09:05:48 -0700 Subject: [PATCH 17/39] ... --- src/calibre/gui2/ui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 6c6e41e0a5..c6d069cc86 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -449,7 +449,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ def set_window_title(self): - self.setWindowTitle(__appname__ + u' - ||%s||'%self.iactions['Choose Library'].library_name()) + self.setWindowTitle(__appname__ + u' - || %s ||'%self.iactions['Choose Library'].library_name()) def location_selected(self, location): ''' From ad521ab6d3a5cdfc78118151244b6107d870b360 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 09:55:36 -0700 Subject: [PATCH 18/39] cover manip controls --- src/calibre/gui2/metadata/single.py | 108 +++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b9fae51789..1575702918 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -10,10 +10,10 @@ import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter + QSplitter, QPushButton, QGroupBox, QHBoxLayout from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog + choose_files, error_dialog, choose_images from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ @@ -569,9 +569,99 @@ class Cover(ImageView): def __init__(self, parent): ImageView.__init__(self, parent) + self.dialog = parent self._cdata = None self.cover_changed.connect(self.set_pixmap_from_data) + self.select_cover_button = QPushButton(QIcon(I('document_open.png')), + _('&Browse'), parent) + self.trim_cover_button = QPushButton(QIcon(I('trim.png')), + _('T&rim'), parent) + self.remove_cover_button = QPushButton(QIcon(I('trash.png')), + _('&Remove'), parent) + + self.select_cover_button.clicked.connect(self.select_cover) + self.remove_cover_button.clicked.connect(self.remove_cover) + self.trim_cover_button.clicked.connect(self.trim_cover) + + self.download_cover_button = QPushButton(_('Download co&ver'), parent) + self.generate_cover_button = QPushButton(_('&Generate cover'), parent) + + self.download_cover_button.clicked.connect(self.download_cover) + self.generate_cover_button.clicked.connect(self.generate_cover) + + self.buttons = [self.select_cover_button, self.remove_cover_button, + self.trim_cover_button, self.download_cover_button, + self.generate_cover_button] + + def select_cover(self, *args): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + + self.dialog.title.current_val) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self, _('Error reading file'), + _("

There was an error reading from file:
") + + _file + "


"+str(e)) + d.exec_() + if cover: + orig = self.current_val + self.current_val = cover + if self.current_val is None: + self.current_val = orig + error_dialog(self, + _("Not a valid picture"), + _file + _(" is not a valid picture"), show=True) + + def remove_cover(self, *args): + self.current_val = None + + def trim_cover(self, *args): + from calibre.utils.magick import Image + cdata = self.current_val + if not cdata: + return + im = Image() + im.load(cdata) + im.trim(10) + cdata = im.export('png') + self.current_val = cdata + + def download_cover(self, *args): + pass # TODO: Implement this + + def generate_cover(self, *args): + from calibre.ebooks import calibre_cover + from calibre.ebooks.metadata import fmt_sidx + from calibre.gui2 import config + title = self.dialog.title.current_val + author = authors_to_string(self.dialog.authors.current_val) + if not title or not author: + return error_dialog(self, _('Specify title and author'), + _('You must specify a title and author before generating ' + 'a cover'), show=True) + series = self.dialog.series.current_val + series_string = None + if series: + series_string = _('Book %s of %s')%( + fmt_sidx(self.dialog.series_index.current_val, + use_roman=config['use_roman_numerals_for_series_number']), series) + self.current_val = calibre_cover(title, author, + series_string=series_string) + def set_pixmap_from_data(self, data): if not data: self.current_val = None @@ -747,6 +837,20 @@ class MetadataSingleDialog(ResizableDialog): self.splitter = QSplitter(Qt.Horizontal, self) self.splitter.addWidget(self.cover) l.addWidget(self.splitter) + self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self) + gb.l = l = QGridLayout() + gb.setLayout(l) + for i, b in enumerate(self.cover.buttons[:3]): + l.addWidget(b, 0, i, 1, 1) + gb.hl = QHBoxLayout() + for b in self.cover.buttons[3:]: + gb.hl.addWidget(b) + l.addLayout(gb.hl, 1, 0, 1, 3) + self.tabs[0].middle = w = QWidget(self) + w.l = l = QGridLayout() + w.setLayout(w.l) + l.addWidget(gb, 0, 0, 1, 3) + self.splitter.addWidget(w) # }}} def __call__(self, id_, has_next=False, has_previous=False): From cdee30ffd39cd25746810b1af5ed341c54eb675e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 10:26:02 -0700 Subject: [PATCH 19/39] And we have comments --- src/calibre/gui2/metadata/single.py | 38 +++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 1575702918..9cffe2ee55 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -25,7 +25,8 @@ from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS from calibre.customize.ui import run_plugins_on_import from calibre.utils.date import utcfromtimestamp - +from calibre.gui2.comments_editor import Editor +from calibre.library.comments import comments_to_html ''' The interface common to all widgets used to set basic metadata @@ -565,7 +566,7 @@ class FormatsManager(QWidget): # {{{ # }}} -class Cover(ImageView): +class Cover(ImageView): # {{{ def __init__(self, parent): ImageView.__init__(self, parent) @@ -713,7 +714,30 @@ class Cover(ImageView): db.remove_cover(id_, notify=False, commit=False) return True +# }}} +class CommentsEdit(Editor): # {{{ + + @dynamic_property + def current_val(self): + def fget(self): + return self.html + def fset(self, val): + if not val or not val.strip(): + val = '' + else: + val = comments_to_html(val) + self.html = val + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.comments(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_comment(id_, self.current_val, notify=False, commit=False) + return True +# }}} class MetadataSingleDialog(ResizableDialog): @@ -799,6 +823,9 @@ class MetadataSingleDialog(ResizableDialog): self.cover = Cover(self) self.basic_metadata_widgets.append(self.cover) + self.comments = CommentsEdit(self) + self.basic_metadata_widgets.append(self.comments) + # }}} def do_layout(self): # {{{ @@ -851,6 +878,13 @@ class MetadataSingleDialog(ResizableDialog): w.setLayout(w.l) l.addWidget(gb, 0, 0, 1, 3) self.splitter.addWidget(w) + + self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) + gb.l = l = QVBoxLayout() + gb.setLayout(l) + l.addWidget(self.comments) + self.splitter.addWidget(gb) + # }}} def __call__(self, id_, has_next=False, has_previous=False): From a6bded4378460a6443f92e2d07719d63855a32c0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:11:26 -0700 Subject: [PATCH 20/39] Fix #8475 (calibre not finding device) --- src/calibre/devices/android/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 277070020b..1b5cbe4bed 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -21,7 +21,7 @@ class ANDROID(USBMS): # HTC 0x0bb4 : { 0x0c02 : [0x100, 0x0227, 0x0226], 0x0c01 : [0x100, 0x0227], 0x0ff9 : [0x0100, 0x0227, 0x0226], 0x0c87: [0x0100, 0x0227, 0x0226], - 0xc92 : [0x100], 0xc97: [0x226]}, + 0xc92 : [0x100], 0xc97: [0x226], 0xc99 : [0x0100]}, # Eken 0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] }, From f573d07e819fe6f557465dd94712492774520ba9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:16:03 -0700 Subject: [PATCH 21/39] And we have tags --- src/calibre/gui2/dialogs/metadata_single.py | 2 +- src/calibre/gui2/dialogs/tag_editor.py | 4 +- src/calibre/gui2/metadata/single.py | 135 +++++++++++++++++++- src/calibre/manual/faq.rst | 2 +- 4 files changed, 133 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index a2ced18e0f..00bc98cb17 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -775,7 +775,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): self.original_tags = unicode(self.tags.text()) else: self.tags.setText(self.original_tags) - d = TagEditor(self, self.db, self.row) + d = TagEditor(self, self.db, self.id) d.exec_() if d.result() == QDialog.Accepted: tag_string = ', '.join(d.tags) diff --git a/src/calibre/gui2/dialogs/tag_editor.py b/src/calibre/gui2/dialogs/tag_editor.py index 6c5aa6de66..c12b7357f1 100644 --- a/src/calibre/gui2/dialogs/tag_editor.py +++ b/src/calibre/gui2/dialogs/tag_editor.py @@ -10,13 +10,13 @@ from calibre.utils.icu import sort_key class TagEditor(QDialog, Ui_TagEditor): - def __init__(self, window, db, index=None): + def __init__(self, window, db, id_=None): QDialog.__init__(self, window) Ui_TagEditor.__init__(self) self.setupUi(self) self.db = db - self.index = index + self.index = db.row(id_) if self.index is not None: tags = self.db.tags(self.index) else: diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 9cffe2ee55..f531e62fde 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -10,14 +10,15 @@ import textwrap, re, os from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter, QPushButton, QGroupBox, QHBoxLayout + QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ + QMessageBox from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog, choose_images + choose_files, error_dialog, choose_images, question_dialog from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView + EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors from calibre.utils.date import local_tz @@ -27,6 +28,7 @@ from calibre.customize.ui import run_plugins_on_import from calibre.utils.date import utcfromtimestamp from calibre.gui2.comments_editor import Editor from calibre.library.comments import comments_to_html +from calibre.gui2.dialogs.tag_editor import TagEditor ''' The interface common to all widgets used to set basic metadata @@ -739,6 +741,104 @@ class CommentsEdit(Editor): # {{{ return True # }}} +class RatingEdit(QSpinBox): # {{{ + LABEL = _('&Rating:') + TOOLTIP = _('Rating of this book. 0-5 stars') + + def __init__(self, parent): + QSpinBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setMaximum(5) + self.setSuffix(' ' + _('stars')) + + @dynamic_property + def current_val(self): + def fget(self): + return self.value() + def fset(self, val): + if val is None: + val = 0 + val = int(val) + if val < 0: + val = 0 + if val > 5: + val = 5 + self.setValue(val) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + val = db.rating(id_, index_is_id=True) + if val > 0: + val = int(val/2.) + else: + val = 0 + self.current_val = val + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_rating(id_, 2*self.current_val, notify=False, commit=False) + return True + +# }}} + +class TagsEdit(CompleteLineEdit): # {{{ + LABEL = _('Ta&gs:') + TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' + 'useful while searching.

They can be any words' + 'or phrases, separated by commas.') + + def __init__(self, parent): + CompleteLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + @dynamic_property + def current_val(self): + def fget(self): + return [x.strip() for x in unicode(self.text()).split(',')] + def fset(self, val): + if not val: + val = [] + self.setText(', '.join([x.strip() for x in val])) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + tags = db.tags(id_, index_is_id=True) + tags = tags.split(',') if tags else [] + self.current_val = tags + self.update_items_cache(db.all_tags()) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + def edit(self, db, id_): + if self.changed: + if question_dialog(self, _('Tags changed'), + _('You have changed the tags. In order to use the tags' + ' editor, you must either discard or apply these ' + 'changes'), show_copy_button=False, + buttons=QMessageBox.Apply|QMessageBox.Discard, + yes_button=QMessageBox.Apply): + self.commit(db, id_) + db.commit() + self.original_val = self.current_val + else: + self.current_val = self.original_val + d = TagEditor(self, db, id_) + if d.exec_() == TagEditor.Accepted: + self.current_val = d.tags + self.update_items_cache(db.all_tags()) + + + def commit(self, db, id_): + db.set_tags(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + class MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -778,7 +878,7 @@ class MetadataSingleDialog(ResizableDialog): def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] - # Title + self.title = TitleEdit(self) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( @@ -791,7 +891,6 @@ class MetadataSingleDialog(ResizableDialog): self.deduce_title_sort_button) self.basic_metadata_widgets.extend([self.title, self.title_sort]) - # Authors self.authors = AuthorsEdit(self) self.deduce_author_sort_button = QToolButton(self) self.deduce_author_sort_button.setToolTip(_( @@ -826,6 +925,17 @@ class MetadataSingleDialog(ResizableDialog): self.comments = CommentsEdit(self) self.basic_metadata_widgets.append(self.comments) + self.rating = RatingEdit(self) + self.basic_metadata_widgets.append(self.rating) + + self.tags = TagsEdit(self) + self.tags_editor_button = QToolButton(self) + self.tags_editor_button.setToolTip(_('Open Tag Editor')) + self.tags_editor_button.setIcon(QIcon(I('chapters.png'))) + self.tags_editor_button.clicked.connect(self.tags_editor) + self.basic_metadata_widgets.append(self.tags) + + # }}} def do_layout(self): # {{{ @@ -876,8 +986,18 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].middle = w = QWidget(self) w.l = l = QGridLayout() w.setLayout(w.l) - l.addWidget(gb, 0, 0, 1, 3) + l.setMargin(0) self.splitter.addWidget(w) + def create_row2(row, widget, button=None): + ql = BuddyLabel(widget) + l.addWidget(ql, row, 0, 1, 1) + l.addWidget(widget, row, 1, 1, 2 if button is None else 1) + if button is not None: + l.addWidget(button, row, 2, 1, 1) + + l.addWidget(gb, 0, 0, 1, 3) + create_row2(1, self.rating) + create_row2(2, self.tags, self.tags_editor_button) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() @@ -911,6 +1031,9 @@ class MetadataSingleDialog(ResizableDialog): self.series.setCurrentIndex(i) break + def tags_editor(self, *args): + self.tags.edit(self.db, self.book_id) + if __name__ == '__main__': from PyQt4.Qt import QApplication diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 3e382c8f10..5ebe91bc76 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -441,7 +441,7 @@ menu, choose "Validate fonts". I downloaded the installer, but it is not working? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Best place to ask for more help is in the `forums `_. +Downloading from the internet can sometimes result in a corrupted download. If the |app| installer you downloaded is not opening, try downloading it again. If re-downloading it does not work, download it from `an alternate location `_. If the installer still doesn't work, then something on your computer is preventing it from running. Try rebooting your computer and running a registry cleaner like `Wise registry cleaner `_. Best place to ask for more help is in the `forums `_. My antivirus program claims |app| is a virus/trojan? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 1820832fa8ae0bcb89c800595de9adc4ed52164b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 11:59:19 -0700 Subject: [PATCH 22/39] Fix #8477 (Series/Sequence Info no longer being downloaded) --- src/calibre/__init__.py | 8 +++++--- src/calibre/ebooks/metadata/library_thing.py | 21 ++++++++++++++++---- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/calibre/__init__.py b/src/calibre/__init__.py index a4f7439405..221f5911c6 100644 --- a/src/calibre/__init__.py +++ b/src/calibre/__init__.py @@ -241,7 +241,7 @@ def get_parsed_proxy(typ='http', debug=True): return ans -def browser(honor_time=True, max_time=2, mobile_browser=False): +def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None): ''' Create a mechanize browser for web scraping. The browser handles cookies, refresh requests and ignores robots.txt. Also uses proxy if avaialable. @@ -253,8 +253,10 @@ def browser(honor_time=True, max_time=2, mobile_browser=False): opener = Browser() opener.set_handle_refresh(True, max_time=max_time, honor_time=honor_time) opener.set_handle_robots(False) - opener.addheaders = [('User-agent', ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \ - 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13')] + if user_agent is None: + user_agent = ' Mozilla/5.0 (Windows; U; Windows CE 5.1; rv:1.8.1a3) Gecko/20060610 Minimo/0.016' if mobile_browser else \ + 'Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.13) Gecko/20101210 Gentoo Firefox/3.6.13' + opener.addheaders = [('User-agent', user_agent)] http_proxy = get_proxies().get('http', None) if http_proxy: opener.set_proxies({'http':http_proxy}) diff --git a/src/calibre/ebooks/metadata/library_thing.py b/src/calibre/ebooks/metadata/library_thing.py index 7f312da1d9..d956747a2b 100644 --- a/src/calibre/ebooks/metadata/library_thing.py +++ b/src/calibre/ebooks/metadata/library_thing.py @@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' Fetch cover from LibraryThing.com based on ISBN number. ''' -import sys, socket, os, re +import sys, socket, os, re, random from lxml import html import mechanize @@ -16,13 +16,26 @@ from calibre.ebooks.chardet import strip_encoding_declarations OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false' +def get_ua(): + choices = [ + 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' + 'Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)' + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)' + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)' + 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us) AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0 Mobile/7A341 Safari/528.16' + 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19' + 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11' + ] + return choices[random.randint(0, len(choices)-1)] + + class HeadRequest(mechanize.Request): def get_method(self): return 'HEAD' def check_for_cover(isbn, timeout=5.): - br = browser() + br = browser(user_agent=get_ua()) br.set_handle_redirect(False) try: br.open_novisit(HeadRequest(OPENLIBRARY%isbn), timeout=timeout) @@ -51,7 +64,7 @@ def login(br, username, password, force=True): def cover_from_isbn(isbn, timeout=5., username=None, password=None): src = None - br = browser() + br = browser(user_agent=get_ua()) try: return br.open(OPENLIBRARY%isbn, timeout=timeout).read(), 'jpg' except: @@ -100,7 +113,7 @@ def get_social_metadata(title, authors, publisher, isbn, username=None, from calibre.ebooks.metadata import MetaInformation mi = MetaInformation(title, authors) if isbn: - br = browser() + br = browser(user_agent=get_ua()) if username and password: try: login(br, username, password, force=False) From 5f9fcaa1882436f3bd995bf330e5281048c3db58 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 13:15:53 -0700 Subject: [PATCH 23/39] Fix #8479 (Updated recipe for Blic) --- resources/recipes/blic.recipe | 44 ++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/resources/recipes/blic.recipe b/resources/recipes/blic.recipe index 0c955bebde..384518ec13 100644 --- a/resources/recipes/blic.recipe +++ b/resources/recipes/blic.recipe @@ -1,6 +1,6 @@ __license__ = 'GPL v3' -__copyright__ = '2008-2010, Darko Miletic ' +__copyright__ = '2008-2011, Darko Miletic ' ''' blic.rs ''' @@ -21,21 +21,53 @@ class Blic(BasicNewsRecipe): masthead_url = 'http://www.blic.rs/resources/images/header/header_back.png' language = 'sr' publication_type = 'newspaper' - extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Georgia, serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} .img_full{float: none} img{margin-bottom: 0.8em} ' + extra_css = """ + @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} + @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + body{font-family: Georgia, serif1, serif} + .articledescription,#nadnaslov,.article_info{font-family: Arial, sans1, sans-serif} + .img_full{float: none} + #nadnaslov{font-size: small} + #article_lead{font-size: 1.5em} + h1{color: red} + .potpis{font-size: x-small; color: gray} + .article_info{font-size: small} + img{margin-bottom: 0.8em; margin-top: 0.8em; display: block} + """ conversion_options = { 'comment' : description , 'tags' : category , 'publisher': publisher , 'language' : language + , 'linearize_tables' : True } preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] remove_tags_before = dict(name='div', attrs={'id':'article_info'}) - remove_tags = [dict(name=['object','link'])] - remove_attributes = ['width','height'] + remove_tags = [dict(name=['object','link','meta','base','object','embed'])] + remove_attributes = ['width','height','m_id','m_ext','mlg_id','poll_id','v_id'] - feeds = [(u'Danasnje Vesti', u'http://www.blic.rs/rss/danasnje-vesti')] + feeds = [ + (u'Politika' , u'http://www.blic.rs/rss/Vesti/Politika') + ,(u'Tema Dana' , u'http://www.blic.rs/rss/Vesti/Tema-Dana') + ,(u'Svet' , u'http://www.blic.rs/rss/Vesti/Svet') + ,(u'Drustvo' , u'http://www.blic.rs/rss/Vesti/Drustvo') + ,(u'Ekonomija' , u'http://www.blic.rs/rss/Vesti/Ekonomija') + ,(u'Hronika' , u'http://www.blic.rs/rss/Vesti/Hronika') + ,(u'Beograd' , u'http://www.blic.rs/rss/Vesti/Beograd') + ,(u'Srbija' , u'http://www.blic.rs/rss/Vesti/Srbija') + ,(u'Vojvodina' , u'http://www.blic.rs/rss/Vesti/Vojvodina') + ,(u'Republika Srpska' , u'http://www.blic.rs/rss/Vesti/Republika-Srpska') + ,(u'Reportaza' , u'http://www.blic.rs/rss/Vesti/Reportaza') + ,(u'Dodatak' , u'http://www.blic.rs/rss/Vesti/Dodatak') + ,(u'Zabava' , u'http://www.blic.rs/rss/Zabava') + ,(u'Kultura' , u'http://www.blic.rs/rss/Kultura') + ,(u'Slobodno Vreme' , u'http://www.blic.rs/rss/Slobodno-vreme') + ,(u'IT' , u'http://www.blic.rs/rss/IT') + ,(u'Komentar' , u'http://www.blic.rs/rss/Komentar') + ,(u'Intervju' , u'http://www.blic.rs/rss/Intervju') + ] def print_version(self, url): @@ -44,4 +76,4 @@ class Blic(BasicNewsRecipe): def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] - return self.adeify_images(soup) + return soup From cf4e47fcf8d7304ed66684f245df9aa460714fde Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 14:56:56 -0700 Subject: [PATCH 24/39] isbn added --- src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index f531e62fde..daae579334 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -11,7 +11,7 @@ from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ - QMessageBox + QMessageBox, QLineEdit from calibre.gui2 import ResizableDialog, file_icon_provider, \ choose_files, error_dialog, choose_images, question_dialog @@ -20,7 +20,7 @@ from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors + string_to_authors, check_isbn from calibre.utils.date import local_tz from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS @@ -839,6 +839,47 @@ class TagsEdit(CompleteLineEdit): # {{{ # }}} +class ISBNEdit(QLineEdit): # {{{ + LABEL = _('IS&BN:') + + def __init__(self, parent): + QLineEdit.__init__(self, parent) + self.pat = re.compile(r'[^0-9a-zA-Z]') + self.textChanged.connect(self.validate) + + @dynamic_property + def current_val(self): + def fget(self): + return self.pat.sub('', unicode(self.text()).strip()) + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.isbn(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_isbn(id_, self.current_val, notify=False, commit=False) + return True + + def validate(self, *args): + isbn = self.current_val + tt = _('This ISBN number is valid') + if not isbn: + col = 'rgba(0,255,0,0%)' + elif check_isbn(isbn) is not None: + col = 'rgba(0,255,0,20%)' + else: + col = 'rgba(255,0,0,20%)' + tt = _('This ISBN number is invalid') + self.setToolTip(tt) + self.setStyleSheet('QLineEdit { background-color: %s }'%col) + +# }}} + class MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -935,6 +976,8 @@ class MetadataSingleDialog(ResizableDialog): self.tags_editor_button.clicked.connect(self.tags_editor) self.basic_metadata_widgets.append(self.tags) + self.isbn = ISBNEdit(self) + self.basic_metadata_widgets.append(self.isbn) # }}} @@ -998,6 +1041,7 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(gb, 0, 0, 1, 3) create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) + create_row2(3, self.isbn) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() From daeaa718123a75157b2053d0b95966ebbfdf5245 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 15:04:40 -0700 Subject: [PATCH 25/39] publisher added --- src/calibre/gui2/metadata/single.py | 48 +++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index daae579334..de59e8075d 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -880,6 +880,50 @@ class ISBNEdit(QLineEdit): # {{{ # }}} +class PublisherEdit(EnComboBox): # {{{ + LABEL = _('&Publisher:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_publishers = db.all_publishers() + all_publishers.sort(key=lambda x : sort_key(x[1])) + publisher_id = db.publisher_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_publishers: + id, name = i + if id == publisher_id: + idx = c + self.addItem(name) + c += 1 + + self.setEditText('') + if idx is not None: + self.setCurrentIndex(idx) + + def commit(self, db, id_): + db.set_publisher(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + class MetadataSingleDialog(ResizableDialog): view_format = pyqtSignal(object) @@ -979,6 +1023,9 @@ class MetadataSingleDialog(ResizableDialog): self.isbn = ISBNEdit(self) self.basic_metadata_widgets.append(self.isbn) + self.publisher = PublisherEdit(self) + self.basic_metadata_widgets.append(self.publisher) + # }}} def do_layout(self): # {{{ @@ -1042,6 +1089,7 @@ class MetadataSingleDialog(ResizableDialog): create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) create_row2(3, self.isbn) + create_row2(4, self.publisher) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() From 631bd5d63d671ad73d912e37ec6e4e045366c217 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 15:14:17 -0700 Subject: [PATCH 26/39] ... --- src/calibre/gui2/metadata/basic_widgets.py | 927 +++++++++++++++++++++ src/calibre/gui2/metadata/single.py | 925 +------------------- 2 files changed, 936 insertions(+), 916 deletions(-) create mode 100644 src/calibre/gui2/metadata/basic_widgets.py diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py new file mode 100644 index 0000000000..eb162ac9d5 --- /dev/null +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -0,0 +1,927 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2011, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import textwrap, re, os + +from PyQt4.Qt import Qt, \ + QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ + QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ + QPushButton, QSpinBox, \ + QMessageBox, QLineEdit + +from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ + EnComboBox, FormatList, ImageView, CompleteLineEdit +from calibre.utils.icu import sort_key +from calibre.utils.config import tweaks +from calibre.ebooks.metadata import title_sort, authors_to_string, \ + string_to_authors, check_isbn +from calibre.gui2 import file_icon_provider, \ + choose_files, error_dialog, choose_images, question_dialog +from calibre.utils.date import local_tz +from calibre import strftime +from calibre.ebooks import BOOK_EXTENSIONS +from calibre.customize.ui import run_plugins_on_import +from calibre.utils.date import utcfromtimestamp +from calibre.gui2.comments_editor import Editor +from calibre.library.comments import comments_to_html +from calibre.gui2.dialogs.tag_editor import TagEditor + +''' +The interface common to all widgets used to set basic metadata +class BasicMetadataWidget(object): + + LABEL = "label text" + + def initialize(self, db, id_): + pass + + def commit(self, db, id_): + return True + + @dynamic_property + def current_val(self): + # Present in most but not all basic metadata widgets + def fget(self): + return None + def fset(self, val): + pass + return property(fget=fget, fset=fset) +''' + +# Title {{{ +class TitleEdit(EnLineEdit): + + TITLE_ATTR = 'title' + COMMIT = True + TOOLTIP = _('Change the title of this book') + LABEL = _('&Title:') + + def __init__(self, parent): + self.dialog = parent + EnLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True) + self.current_val = title + self.original_val = self.current_val + + def commit(self, db, id_): + title = self.current_val + if self.COMMIT: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) + else: + getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, + commit=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + title = unicode(self.text()).strip() + if not title: + title = self.get_default() + return title + + def fset(self, val): + if hasattr(val, 'strip'): + val = val.strip() + if not val: + val = self.get_default() + self.setText(val) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + +class TitleSortEdit(TitleEdit): + + TITLE_ATTR = 'title_sort' + COMMIT = False + TOOLTIP = _('Specify how this book should be sorted when by title.' + ' For example, The Exorcist might be sorted as Exorcist, The.') + LABEL = _('Title &sort:') + + def __init__(self, parent, title_edit, autogen_button): + TitleEdit.__init__(self, parent) + self.title_edit = title_edit + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'title sort matches the current title')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color warns that the current ' + 'title sort does not match the current title. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.title_edit.textChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + def update_state(self, *args): + ts = title_sort(self.title_edit.current_val) + normal = ts == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + self.current_val = title_sort(self.title_edit.current_val) + +# }}} + +# Authors {{{ +class AuthorsEdit(CompleteComboBox): + + TOOLTIP = '' + LABEL = _('&Author(s):') + + def __init__(self, parent): + self.dialog = parent + CompleteComboBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) + + def get_default(self): + return _('Unknown') + + def initialize(self, db, id_): + all_authors = db.all_authors() + all_authors.sort(key=lambda x : sort_key(x[1])) + for i in all_authors: + id, name = i + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.addItem(authors_to_string(name)) + + self.set_separator('&') + self.set_space_before_sep(True) + self.update_items_cache(db.all_author_names()) + + au = db.authors(id_, index_is_id=True) + if not au: + au = _('Unknown') + self.current_val = [a.strip().replace('|', ',') for a in au.split(',')] + self.original_val = self.current_val + + def commit(self, db, id_): + authors = self.current_val + db.set_authors(id_, authors, notify=False) + return True + + @dynamic_property + def current_val(self): + + def fget(self): + au = unicode(self.text()).strip() + if not au: + au = self.get_default() + return string_to_authors(au) + + def fset(self, val): + if not val: + val = [self.get_default()] + self.setEditText(' & '.join([x.strip() for x in val])) + self.lineEdit().setCursorPosition(0) + + + return property(fget=fget, fset=fset) + +class AuthorSortEdit(EnLineEdit): + + TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' + 'For example Charles Dickens should be sorted as Dickens, ' + 'Charles.\nIf the box is colored green, then text matches ' + 'the individual author\'s sort strings. If it is colored ' + 'red, then the authors and this text do not match.') + LABEL = _('Author s&ort:') + + def __init__(self, parent, authors_edit, autogen_button, db): + EnLineEdit.__init__(self, parent) + self.authors_edit = authors_edit + self.db = db + + base = self.TOOLTIP + ok_tooltip = '

' + textwrap.fill(base+'

'+ + _(' The green color indicates that the current ' + 'author sort matches the current author')) + bad_tooltip = '

'+textwrap.fill(base + '

'+ + _(' The red color indicates that the current ' + 'author sort does not match the current author. ' + 'No action is required if this is what you want.')) + self.tooltips = (ok_tooltip, bad_tooltip) + + self.authors_edit.editTextChanged.connect(self.update_state) + self.textChanged.connect(self.update_state) + + autogen_button.clicked.connect(self.auto_generate) + self.update_state() + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.text()).strip() + + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def update_state(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + au = self.db.author_sort_from_authors(string_to_authors(au)) + + normal = au == self.current_val + if normal: + col = 'rgb(0, 255, 0, 20%)' + else: + col = 'rgb(255, 0, 0, 20%)' + self.setStyleSheet('QLineEdit { color: black; ' + 'background-color: %s; }'%col) + tt = self.tooltips[0 if normal else 1] + self.setToolTip(tt) + self.setWhatsThis(tt) + + def auto_generate(self, *args): + au = unicode(self.authors_edit.text()) + au = re.sub(r'\s+et al\.$', '', au) + authors = string_to_authors(au) + self.current_val = self.db.author_sort_from_authors(authors) + + def initialize(self, db, id_): + self.current_val = db.author_sort(id_, index_is_id=True) + + def commit(self, db, id_): + aus = self.current_val + db.set_author_sort(id_, aus, notify=False, commit=False) + return True + +# }}} + +# Series {{{ +class SeriesEdit(EnComboBox): + + TOOLTIP = _('List of known series. You can add new series.') + LABEL = _('&Series:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.dialog = parent + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setEditable(True) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_series = db.all_series() + all_series.sort(key=lambda x : sort_key(x[1])) + series_id = db.series_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_series: + id, name = i + if id == series_id: + idx = c + self.addItem(name) + c += 1 + + self.lineEdit().setText('') + if idx is not None: + self.setCurrentIndex(idx) + self.original_val = self.current_val + + def commit(self, db, id_): + series = self.current_val + db.set_series(id_, series, notify=False, commit=True) + return True + +class SeriesIndexEdit(QDoubleSpinBox): + + TOOLTIP = '' + LABEL = _('&Number:') + + def __init__(self, parent, series_edit): + QDoubleSpinBox.__init__(self, parent) + self.dialog = parent + self.db = self.original_series_name = None + self.setMaximum(1000000) + self.series_edit = series_edit + series_edit.currentIndexChanged.connect(self.enable) + series_edit.editTextChanged.connect(self.enable) + series_edit.lineEdit().editingFinished.connect(self.increment) + self.enable() + + def enable(self, *args): + self.setEnabled(bool(self.series_edit.current_val)) + + @dynamic_property + def current_val(self): + + def fget(self): + return self.value() + + def fset(self, val): + if val is None: + val = 1.0 + val = float(val) + self.setValue(val) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.db = db + if self.series_edit.current_val: + val = db.series_index(id_, index_is_id=True) + else: + val = 1.0 + self.current_val = val + self.original_val = self.current_val + self.original_series_name = self.series_edit.original_val + + def commit(self, db, id_): + db.set_series_index(id_, self.current_val, notify=False, commit=False) + return True + + def increment(self): + if self.db is not None: + try: + series = self.series_edit.current_val + if series and series != self.original_series_name: + ns = 1.0 + if tweaks['series_index_auto_increment'] != 'const': + ns = self.db.get_next_series_num_for(series) + self.current_val = ns + self.original_series_name = series + except: + import traceback + traceback.print_exc() + + +# }}} + +class BuddyLabel(QLabel): # {{{ + + def __init__(self, buddy): + QLabel.__init__(self, buddy.LABEL) + self.setBuddy(buddy) + self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) +# }}} + +class Format(QListWidgetItem): # {{{ + + def __init__(self, parent, ext, size, path=None, timestamp=None): + self.path = path + self.ext = ext + self.size = float(size)/(1024*1024) + text = '%s (%.2f MB)'%(self.ext.upper(), self.size) + QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), + text, parent, QListWidgetItem.UserType) + if timestamp is not None: + ts = timestamp.astimezone(local_tz) + t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) + text = _('Last modified: %s')%t + self.setToolTip(text) + self.setStatusTip(text) + +# }}} + +class FormatsManager(QWidget): # {{{ + + def __init__(self, parent): + QWidget.__init__(self, parent) + self.dialog = parent + self.changed = False + + self.l = l = QGridLayout() + self.setLayout(l) + self.cover_from_format_button = QToolButton(self) + self.cover_from_format_button.setToolTip( + _('Set the cover for the book from the selected format')) + self.cover_from_format_button.setIcon(QIcon(I('book.png'))) + self.cover_from_format_button.setIconSize(QSize(32, 32)) + + self.metadata_from_format_button = QToolButton(self) + self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) + self.metadata_from_format_button.setIconSize(QSize(32, 32)) + # TODO: Implement the *_from_format buttons + + self.add_format_button = QToolButton(self) + self.add_format_button.setIcon(QIcon(I('add_book.png'))) + self.add_format_button.setIconSize(QSize(32, 32)) + self.add_format_button.clicked.connect(self.add_format) + + self.remove_format_button = QToolButton(self) + self.remove_format_button.setIcon(QIcon(I('trash.png'))) + self.remove_format_button.setIconSize(QSize(32, 32)) + self.remove_format_button.clicked.connect(self.remove_format) + + self.formats = FormatList(self) + self.formats.setAcceptDrops(True) + self.formats.formats_dropped.connect(self.formats_dropped) + self.formats.delete_format.connect(self.remove_format) + self.formats.itemDoubleClicked.connect(self.show_format) + self.formats.setDragDropMode(self.formats.DropOnly) + self.formats.setIconSize(QSize(32, 32)) + self.formats.setMaximumWidth(200) + + l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) + l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) + l.addWidget(self.add_format_button, 0, 2, 1, 1) + l.addWidget(self.remove_format_button, 2, 2, 1, 1) + l.addWidget(self.formats, 0, 1, 3, 1) + + + + def initialize(self, db, id_): + self.changed = False + exts = db.formats(id_, index_is_id=True) + if exts: + exts = exts.split(',') + for ext in exts: + if not ext: + ext = '' + size = db.sizeof_format(id_, ext, index_is_id=True) + timestamp = db.format_last_modified(id_, ext) + if size is None: + continue + Format(self.formats, ext, size, timestamp=timestamp) + + def commit(self, db, id_): + if not self.changed: + return True + old_extensions, new_extensions, paths = set(), set(), {} + for row in range(self.formats.count()): + fmt = self.formats.item(row) + ext, path = fmt.ext.lower(), fmt.path + if 'unknown' in ext.lower(): + ext = None + if path: + new_extensions.add(ext) + paths[ext] = path + else: + old_extensions.add(ext) + for ext in new_extensions: + db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, + index_is_id=True) + db_extensions = set([f.lower() for f in db.formats(id_, + index_is_id=True).split(',')]) + extensions = new_extensions.union(old_extensions) + for ext in db_extensions: + if ext not in extensions: + db.remove_format(id_, ext, notify=False, index_is_id=True) + + self.changed = False + return True + + def add_format(self, *args): + files = choose_files(self, 'add formats dialog', + _("Choose formats for ") + + self.dialog.title.current_val, + [(_('Books'), BOOK_EXTENSIONS)]) + self._add_formats(files) + + def _add_formats(self, paths): + added = False + if not paths: + return added + bad_perms = [] + for _file in paths: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + bad_perms.append(_file) + continue + + nfile = run_plugins_on_import(_file) + if nfile is not None: + _file = nfile + stat = os.stat(_file) + size = stat.st_size + ext = os.path.splitext(_file)[1].lower().replace('.', '') + timestamp = utcfromtimestamp(stat.st_mtime) + for row in range(self.formats.count()): + fmt = self.formats.item(row) + if fmt.ext.lower() == ext: + self.formats.takeItem(row) + break + Format(self.formats, ext, size, path=_file, timestamp=timestamp) + self.changed = True + added = True + if bad_perms: + error_dialog(self, _('No permission'), + _('You do not have ' + 'permission to read the following files:'), + det_msg='\n'.join(bad_perms), show=True) + + return added + + def formats_dropped(self, event, paths): + if self._add_formats(paths): + event.accept() + + def remove_format(self, *args): + rows = self.formats.selectionModel().selectedRows(0) + for row in rows: + self.formats.takeItem(row.row()) + self.changed = True + + def show_format(self, item, *args): + fmt = item.ext + self.dialog.view_format.emit(fmt) + +# }}} + +class Cover(ImageView): # {{{ + + def __init__(self, parent): + ImageView.__init__(self, parent) + self.dialog = parent + self._cdata = None + self.cover_changed.connect(self.set_pixmap_from_data) + + self.select_cover_button = QPushButton(QIcon(I('document_open.png')), + _('&Browse'), parent) + self.trim_cover_button = QPushButton(QIcon(I('trim.png')), + _('T&rim'), parent) + self.remove_cover_button = QPushButton(QIcon(I('trash.png')), + _('&Remove'), parent) + + self.select_cover_button.clicked.connect(self.select_cover) + self.remove_cover_button.clicked.connect(self.remove_cover) + self.trim_cover_button.clicked.connect(self.trim_cover) + + self.download_cover_button = QPushButton(_('Download co&ver'), parent) + self.generate_cover_button = QPushButton(_('&Generate cover'), parent) + + self.download_cover_button.clicked.connect(self.download_cover) + self.generate_cover_button.clicked.connect(self.generate_cover) + + self.buttons = [self.select_cover_button, self.remove_cover_button, + self.trim_cover_button, self.download_cover_button, + self.generate_cover_button] + + def select_cover(self, *args): + files = choose_images(self, 'change cover dialog', + _('Choose cover for ') + + self.dialog.title.current_val) + if not files: + return + _file = files[0] + if _file: + _file = os.path.abspath(_file) + if not os.access(_file, os.R_OK): + d = error_dialog(self, _('Cannot read'), + _('You do not have permission to read the file: ') + _file) + d.exec_() + return + cf, cover = None, None + try: + cf = open(_file, "rb") + cover = cf.read() + except IOError, e: + d = error_dialog(self, _('Error reading file'), + _("

There was an error reading from file:
") + + _file + "


"+str(e)) + d.exec_() + if cover: + orig = self.current_val + self.current_val = cover + if self.current_val is None: + self.current_val = orig + error_dialog(self, + _("Not a valid picture"), + _file + _(" is not a valid picture"), show=True) + + def remove_cover(self, *args): + self.current_val = None + + def trim_cover(self, *args): + from calibre.utils.magick import Image + cdata = self.current_val + if not cdata: + return + im = Image() + im.load(cdata) + im.trim(10) + cdata = im.export('png') + self.current_val = cdata + + def download_cover(self, *args): + pass # TODO: Implement this + + def generate_cover(self, *args): + from calibre.ebooks import calibre_cover + from calibre.ebooks.metadata import fmt_sidx + from calibre.gui2 import config + title = self.dialog.title.current_val + author = authors_to_string(self.dialog.authors.current_val) + if not title or not author: + return error_dialog(self, _('Specify title and author'), + _('You must specify a title and author before generating ' + 'a cover'), show=True) + series = self.dialog.series.current_val + series_string = None + if series: + series_string = _('Book %s of %s')%( + fmt_sidx(self.dialog.series_index.current_val, + use_roman=config['use_roman_numerals_for_series_number']), series) + self.current_val = calibre_cover(title, author, + series_string=series_string) + + def set_pixmap_from_data(self, data): + if not data: + self.current_val = None + return + orig = self.current_val + self.current_val = data + if self.current_val is None: + error_dialog(self, _('Invalid cover'), + _('Could not change cover as the image is invalid.'), + show=True) + self.current_val = orig + + def initialize(self, db, id_): + self._cdata = None + self.current_val = db.cover(id_, index_is_id=True) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + @dynamic_property + def current_val(self): + def fget(self): + return self._cdata + def fset(self, cdata): + self._cdata = None + pm = QPixmap() + if cdata: + pm.loadFromData(cdata) + if pm.isNull(): + pm = QPixmap(I('default_cover.png')) + else: + self._cdata = cdata + self.setPixmap(pm) + tt = _('This book has no cover') + if self._cdata: + tt = _('Cover size: %dx%d pixels') % \ + (pm.width(), pm.height()) + self.setToolTip(tt) + + return property(fget=fget, fset=fset) + + def commit(self, db, id_): + if self.changed: + if self.current_val: + db.set_cover(id_, self.current_val, notify=False, commit=False) + else: + db.remove_cover(id_, notify=False, commit=False) + return True + +# }}} + +class CommentsEdit(Editor): # {{{ + + @dynamic_property + def current_val(self): + def fget(self): + return self.html + def fset(self, val): + if not val or not val.strip(): + val = '' + else: + val = comments_to_html(val) + self.html = val + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.comments(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_comment(id_, self.current_val, notify=False, commit=False) + return True +# }}} + +class RatingEdit(QSpinBox): # {{{ + LABEL = _('&Rating:') + TOOLTIP = _('Rating of this book. 0-5 stars') + + def __init__(self, parent): + QSpinBox.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + self.setMaximum(5) + self.setSuffix(' ' + _('stars')) + + @dynamic_property + def current_val(self): + def fget(self): + return self.value() + def fset(self, val): + if val is None: + val = 0 + val = int(val) + if val < 0: + val = 0 + if val > 5: + val = 5 + self.setValue(val) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + val = db.rating(id_, index_is_id=True) + if val > 0: + val = int(val/2.) + else: + val = 0 + self.current_val = val + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_rating(id_, 2*self.current_val, notify=False, commit=False) + return True + +# }}} + +class TagsEdit(CompleteLineEdit): # {{{ + LABEL = _('Ta&gs:') + TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' + 'useful while searching.

They can be any words' + 'or phrases, separated by commas.') + + def __init__(self, parent): + CompleteLineEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + + @dynamic_property + def current_val(self): + def fget(self): + return [x.strip() for x in unicode(self.text()).split(',')] + def fset(self, val): + if not val: + val = [] + self.setText(', '.join([x.strip() for x in val])) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + tags = db.tags(id_, index_is_id=True) + tags = tags.split(',') if tags else [] + self.current_val = tags + self.update_items_cache(db.all_tags()) + self.original_val = self.current_val + + @property + def changed(self): + return self.current_val != self.original_val + + def edit(self, db, id_): + if self.changed: + if question_dialog(self, _('Tags changed'), + _('You have changed the tags. In order to use the tags' + ' editor, you must either discard or apply these ' + 'changes'), show_copy_button=False, + buttons=QMessageBox.Apply|QMessageBox.Discard, + yes_button=QMessageBox.Apply): + self.commit(db, id_) + db.commit() + self.original_val = self.current_val + else: + self.current_val = self.original_val + d = TagEditor(self, db, id_) + if d.exec_() == TagEditor.Accepted: + self.current_val = d.tags + self.update_items_cache(db.all_tags()) + + + def commit(self, db, id_): + db.set_tags(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + +class ISBNEdit(QLineEdit): # {{{ + LABEL = _('IS&BN:') + + def __init__(self, parent): + QLineEdit.__init__(self, parent) + self.pat = re.compile(r'[^0-9a-zA-Z]') + self.textChanged.connect(self.validate) + + @dynamic_property + def current_val(self): + def fget(self): + return self.pat.sub('', unicode(self.text()).strip()) + def fset(self, val): + if not val: + val = '' + self.setText(val.strip()) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = db.isbn(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + db.set_isbn(id_, self.current_val, notify=False, commit=False) + return True + + def validate(self, *args): + isbn = self.current_val + tt = _('This ISBN number is valid') + if not isbn: + col = 'rgba(0,255,0,0%)' + elif check_isbn(isbn) is not None: + col = 'rgba(0,255,0,20%)' + else: + col = 'rgba(255,0,0,20%)' + tt = _('This ISBN number is invalid') + self.setToolTip(tt) + self.setStyleSheet('QLineEdit { background-color: %s }'%col) + +# }}} + +class PublisherEdit(EnComboBox): # {{{ + LABEL = _('&Publisher:') + + def __init__(self, parent): + EnComboBox.__init__(self, parent) + self.setSizeAdjustPolicy( + self.AdjustToMinimumContentsLengthWithIcon) + + @dynamic_property + def current_val(self): + + def fget(self): + return unicode(self.currentText()).strip() + + def fset(self, val): + if not val: + val = '' + self.setEditText(val.strip()) + self.setCursorPosition(0) + + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + all_publishers = db.all_publishers() + all_publishers.sort(key=lambda x : sort_key(x[1])) + publisher_id = db.publisher_id(id_, index_is_id=True) + idx, c = None, 0 + for i in all_publishers: + id, name = i + if id == publisher_id: + idx = c + self.addItem(name) + c += 1 + + self.setEditText('') + if idx is not None: + self.setCurrentIndex(idx) + + def commit(self, db, id_): + db.set_publisher(id_, self.current_val, notify=False, commit=False) + return True + +# }}} + + diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index de59e8075d..2256816091 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,924 +5,17 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import textwrap, re, os -from PyQt4.Qt import QDialogButtonBox, Qt, QTabWidget, QScrollArea, \ - QVBoxLayout, QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ - QDoubleSpinBox, QListWidgetItem, QSize, pyqtSignal, QPixmap, \ - QSplitter, QPushButton, QGroupBox, QHBoxLayout, QSpinBox, \ - QMessageBox, QLineEdit +from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \ + QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \ + QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox -from calibre.gui2 import ResizableDialog, file_icon_provider, \ - choose_files, error_dialog, choose_images, question_dialog -from calibre.utils.icu import sort_key -from calibre.utils.config import tweaks -from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ - EnComboBox, FormatList, ImageView, CompleteLineEdit -from calibre.ebooks.metadata import title_sort, authors_to_string, \ - string_to_authors, check_isbn -from calibre.utils.date import local_tz -from calibre import strftime -from calibre.ebooks import BOOK_EXTENSIONS -from calibre.customize.ui import run_plugins_on_import -from calibre.utils.date import utcfromtimestamp -from calibre.gui2.comments_editor import Editor -from calibre.library.comments import comments_to_html -from calibre.gui2.dialogs.tag_editor import TagEditor - -''' -The interface common to all widgets used to set basic metadata -class BasicMetadataWidget(object): - - LABEL = "label text" - - def initialize(self, db, id_): - pass - - def commit(self, db, id_): - return True - - @dynamic_property - def current_val(self): - # Present in most but not all basic metadata widgets - def fget(self): - return None - def fset(self, val): - pass - return property(fget=fget, fset=fset) -''' - -# Title {{{ -class TitleEdit(EnLineEdit): - - TITLE_ATTR = 'title' - COMMIT = True - TOOLTIP = _('Change the title of this book') - LABEL = _('&Title:') - - def __init__(self, parent): - self.dialog = parent - EnLineEdit.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - - def get_default(self): - return _('Unknown') - - def initialize(self, db, id_): - title = getattr(db, self.TITLE_ATTR)(id_, index_is_id=True) - self.current_val = title - self.original_val = self.current_val - - def commit(self, db, id_): - title = self.current_val - if self.COMMIT: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False) - else: - getattr(db, 'set_', self.TITLE_ATTR)(id_, title, notify=False, - commit=False) - return True - - @dynamic_property - def current_val(self): - - def fget(self): - title = unicode(self.text()).strip() - if not title: - title = self.get_default() - return title - - def fset(self, val): - if hasattr(val, 'strip'): - val = val.strip() - if not val: - val = self.get_default() - self.setText(val) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - -class TitleSortEdit(TitleEdit): - - TITLE_ATTR = 'title_sort' - COMMIT = False - TOOLTIP = _('Specify how this book should be sorted when by title.' - ' For example, The Exorcist might be sorted as Exorcist, The.') - LABEL = _('Title &sort:') - - def __init__(self, parent, title_edit, autogen_button): - TitleEdit.__init__(self, parent) - self.title_edit = title_edit - - base = self.TOOLTIP - ok_tooltip = '

' + textwrap.fill(base+'

'+ - _(' The green color indicates that the current ' - 'title sort matches the current title')) - bad_tooltip = '

'+textwrap.fill(base + '

'+ - _(' The red color warns that the current ' - 'title sort does not match the current title. ' - 'No action is required if this is what you want.')) - self.tooltips = (ok_tooltip, bad_tooltip) - - self.title_edit.textChanged.connect(self.update_state) - self.textChanged.connect(self.update_state) - - autogen_button.clicked.connect(self.auto_generate) - self.update_state() - - def update_state(self, *args): - ts = title_sort(self.title_edit.current_val) - normal = ts == self.current_val - if normal: - col = 'rgb(0, 255, 0, 20%)' - else: - col = 'rgb(255, 0, 0, 20%)' - self.setStyleSheet('QLineEdit { color: black; ' - 'background-color: %s; }'%col) - tt = self.tooltips[0 if normal else 1] - self.setToolTip(tt) - self.setWhatsThis(tt) - - def auto_generate(self, *args): - self.current_val = title_sort(self.title_edit.current_val) - -# }}} - -# Authors {{{ -class AuthorsEdit(CompleteComboBox): - - TOOLTIP = '' - LABEL = _('&Author(s):') - - def __init__(self, parent): - self.dialog = parent - CompleteComboBox.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - self.setEditable(True) - self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) - - def get_default(self): - return _('Unknown') - - def initialize(self, db, id_): - all_authors = db.all_authors() - all_authors.sort(key=lambda x : sort_key(x[1])) - for i in all_authors: - id, name = i - name = [name.strip().replace('|', ',') for n in name.split(',')] - self.addItem(authors_to_string(name)) - - self.set_separator('&') - self.set_space_before_sep(True) - self.update_items_cache(db.all_author_names()) - - au = db.authors(id_, index_is_id=True) - if not au: - au = _('Unknown') - self.current_val = [a.strip().replace('|', ',') for a in au.split(',')] - self.original_val = self.current_val - - def commit(self, db, id_): - authors = self.current_val - db.set_authors(id_, authors, notify=False) - return True - - @dynamic_property - def current_val(self): - - def fget(self): - au = unicode(self.text()).strip() - if not au: - au = self.get_default() - return string_to_authors(au) - - def fset(self, val): - if not val: - val = [self.get_default()] - self.setEditText(' & '.join([x.strip() for x in val])) - self.lineEdit().setCursorPosition(0) - - - return property(fget=fget, fset=fset) - -class AuthorSortEdit(EnLineEdit): - - TOOLTIP = _('Specify how the author(s) of this book should be sorted. ' - 'For example Charles Dickens should be sorted as Dickens, ' - 'Charles.\nIf the box is colored green, then text matches ' - 'the individual author\'s sort strings. If it is colored ' - 'red, then the authors and this text do not match.') - LABEL = _('Author s&ort:') - - def __init__(self, parent, authors_edit, autogen_button, db): - EnLineEdit.__init__(self, parent) - self.authors_edit = authors_edit - self.db = db - - base = self.TOOLTIP - ok_tooltip = '

' + textwrap.fill(base+'

'+ - _(' The green color indicates that the current ' - 'author sort matches the current author')) - bad_tooltip = '

'+textwrap.fill(base + '

'+ - _(' The red color indicates that the current ' - 'author sort does not match the current author. ' - 'No action is required if this is what you want.')) - self.tooltips = (ok_tooltip, bad_tooltip) - - self.authors_edit.editTextChanged.connect(self.update_state) - self.textChanged.connect(self.update_state) - - autogen_button.clicked.connect(self.auto_generate) - self.update_state() - - @dynamic_property - def current_val(self): - - def fget(self): - return unicode(self.text()).strip() - - def fset(self, val): - if not val: - val = '' - self.setText(val.strip()) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - - def update_state(self, *args): - au = unicode(self.authors_edit.text()) - au = re.sub(r'\s+et al\.$', '', au) - au = self.db.author_sort_from_authors(string_to_authors(au)) - - normal = au == self.current_val - if normal: - col = 'rgb(0, 255, 0, 20%)' - else: - col = 'rgb(255, 0, 0, 20%)' - self.setStyleSheet('QLineEdit { color: black; ' - 'background-color: %s; }'%col) - tt = self.tooltips[0 if normal else 1] - self.setToolTip(tt) - self.setWhatsThis(tt) - - def auto_generate(self, *args): - au = unicode(self.authors_edit.text()) - au = re.sub(r'\s+et al\.$', '', au) - authors = string_to_authors(au) - self.current_val = self.db.author_sort_from_authors(authors) - - def initialize(self, db, id_): - self.current_val = db.author_sort(id_, index_is_id=True) - - def commit(self, db, id_): - aus = self.current_val - db.set_author_sort(id_, aus, notify=False, commit=False) - return True - -# }}} - -# Series {{{ -class SeriesEdit(EnComboBox): - - TOOLTIP = _('List of known series. You can add new series.') - LABEL = _('&Series:') - - def __init__(self, parent): - EnComboBox.__init__(self, parent) - self.dialog = parent - self.setSizeAdjustPolicy( - self.AdjustToMinimumContentsLengthWithIcon) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - self.setEditable(True) - - @dynamic_property - def current_val(self): - - def fget(self): - return unicode(self.currentText()).strip() - - def fset(self, val): - if not val: - val = '' - self.setEditText(val.strip()) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - all_series = db.all_series() - all_series.sort(key=lambda x : sort_key(x[1])) - series_id = db.series_id(id_, index_is_id=True) - idx, c = None, 0 - for i in all_series: - id, name = i - if id == series_id: - idx = c - self.addItem(name) - c += 1 - - self.lineEdit().setText('') - if idx is not None: - self.setCurrentIndex(idx) - self.original_val = self.current_val - - def commit(self, db, id_): - series = self.current_val - db.set_series(id_, series, notify=False, commit=True) - return True - -class SeriesIndexEdit(QDoubleSpinBox): - - TOOLTIP = '' - LABEL = _('&Number:') - - def __init__(self, parent, series_edit): - QDoubleSpinBox.__init__(self, parent) - self.dialog = parent - self.db = self.original_series_name = None - self.setMaximum(1000000) - self.series_edit = series_edit - series_edit.currentIndexChanged.connect(self.enable) - series_edit.editTextChanged.connect(self.enable) - series_edit.lineEdit().editingFinished.connect(self.increment) - self.enable() - - def enable(self, *args): - self.setEnabled(bool(self.series_edit.current_val)) - - @dynamic_property - def current_val(self): - - def fget(self): - return self.value() - - def fset(self, val): - if val is None: - val = 1.0 - val = float(val) - self.setValue(val) - - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - self.db = db - if self.series_edit.current_val: - val = db.series_index(id_, index_is_id=True) - else: - val = 1.0 - self.current_val = val - self.original_val = self.current_val - self.original_series_name = self.series_edit.original_val - - def commit(self, db, id_): - db.set_series_index(id_, self.current_val, notify=False, commit=False) - return True - - def increment(self): - if self.db is not None: - try: - series = self.series_edit.current_val - if series and series != self.original_series_name: - ns = 1.0 - if tweaks['series_index_auto_increment'] != 'const': - ns = self.db.get_next_series_num_for(series) - self.current_val = ns - self.original_series_name = series - except: - import traceback - traceback.print_exc() - - -# }}} - -class BuddyLabel(QLabel): # {{{ - - def __init__(self, buddy): - QLabel.__init__(self, buddy.LABEL) - self.setBuddy(buddy) - self.setAlignment(Qt.AlignRight|Qt.AlignVCenter) -# }}} - -class Format(QListWidgetItem): # {{{ - - def __init__(self, parent, ext, size, path=None, timestamp=None): - self.path = path - self.ext = ext - self.size = float(size)/(1024*1024) - text = '%s (%.2f MB)'%(self.ext.upper(), self.size) - QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), - text, parent, QListWidgetItem.UserType) - if timestamp is not None: - ts = timestamp.astimezone(local_tz) - t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) - text = _('Last modified: %s')%t - self.setToolTip(text) - self.setStatusTip(text) - -# }}} - -class FormatsManager(QWidget): # {{{ - - def __init__(self, parent): - QWidget.__init__(self, parent) - self.dialog = parent - self.changed = False - - self.l = l = QGridLayout() - self.setLayout(l) - self.cover_from_format_button = QToolButton(self) - self.cover_from_format_button.setToolTip( - _('Set the cover for the book from the selected format')) - self.cover_from_format_button.setIcon(QIcon(I('book.png'))) - self.cover_from_format_button.setIconSize(QSize(32, 32)) - - self.metadata_from_format_button = QToolButton(self) - self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) - self.metadata_from_format_button.setIconSize(QSize(32, 32)) - # TODO: Implement the *_from_format buttons - - self.add_format_button = QToolButton(self) - self.add_format_button.setIcon(QIcon(I('add_book.png'))) - self.add_format_button.setIconSize(QSize(32, 32)) - self.add_format_button.clicked.connect(self.add_format) - - self.remove_format_button = QToolButton(self) - self.remove_format_button.setIcon(QIcon(I('trash.png'))) - self.remove_format_button.setIconSize(QSize(32, 32)) - self.remove_format_button.clicked.connect(self.remove_format) - - self.formats = FormatList(self) - self.formats.setAcceptDrops(True) - self.formats.formats_dropped.connect(self.formats_dropped) - self.formats.delete_format.connect(self.remove_format) - self.formats.itemDoubleClicked.connect(self.show_format) - self.formats.setDragDropMode(self.formats.DropOnly) - self.formats.setIconSize(QSize(32, 32)) - self.formats.setMaximumWidth(200) - - l.addWidget(self.cover_from_format_button, 0, 0, 1, 1) - l.addWidget(self.metadata_from_format_button, 2, 0, 1, 1) - l.addWidget(self.add_format_button, 0, 2, 1, 1) - l.addWidget(self.remove_format_button, 2, 2, 1, 1) - l.addWidget(self.formats, 0, 1, 3, 1) - - - - def initialize(self, db, id_): - self.changed = False - exts = db.formats(id_, index_is_id=True) - if exts: - exts = exts.split(',') - for ext in exts: - if not ext: - ext = '' - size = db.sizeof_format(id_, ext, index_is_id=True) - timestamp = db.format_last_modified(id_, ext) - if size is None: - continue - Format(self.formats, ext, size, timestamp=timestamp) - - def commit(self, db, id_): - if not self.changed: - return True - old_extensions, new_extensions, paths = set(), set(), {} - for row in range(self.formats.count()): - fmt = self.formats.item(row) - ext, path = fmt.ext.lower(), fmt.path - if 'unknown' in ext.lower(): - ext = None - if path: - new_extensions.add(ext) - paths[ext] = path - else: - old_extensions.add(ext) - for ext in new_extensions: - db.add_format(id_, ext, open(paths[ext], 'rb'), notify=False, - index_is_id=True) - db_extensions = set([f.lower() for f in db.formats(id_, - index_is_id=True).split(',')]) - extensions = new_extensions.union(old_extensions) - for ext in db_extensions: - if ext not in extensions: - db.remove_format(id_, ext, notify=False, index_is_id=True) - - self.changed = False - return True - - def add_format(self, *args): - files = choose_files(self, 'add formats dialog', - _("Choose formats for ") + - self.dialog.title.current_val, - [(_('Books'), BOOK_EXTENSIONS)]) - self._add_formats(files) - - def _add_formats(self, paths): - added = False - if not paths: - return added - bad_perms = [] - for _file in paths: - _file = os.path.abspath(_file) - if not os.access(_file, os.R_OK): - bad_perms.append(_file) - continue - - nfile = run_plugins_on_import(_file) - if nfile is not None: - _file = nfile - stat = os.stat(_file) - size = stat.st_size - ext = os.path.splitext(_file)[1].lower().replace('.', '') - timestamp = utcfromtimestamp(stat.st_mtime) - for row in range(self.formats.count()): - fmt = self.formats.item(row) - if fmt.ext.lower() == ext: - self.formats.takeItem(row) - break - Format(self.formats, ext, size, path=_file, timestamp=timestamp) - self.changed = True - added = True - if bad_perms: - error_dialog(self, _('No permission'), - _('You do not have ' - 'permission to read the following files:'), - det_msg='\n'.join(bad_perms), show=True) - - return added - - def formats_dropped(self, event, paths): - if self._add_formats(paths): - event.accept() - - def remove_format(self, *args): - rows = self.formats.selectionModel().selectedRows(0) - for row in rows: - self.formats.takeItem(row.row()) - self.changed = True - - def show_format(self, item, *args): - fmt = item.ext - self.dialog.view_format.emit(fmt) - -# }}} - -class Cover(ImageView): # {{{ - - def __init__(self, parent): - ImageView.__init__(self, parent) - self.dialog = parent - self._cdata = None - self.cover_changed.connect(self.set_pixmap_from_data) - - self.select_cover_button = QPushButton(QIcon(I('document_open.png')), - _('&Browse'), parent) - self.trim_cover_button = QPushButton(QIcon(I('trim.png')), - _('T&rim'), parent) - self.remove_cover_button = QPushButton(QIcon(I('trash.png')), - _('&Remove'), parent) - - self.select_cover_button.clicked.connect(self.select_cover) - self.remove_cover_button.clicked.connect(self.remove_cover) - self.trim_cover_button.clicked.connect(self.trim_cover) - - self.download_cover_button = QPushButton(_('Download co&ver'), parent) - self.generate_cover_button = QPushButton(_('&Generate cover'), parent) - - self.download_cover_button.clicked.connect(self.download_cover) - self.generate_cover_button.clicked.connect(self.generate_cover) - - self.buttons = [self.select_cover_button, self.remove_cover_button, - self.trim_cover_button, self.download_cover_button, - self.generate_cover_button] - - def select_cover(self, *args): - files = choose_images(self, 'change cover dialog', - _('Choose cover for ') + - self.dialog.title.current_val) - if not files: - return - _file = files[0] - if _file: - _file = os.path.abspath(_file) - if not os.access(_file, os.R_OK): - d = error_dialog(self, _('Cannot read'), - _('You do not have permission to read the file: ') + _file) - d.exec_() - return - cf, cover = None, None - try: - cf = open(_file, "rb") - cover = cf.read() - except IOError, e: - d = error_dialog(self, _('Error reading file'), - _("

There was an error reading from file:
") - + _file + "


"+str(e)) - d.exec_() - if cover: - orig = self.current_val - self.current_val = cover - if self.current_val is None: - self.current_val = orig - error_dialog(self, - _("Not a valid picture"), - _file + _(" is not a valid picture"), show=True) - - def remove_cover(self, *args): - self.current_val = None - - def trim_cover(self, *args): - from calibre.utils.magick import Image - cdata = self.current_val - if not cdata: - return - im = Image() - im.load(cdata) - im.trim(10) - cdata = im.export('png') - self.current_val = cdata - - def download_cover(self, *args): - pass # TODO: Implement this - - def generate_cover(self, *args): - from calibre.ebooks import calibre_cover - from calibre.ebooks.metadata import fmt_sidx - from calibre.gui2 import config - title = self.dialog.title.current_val - author = authors_to_string(self.dialog.authors.current_val) - if not title or not author: - return error_dialog(self, _('Specify title and author'), - _('You must specify a title and author before generating ' - 'a cover'), show=True) - series = self.dialog.series.current_val - series_string = None - if series: - series_string = _('Book %s of %s')%( - fmt_sidx(self.dialog.series_index.current_val, - use_roman=config['use_roman_numerals_for_series_number']), series) - self.current_val = calibre_cover(title, author, - series_string=series_string) - - def set_pixmap_from_data(self, data): - if not data: - self.current_val = None - return - orig = self.current_val - self.current_val = data - if self.current_val is None: - error_dialog(self, _('Invalid cover'), - _('Could not change cover as the image is invalid.'), - show=True) - self.current_val = orig - - def initialize(self, db, id_): - self._cdata = None - self.current_val = db.cover(id_, index_is_id=True) - self.original_val = self.current_val - - @property - def changed(self): - return self.current_val != self.original_val - - @dynamic_property - def current_val(self): - def fget(self): - return self._cdata - def fset(self, cdata): - self._cdata = None - pm = QPixmap() - if cdata: - pm.loadFromData(cdata) - if pm.isNull(): - pm = QPixmap(I('default_cover.png')) - else: - self._cdata = cdata - self.setPixmap(pm) - tt = _('This book has no cover') - if self._cdata: - tt = _('Cover size: %dx%d pixels') % \ - (pm.width(), pm.height()) - self.setToolTip(tt) - - return property(fget=fget, fset=fset) - - def commit(self, db, id_): - if self.changed: - if self.current_val: - db.set_cover(id_, self.current_val, notify=False, commit=False) - else: - db.remove_cover(id_, notify=False, commit=False) - return True - -# }}} - -class CommentsEdit(Editor): # {{{ - - @dynamic_property - def current_val(self): - def fget(self): - return self.html - def fset(self, val): - if not val or not val.strip(): - val = '' - else: - val = comments_to_html(val) - self.html = val - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - self.current_val = db.comments(id_, index_is_id=True) - self.original_val = self.current_val - - def commit(self, db, id_): - db.set_comment(id_, self.current_val, notify=False, commit=False) - return True -# }}} - -class RatingEdit(QSpinBox): # {{{ - LABEL = _('&Rating:') - TOOLTIP = _('Rating of this book. 0-5 stars') - - def __init__(self, parent): - QSpinBox.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - self.setMaximum(5) - self.setSuffix(' ' + _('stars')) - - @dynamic_property - def current_val(self): - def fget(self): - return self.value() - def fset(self, val): - if val is None: - val = 0 - val = int(val) - if val < 0: - val = 0 - if val > 5: - val = 5 - self.setValue(val) - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - val = db.rating(id_, index_is_id=True) - if val > 0: - val = int(val/2.) - else: - val = 0 - self.current_val = val - self.original_val = self.current_val - - def commit(self, db, id_): - db.set_rating(id_, 2*self.current_val, notify=False, commit=False) - return True - -# }}} - -class TagsEdit(CompleteLineEdit): # {{{ - LABEL = _('Ta&gs:') - TOOLTIP = '

'+_('Tags categorize the book. This is particularly ' - 'useful while searching.

They can be any words' - 'or phrases, separated by commas.') - - def __init__(self, parent): - CompleteLineEdit.__init__(self, parent) - self.setToolTip(self.TOOLTIP) - self.setWhatsThis(self.TOOLTIP) - - @dynamic_property - def current_val(self): - def fget(self): - return [x.strip() for x in unicode(self.text()).split(',')] - def fset(self, val): - if not val: - val = [] - self.setText(', '.join([x.strip() for x in val])) - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - tags = db.tags(id_, index_is_id=True) - tags = tags.split(',') if tags else [] - self.current_val = tags - self.update_items_cache(db.all_tags()) - self.original_val = self.current_val - - @property - def changed(self): - return self.current_val != self.original_val - - def edit(self, db, id_): - if self.changed: - if question_dialog(self, _('Tags changed'), - _('You have changed the tags. In order to use the tags' - ' editor, you must either discard or apply these ' - 'changes'), show_copy_button=False, - buttons=QMessageBox.Apply|QMessageBox.Discard, - yes_button=QMessageBox.Apply): - self.commit(db, id_) - db.commit() - self.original_val = self.current_val - else: - self.current_val = self.original_val - d = TagEditor(self, db, id_) - if d.exec_() == TagEditor.Accepted: - self.current_val = d.tags - self.update_items_cache(db.all_tags()) - - - def commit(self, db, id_): - db.set_tags(id_, self.current_val, notify=False, commit=False) - return True - -# }}} - -class ISBNEdit(QLineEdit): # {{{ - LABEL = _('IS&BN:') - - def __init__(self, parent): - QLineEdit.__init__(self, parent) - self.pat = re.compile(r'[^0-9a-zA-Z]') - self.textChanged.connect(self.validate) - - @dynamic_property - def current_val(self): - def fget(self): - return self.pat.sub('', unicode(self.text()).strip()) - def fset(self, val): - if not val: - val = '' - self.setText(val.strip()) - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - self.current_val = db.isbn(id_, index_is_id=True) - self.original_val = self.current_val - - def commit(self, db, id_): - db.set_isbn(id_, self.current_val, notify=False, commit=False) - return True - - def validate(self, *args): - isbn = self.current_val - tt = _('This ISBN number is valid') - if not isbn: - col = 'rgba(0,255,0,0%)' - elif check_isbn(isbn) is not None: - col = 'rgba(0,255,0,20%)' - else: - col = 'rgba(255,0,0,20%)' - tt = _('This ISBN number is invalid') - self.setToolTip(tt) - self.setStyleSheet('QLineEdit { background-color: %s }'%col) - -# }}} - -class PublisherEdit(EnComboBox): # {{{ - LABEL = _('&Publisher:') - - def __init__(self, parent): - EnComboBox.__init__(self, parent) - self.setSizeAdjustPolicy( - self.AdjustToMinimumContentsLengthWithIcon) - - @dynamic_property - def current_val(self): - - def fget(self): - return unicode(self.currentText()).strip() - - def fset(self, val): - if not val: - val = '' - self.setEditText(val.strip()) - self.setCursorPosition(0) - - return property(fget=fget, fset=fset) - - def initialize(self, db, id_): - all_publishers = db.all_publishers() - all_publishers.sort(key=lambda x : sort_key(x[1])) - publisher_id = db.publisher_id(id_, index_is_id=True) - idx, c = None, 0 - for i in all_publishers: - id, name = i - if id == publisher_id: - idx = c - self.addItem(name) - c += 1 - - self.setEditText('') - if idx is not None: - self.setCurrentIndex(idx) - - def commit(self, db, id_): - db.set_publisher(id_, self.current_val, notify=False, commit=False) - return True - -# }}} +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.gui2 import ResizableDialog +from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ + AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ + RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ + BuddyLabel class MetadataSingleDialog(ResizableDialog): From 96be6e90351041cac47cb5f17efc4fd1cd9d672b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 16:03:54 -0700 Subject: [PATCH 27/39] Basic metadata widgets layout complete --- src/calibre/gui2/metadata/basic_widgets.py | 67 ++++++++++++++++++++-- src/calibre/gui2/metadata/single.py | 35 +++++++++-- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index eb162ac9d5..5d37e854da 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -7,11 +7,10 @@ __docformat__ = 'restructuredtext en' import textwrap, re, os -from PyQt4.Qt import Qt, \ +from PyQt4.Qt import Qt, QDateEdit, QDate, \ QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ - QPushButton, QSpinBox, \ - QMessageBox, QLineEdit + QPushButton, QSpinBox, QMessageBox, QLineEdit from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit @@ -19,9 +18,9 @@ from calibre.utils.icu import sort_key from calibre.utils.config import tweaks from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors, check_isbn -from calibre.gui2 import file_icon_provider, \ +from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ choose_files, error_dialog, choose_images, question_dialog -from calibre.utils.date import local_tz +from calibre.utils.date import local_tz, qt_to_dt from calibre import strftime from calibre.ebooks import BOOK_EXTENSIONS from calibre.customize.ui import run_plugins_on_import @@ -924,4 +923,62 @@ class PublisherEdit(EnComboBox): # {{{ # }}} +class DateEdit(QDateEdit): # {{{ + TOOLTIP = '' + LABEL = _('&Date:') + FMT = 'd MMM yyyy' + ATTR = 'timestamp' + + def __init__(self, parent): + QDateEdit.__init__(self, parent) + self.setToolTip(self.TOOLTIP) + self.setWhatsThis(self.TOOLTIP) + fmt = self.FMT + if fmt is None: + fmt = tweaks['gui_pubdate_display_format'] + if fmt is None: + fmt = 'MMM yyyy' + self.setDisplayFormat(fmt) + self.setCalendarPopup(True) + self.setMinimumDate(UNDEFINED_QDATE) + self.setSpecialValueText(_('Undefined')) + self.clear_button = QToolButton(parent) + self.clear_button.setIcon(QIcon(I('trash.png'))) + self.clear_button.setToolTip(_('Clear date')) + self.clear_button.clicked.connect(self.reset_date) + + def reset_date(self, *args): + self.current_val = None + + @dynamic_property + def current_val(self): + def fget(self): + return qt_to_dt(self.date()) + def fset(self, val): + if val is None: + val = UNDEFINED_DATE + self.setDate(QDate(val.year, val.month, val.day)) + return property(fget=fget, fset=fset) + + def initialize(self, db, id_): + self.current_val = getattr(db, self.ATTR)(id_, index_is_id=True) + self.original_val = self.current_val + + def commit(self, db, id_): + if self.changed: + getattr(db, 'set_'+self.ATTR)(id_, self.current_val, commit=False, + notify=False) + return True + + @property + def changed(self): + o, c = self.original_val, self.current_val + return o.year != c.year or o.month != c.month or o.day != c.day + +class PubdateEdit(DateEdit): + LABEL = _('Publishe&d:') + FMT = None + ATTR = 'pubdate' + +# }}} diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 2256816091..730e5f10b6 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -6,16 +6,17 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, \ - QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, \ - QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox +from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ + QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ + QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ + QSizePolicy from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.gui2 import ResizableDialog from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ - BuddyLabel + BuddyLabel, DateEdit, PubdateEdit class MetadataSingleDialog(ResizableDialog): @@ -119,6 +120,17 @@ class MetadataSingleDialog(ResizableDialog): self.publisher = PublisherEdit(self) self.basic_metadata_widgets.append(self.publisher) + self.timestamp = DateEdit(self) + self.pubdate = PubdateEdit(self) + self.basic_metadata_widgets.extend([self.timestamp, self.pubdate]) + + self.fetch_metadata_button = QPushButton( + _('&Fetch metadata from server'), self) + self.fetch_metadata_button.clicked.connect(self.fetch_metadata) + font = self.fmb_font = QFont() + font.setBold(True) + self.fetch_metadata_button.setFont(font) + # }}} def do_layout(self): # {{{ @@ -172,6 +184,7 @@ class MetadataSingleDialog(ResizableDialog): l.setMargin(0) self.splitter.addWidget(w) def create_row2(row, widget, button=None): + row += 1 ql = BuddyLabel(widget) l.addWidget(ql, row, 0, 1, 1) l.addWidget(widget, row, 1, 1, 2 if button is None else 1) @@ -179,10 +192,19 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(button, row, 2, 1, 1) l.addWidget(gb, 0, 0, 1, 3) + self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding) + l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3) create_row2(1, self.rating) create_row2(2, self.tags, self.tags_editor_button) create_row2(3, self.isbn) - create_row2(4, self.publisher) + create_row2(4, self.timestamp, self.timestamp.clear_button) + create_row2(5, self.pubdate, self.pubdate.clear_button) + create_row2(6, self.publisher) + self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding, + QSizePolicy.Expanding) + l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3) + l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3) self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) gb.l = l = QVBoxLayout() @@ -219,6 +241,9 @@ class MetadataSingleDialog(ResizableDialog): def tags_editor(self, *args): self.tags.edit(self.db, self.book_id) + def fetch_metadata(self, *args): + pass # TODO: fetch metadata + if __name__ == '__main__': from PyQt4.Qt import QApplication From d3bd5b07e8268d1ff3b79f15cae60b5e9efa87d4 Mon Sep 17 00:00:00 2001 From: ldolse Date: Fri, 21 Jan 2011 08:35:14 +0800 Subject: [PATCH 28/39] false positive tuning in txt input and dehyphenate --- src/calibre/ebooks/conversion/preprocess.py | 4 ++++ src/calibre/ebooks/txt/processor.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/conversion/preprocess.py b/src/calibre/ebooks/conversion/preprocess.py index 5fceeb7aed..691aa307d7 100644 --- a/src/calibre/ebooks/conversion/preprocess.py +++ b/src/calibre/ebooks/conversion/preprocess.py @@ -224,6 +224,10 @@ class Dehyphenator(object): return firsthalf+u'\u2014'+wraptags+secondhalf else: + if self.format == 'individual_words' and len(firsthalf) + len(secondhalf) <= 6: + if self.verbose > 2: + self.log("too short, returned hyphenated word: " + str(hyphenated)) + return hyphenated if len(firsthalf) <= 2 and len(secondhalf) <= 2: if self.verbose > 2: self.log("too short, returned hyphenated word: " + str(hyphenated)) diff --git a/src/calibre/ebooks/txt/processor.py b/src/calibre/ebooks/txt/processor.py index 9fd8af0d70..43aadc6576 100644 --- a/src/calibre/ebooks/txt/processor.py +++ b/src/calibre/ebooks/txt/processor.py @@ -175,9 +175,9 @@ def detect_formatting_type(txt): # Block quote. textile_count += len(re.findall(r'(?mu)^bq\.', txt)) # Images - textile_count += len(re.findall(r'\![^\s]+(:[^\s]+)*', txt)) + textile_count += len(re.findall(r'\![^\s]+(?=.*?/)(:[^\s]+)*', txt)) # Links - textile_count += len(re.findall(r'"(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt)) + textile_count += len(re.findall(r'"(?=".*?\()(\(.+?\))*[^\(]+?(\(.+?\))*":[^\s]+', txt)) if markdown_count > 5 or textile_count > 5: if markdown_count > textile_count: From a8aa6ef54aeeacab8a1f8045a0aff87db61e19ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:10:54 -0700 Subject: [PATCH 29/39] Implement accept in the new metadata dialog --- src/calibre/gui2/metadata/single.py | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 730e5f10b6..b53637d66a 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -12,7 +12,7 @@ from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QSizePolicy from calibre.ebooks.metadata import authors_to_string, string_to_authors -from calibre.gui2 import ResizableDialog +from calibre.gui2 import ResizableDialog, error_dialog, gprefs from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ @@ -53,12 +53,16 @@ class MetadataSingleDialog(ResizableDialog): self.create_basic_metadata_widgets() self.do_layout() + geom = gprefs.get('metasingle_window_geometry3', None) + if geom is not None: + self.restoreGeometry(bytes(geom)) # }}} def create_basic_metadata_widgets(self): # {{{ self.basic_metadata_widgets = [] self.title = TitleEdit(self) + self.title.textChanged.connect(self.update_window_title) self.deduce_title_sort_button = QToolButton(self) self.deduce_title_sort_button.setToolTip( _('Automatically create the title sort entry based on the current ' @@ -144,6 +148,9 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].setLayout(l) l.addLayout(tl) + sto = QWidget.setTabOrder + sto(self.fetch_metadata_button, self.title) + def create_row(row, one, two, three, col=1, icon='forward.png'): ql = BuddyLabel(one) tl.addWidget(ql, row, col+0, 1, 1) @@ -156,13 +163,18 @@ class MetadataSingleDialog(ResizableDialog): tl.addWidget(ql, row, col+3, 1, 1) self.labels.append(ql) tl.addWidget(three, row, col+4, 1, 1) + sto(one, two) + sto(two, three) tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1) create_row(0, self.title, self.deduce_title_sort_button, self.title_sort) + sto(self.title_sort, self.authors) create_row(1, self.authors, self.deduce_author_sort_button, self.author_sort) + sto(self.author_sort, self.series) create_row(2, self.series, self.remove_unused_series_button, self.series_index, icon='trash.png') + sto(self.series_index, self.swap_title_author_button) tl.addWidget(self.formats_manager, 0, 6, 3, 1) @@ -219,6 +231,16 @@ class MetadataSingleDialog(ResizableDialog): self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) + self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) + + + def update_window_title(self, *args): + title = self.title.current_val + if len(title) > 50: + title = title[:50] + u'\u2026' + self.setWindowTitle(_('Edit Meta Information') + ' - ' + + title) + def swap_title_author(self, *args): title = self.title.current_val @@ -244,6 +266,30 @@ class MetadataSingleDialog(ResizableDialog): def fetch_metadata(self, *args): pass # TODO: fetch metadata + def accept(self): + for widget in self.basic_metadata_widgets: + try: + if not widget.commit(self.db, self.book_id): + return + except IOError, err: + if err.errno == 13: # Permission denied + import traceback + fname = err.filename if err.filename else 'file' + return error_dialog(self, _('Permission denied'), + _('Could not open %s. Is it being used by another' + ' program?')%fname, det_msg=traceback.format_exc(), + show=True) + raise + self.save_state() + ResizableDialog.accept(self) + + def reject(self): + self.save_state() + ResizableDialog.reject(self) + + def save_state(self): + gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + if __name__ == '__main__': from PyQt4.Qt import QApplication From 5a2cbbc8c41dfe0e226b945c7496e2ba67b276b5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:14:20 -0700 Subject: [PATCH 30/39] Fix #8483 (Need Support for Archos 101) --- src/calibre/devices/android/driver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index 1b5cbe4bed..a95e3c46fa 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -54,7 +54,7 @@ class ANDROID(USBMS): 0x1004 : { 0x61cc : [0x100] }, # Archos - 0x0e79 : { 0x1420 : [0x0216]}, + 0x0e79 : { 0x1419: [0x0216], 0x1420 : [0x0216]}, } EBOOK_DIR_MAIN = ['eBooks/import', 'wordplayer/calibretransfer', 'Books'] @@ -70,10 +70,10 @@ class ANDROID(USBMS): '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', - 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID'] + 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', - 'A70S'] + 'A70S', 'A101IT'] OSX_MAIN_MEM = 'Android Device Main Memory' From 2e7967bd6e70670a3a57063bbc4647bb54c253f0 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 19:45:59 -0700 Subject: [PATCH 31/39] Fix #7702 (Option to populate author from selected row when adding empty books) --- src/calibre/gui2/actions/add.py | 22 ++++-- src/calibre/gui2/dialogs/add_empty_book.py | 85 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 6 deletions(-) create mode 100644 src/calibre/gui2/dialogs/add_empty_book.py diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index 6fa53d6290..7c454d0a94 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -8,11 +8,12 @@ __docformat__ = 'restructuredtext en' import os from functools import partial -from PyQt4.Qt import QInputDialog, QPixmap, QMenu +from PyQt4.Qt import QPixmap, QMenu from calibre.gui2 import error_dialog, choose_files, \ choose_dir, warning_dialog, info_dialog +from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog from calibre.gui2.widgets import IMAGE_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS from calibre.utils.filenames import ascii_filename @@ -42,7 +43,7 @@ class AddAction(InterfaceAction): 'ebook file is a different book)'), self.add_recursive_multiple) self.add_menu.addSeparator() self.add_menu.addAction(_('Add Empty book. (Book entry with no ' - 'formats)'), self.add_empty) + 'formats)'), self.add_empty, _('Shift+Ctrl+E')) self.add_menu.addAction(_('Add from ISBN'), self.add_from_isbn) self.qaction.setMenu(self.add_menu) self.qaction.triggered.connect(self.add_books) @@ -83,12 +84,21 @@ class AddAction(InterfaceAction): Add an empty book item to the library. This does not import any formats from a book file. ''' - num, ok = QInputDialog.getInt(self.gui, _('How many empty books?'), - _('How many empty books should be added?'), 1, 1, 100) - if ok: + author = None + index = self.gui.library_view.currentIndex() + if index.isValid(): + raw = index.model().db.authors(index.row()) + if raw: + authors = [a.strip().replace('|', ',') for a in raw.split(',')] + if authors: + author = authors[0] + dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db, author) + if dlg.exec_() == dlg.Accepted: + num = dlg.qty_to_add from calibre.ebooks.metadata import MetaInformation for x in xrange(num): - self.gui.library_view.model().db.import_book(MetaInformation(None), []) + mi = MetaInformation(_('Unknown'), dlg.selected_authors) + self.gui.library_view.model().db.import_book(mi, []) self.gui.library_view.model().books_added(num) def add_isbns(self, books, add_tags=[]): diff --git a/src/calibre/gui2/dialogs/add_empty_book.py b/src/calibre/gui2/dialogs/add_empty_book.py new file mode 100644 index 0000000000..b8339f95f5 --- /dev/null +++ b/src/calibre/gui2/dialogs/add_empty_book.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' +__license__ = 'GPL v3' + + +from PyQt4.Qt import QDialog, QGridLayout, QLabel, QDialogButtonBox, \ + QApplication, QSpinBox, QToolButton, QIcon +from calibre.ebooks.metadata import authors_to_string, string_to_authors +from calibre.gui2.widgets import CompleteComboBox +from calibre.utils.icu import sort_key + +class AddEmptyBookDialog(QDialog): + + def __init__(self, parent, db, author): + QDialog.__init__(self, parent) + self.db = db + + self.setWindowTitle(_('How many empty books?')) + + self._layout = QGridLayout(self) + self.setLayout(self._layout) + + self.qty_label = QLabel(_('How many empty books should be added?')) + self._layout.addWidget(self.qty_label, 0, 0, 1, 2) + + self.qty_spinbox = QSpinBox(self) + self.qty_spinbox.setRange(1, 10000) + self.qty_spinbox.setValue(1) + self._layout.addWidget(self.qty_spinbox, 1, 0, 1, 2) + + self.author_label = QLabel(_('Set the author of the new books to:')) + self._layout.addWidget(self.author_label, 2, 0, 1, 2) + + self.authors_combo = CompleteComboBox(self) + self.authors_combo.setSizeAdjustPolicy( + self.authors_combo.AdjustToMinimumContentsLengthWithIcon) + self.authors_combo.setEditable(True) + self._layout.addWidget(self.authors_combo, 3, 0, 1, 1) + self.initialize_authors(db, author) + + self.clear_button = QToolButton(self) + self.clear_button.setIcon(QIcon(I('trash.png'))) + self.clear_button.setToolTip(_('Reset author to Unknown')) + self.clear_button.clicked.connect(self.reset_author) + self._layout.addWidget(self.clear_button, 3, 1, 1, 1) + + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box.accepted.connect(self.accept) + button_box.rejected.connect(self.reject) + self._layout.addWidget(button_box) + self.resize(self.sizeHint()) + + def reset_author(self, *args): + self.authors_combo.setEditText(_('Unknown')) + + def initialize_authors(self, db, author): + all_authors = db.all_authors() + all_authors.sort(key=lambda x : sort_key(x[1])) + for i in all_authors: + id, name = i + name = [name.strip().replace('|', ',') for n in name.split(',')] + self.authors_combo.addItem(authors_to_string(name)) + + au = author + if not au: + au = _('Unknown') + self.authors_combo.setEditText(au.replace('|', ',')) + + self.authors_combo.set_separator('&') + self.authors_combo.set_space_before_sep(True) + self.authors_combo.update_items_cache(db.all_author_names()) + + @property + def qty_to_add(self): + return self.qty_spinbox.value() + + @property + def selected_authors(self): + return string_to_authors(unicode(self.authors_combo.text())) + +if __name__ == '__main__': + app = QApplication([]) + d = AddEmptyBookDialog() + d.exec_() From 3e7afccf7e8711a4e1f631861532ee1c31489007 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:28:58 -0700 Subject: [PATCH 32/39] ... --- src/calibre/gui2/actions/choose_library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py index 6f4e883b1a..a2679c2482 100644 --- a/src/calibre/gui2/actions/choose_library.py +++ b/src/calibre/gui2/actions/choose_library.py @@ -32,7 +32,7 @@ class LibraryUsageStats(object): # {{{ locs = list(self.stats.keys()) locs.sort(cmp=lambda x, y: cmp(self.stats[x], self.stats[y]), reverse=True) - for key in locs[15:]: + for key in locs[25:]: self.stats.pop(key) gprefs.set('library_usage_stats', self.stats) From 96a369f1a55577b567190304cfe299c53e417ce6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:31:54 -0700 Subject: [PATCH 33/39] Link up get metadata and cover from format buttons --- src/calibre/gui2/metadata/basic_widgets.py | 34 +++++++++++- src/calibre/gui2/metadata/single.py | 64 +++++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 5d37e854da..96ea785ff2 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -15,9 +15,10 @@ from PyQt4.Qt import Qt, QDateEdit, QDate, \ from calibre.gui2.widgets import EnLineEdit, CompleteComboBox, \ EnComboBox, FormatList, ImageView, CompleteLineEdit from calibre.utils.icu import sort_key -from calibre.utils.config import tweaks +from calibre.utils.config import tweaks, prefs from calibre.ebooks.metadata import title_sort, authors_to_string, \ string_to_authors, check_isbn +from calibre.ebooks.metadata.meta import get_metadata from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ choose_files, error_dialog, choose_images, question_dialog from calibre.utils.date import local_tz, qt_to_dt @@ -440,7 +441,6 @@ class FormatsManager(QWidget): # {{{ self.metadata_from_format_button = QToolButton(self) self.metadata_from_format_button.setIcon(QIcon(I('edit_input.png'))) self.metadata_from_format_button.setIconSize(QSize(32, 32)) - # TODO: Implement the *_from_format buttons self.add_format_button = QToolButton(self) self.add_format_button.setIcon(QIcon(I('add_book.png'))) @@ -565,6 +565,36 @@ class FormatsManager(QWidget): # {{{ fmt = item.ext self.dialog.view_format.emit(fmt) + def get_selected_format_metadata(self, db, id_): + old = prefs['read_file_metadata'] + if not old: + prefs['read_file_metadata'] = True + try: + row = self.formats.currentRow() + fmt = self.formats.item(row) + if fmt is None: + if self.formats.count() == 1: + fmt = self.formats.item(0) + if fmt is None: + error_dialog(self, _('No format selected'), + _('No format selected')).exec_() + return None, None + ext = fmt.ext.lower() + if fmt.path is None: + stream = db.format(id_, ext, as_file=True, index_is_id=True) + else: + stream = open(fmt.path, 'r+b') + try: + mi = get_metadata(stream, ext) + return mi, ext + except: + error_dialog(self, _('Could not read metadata'), + _('Could not read metadata from %s format')%ext).exec_() + return None, None + finally: + if old != prefs['read_file_metadata']: + prefs['read_file_metadata'] = old + # }}} class Cover(ImageView): # {{{ diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index b53637d66a..c7b5e7f99b 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -5,6 +5,7 @@ __license__ = 'GPL v3' __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import os from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ @@ -101,7 +102,10 @@ class MetadataSingleDialog(ResizableDialog): self.formats_manager = FormatsManager(self) self.basic_metadata_widgets.append(self.formats_manager) - + self.formats_manager.metadata_from_format_button.clicked.connect( + self.metadata_from_format) + self.formats_manager.cover_from_format_button.clicked.connect( + self.cover_from_format) self.cover = Cover(self) self.basic_metadata_widgets.append(self.cover) @@ -263,6 +267,64 @@ class MetadataSingleDialog(ResizableDialog): def tags_editor(self, *args): self.tags.edit(self.db, self.book_id) + def metadata_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is not None: + self.update_from_mi(mi) + + def cover_from_format(self, *args): + mi, ext = self.formats_manager.get_selected_format_metadata(self.db, + self.book_id) + if mi is None: + return + cdata = None + if mi.cover and os.access(mi.cover, os.R_OK): + cdata = open(mi.cover).read() + elif mi.cover_data[1] is not None: + cdata = mi.cover_data[1] + if cdata is None: + error_dialog(self, _('Could not read cover'), + _('Could not read cover from %s format')%ext).exec_() + return + orig = self.cover.current_val + self.cover.current_val = cdata + if self.cover.current_val is None: + self.cover.current_val = orig + return error_dialog(self, _('Could not read cover'), + _('The cover in the %s format is invalid')%ext, + show=True) + return + + def update_from_mi(self, mi): + if not mi.is_null('title'): + self.title.current_val = mi.title + if not mi.is_null('authors'): + self.authors.current_val = mi.authors + if not mi.is_null('author_sort'): + self.author_sort.current_val = mi.author_sort + if not mi.is_null('rating'): + try: + self.rating.current_val = mi.rating + except: + pass + if not mi.is_null('publisher'): + self.publisher.current_val = mi.publisher + if not mi.is_null('tags'): + self.tags.current_val = mi.tags + if not mi.is_null('isbn'): + self.isbn.current_val = mi.isbn + if not mi.is_null('pubdate'): + self.pubdate.current_val = mi.pubdate + if not mi.is_null('series') and mi.series.strip(): + self.series.current_val = mi.series + if mi.series_index is not None: + self.series_index.current_val = float(mi.series_index) + if mi.comments and mi.comments.strip(): + self.comments.current_val = mi.comments + + + def fetch_metadata(self, *args): pass # TODO: fetch metadata From 38e6416c3b69b6f98c35b34726259683cff0c800 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 20:58:32 -0700 Subject: [PATCH 34/39] completed implementation of new metadata dialog, except for downloading of metadata which is also going to be refactored, separately --- src/calibre/gui2/custom_column_widgets.py | 3 +- src/calibre/gui2/metadata/single.py | 51 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 58985d1121..c873d1ed94 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -379,7 +379,8 @@ def populate_metadata_page(layout, db, book_id, bulk=False, two_column=False, pa w = bulk_widgets[type](db, col, parent) else: w = widgets[type](db, col, parent) - w.initialize(book_id) + if book_id is not None: + w.initialize(book_id) return w x = db.custom_column_num_map cols = list(x) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index c7b5e7f99b..26acf944e1 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -18,6 +18,8 @@ from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ BuddyLabel, DateEdit, PubdateEdit +from calibre.gui2.custom_column_widgets import populate_metadata_page +from calibre.utils.config import tweaks class MetadataSingleDialog(ResizableDialog): @@ -53,6 +55,12 @@ class MetadataSingleDialog(ResizableDialog): self.create_basic_metadata_widgets() + if len(self.db.custom_column_label_map) == 0: + self.central_widget.tabBar().setVisible(False) + else: + self.create_custom_metadata_widgets() + + self.do_layout() geom = gprefs.get('metasingle_window_geometry3', None) if geom is not None: @@ -139,6 +147,25 @@ class MetadataSingleDialog(ResizableDialog): font.setBold(True) self.fetch_metadata_button.setFont(font) + + # }}} + + def create_custom_metadata_widgets(self): # {{{ + self.custom_metadata_widgets_parent = w = QWidget(self) + layout = QGridLayout() + w.setLayout(layout) + self.custom_metadata_widgets, self.__cc_spacers = \ + populate_metadata_page(layout, self.db, None, parent=w, bulk=False, + two_column=tweaks['metadata_single_use_2_cols_for_custom_fields']) + self.__custom_col_layouts = [layout] + ans = self.custom_metadata_widgets + for i in range(len(ans)-1): + if len(ans[i+1].widgets) == 2: + w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1]) + else: + w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0]) + for c in range(2, len(ans[i].widgets), 2): + w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1]) # }}} def do_layout(self): # {{{ @@ -150,6 +177,10 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].l = l = QVBoxLayout() self.tabs[0].tl = tl = QGridLayout() self.tabs[0].setLayout(l) + w = getattr(self, 'custom_metadata_widgets_parent', None) + if w is not None: + self.tabs.append(w) + self.central_widget.addTab(w, _('&Custom metadata')) l.addLayout(tl) sto = QWidget.setTabOrder @@ -235,6 +266,8 @@ class MetadataSingleDialog(ResizableDialog): self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) + for widget in self.custom_metadata_widgets: + widget.initialize(id_) self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) @@ -323,26 +356,34 @@ class MetadataSingleDialog(ResizableDialog): if mi.comments and mi.comments.strip(): self.comments.current_val = mi.comments - - def fetch_metadata(self, *args): pass # TODO: fetch metadata - def accept(self): + def apply_changes(self): for widget in self.basic_metadata_widgets: try: if not widget.commit(self.db, self.book_id): - return + return False except IOError, err: if err.errno == 13: # Permission denied import traceback fname = err.filename if err.filename else 'file' - return error_dialog(self, _('Permission denied'), + error_dialog(self, _('Permission denied'), _('Could not open %s. Is it being used by another' ' program?')%fname, det_msg=traceback.format_exc(), show=True) + return False raise + for widget in getattr(self, 'custom_metadata_widgets', []): + widget.commit(self.book_id) + + self.db.commit() + return True + + def accept(self): self.save_state() + if not self.apply_changes(): + return ResizableDialog.accept(self) def reject(self): From 6cc052260bf88895a6fbd171410c9fdd846e6059 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 22:27:12 -0700 Subject: [PATCH 35/39] Fix tab order --- src/calibre/gui2/metadata/single.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 26acf944e1..8b0e3da2d2 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -219,11 +219,14 @@ class MetadataSingleDialog(ResizableDialog): self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self) gb.l = l = QGridLayout() gb.setLayout(l) + sto(self.swap_title_author_button, self.cover.buttons[0]) for i, b in enumerate(self.cover.buttons[:3]): l.addWidget(b, 0, i, 1, 1) + sto(b, self.cover.buttons[i+1]) gb.hl = QHBoxLayout() for b in self.cover.buttons[3:]: gb.hl.addWidget(b) + sto(self.cover.buttons[-2], self.cover.buttons[-1]) l.addLayout(gb.hl, 1, 0, 1, 3) self.tabs[0].middle = w = QWidget(self) w.l = l = QGridLayout() @@ -237,23 +240,31 @@ class MetadataSingleDialog(ResizableDialog): l.addWidget(widget, row, 1, 1, 2 if button is None else 1) if button is not None: l.addWidget(button, row, 2, 1, 1) + if button is not None: + sto(widget, button) l.addWidget(gb, 0, 0, 1, 3) self.tabs[0].spc_one = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Expanding) l.addItem(self.tabs[0].spc_one, 1, 0, 1, 3) + sto(self.cover.buttons[-1], self.rating) create_row2(1, self.rating) + sto(self.rating, self.tags) create_row2(2, self.tags, self.tags_editor_button) + sto(self.tags_editor_button, self.isbn) create_row2(3, self.isbn) + sto(self.isbn, self.timestamp) create_row2(4, self.timestamp, self.timestamp.clear_button) + sto(self.timestamp.clear_button, self.pubdate) create_row2(5, self.pubdate, self.pubdate.clear_button) + sto(self.pubdate.clear_button, self.publisher) create_row2(6, self.publisher) self.tabs[0].spc_two = QSpacerItem(10, 10, QSizePolicy.Expanding, QSizePolicy.Expanding) l.addItem(self.tabs[0].spc_two, 8, 0, 1, 3) l.addWidget(self.fetch_metadata_button, 9, 0, 1, 3) - self.tabs[0].gb2 = gb = QGroupBox(_('&Comments'), self) + self.tabs[0].gb2 = gb = QGroupBox(_('Co&mments'), self) gb.l = l = QVBoxLayout() gb.setLayout(l) l.addWidget(self.comments) From 5203777b2ab72372e33d16070c434d5074a3b562 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:09:22 -0700 Subject: [PATCH 36/39] next, previous buttons --- src/calibre/gui2/metadata/single.py | 70 ++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 8b0e3da2d2..99d10a156d 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -6,6 +6,7 @@ __copyright__ = '2011, Kovid Goyal ' __docformat__ = 'restructuredtext en' import os +from functools import partial from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ @@ -27,6 +28,7 @@ class MetadataSingleDialog(ResizableDialog): def __init__(self, db, parent=None): self.db = db + self.changed = set([]) ResizableDialog.__init__(self, parent) def setupUi(self, *args): # {{{ @@ -37,6 +39,14 @@ class MetadataSingleDialog(ResizableDialog): self) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) + self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'), + self) + self.next_button.clicked.connect(partial(self.do_one, delta=1)) + self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'), + self) + self.button_box.addButton(self.prev_button, self.button_box.ActionRole) + self.button_box.addButton(self.next_button, self.button_box.ActionRole) + self.prev_button.clicked.connect(partial(self.do_one, delta=-1)) self.scroll_area = QScrollArea(self) self.scroll_area.setFrameShape(QScrollArea.NoFrame) @@ -184,6 +194,7 @@ class MetadataSingleDialog(ResizableDialog): l.addLayout(tl) sto = QWidget.setTabOrder + sto(self.button_box, self.fetch_metadata_button) sto(self.fetch_metadata_button, self.title) def create_row(row, one, two, three, col=1, icon='forward.png'): @@ -272,14 +283,14 @@ class MetadataSingleDialog(ResizableDialog): # }}} - def __call__(self, id_, has_next=False, has_previous=False): - # TODO: Next and previous buttons + def __call__(self, id_): self.book_id = id_ for widget in self.basic_metadata_widgets: widget.initialize(self.db, id_) for widget in self.custom_metadata_widgets: widget.initialize(id_) - self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) + # Commented out as it doesn't play nice with Next, Prev buttons + #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) def update_window_title(self, *args): @@ -289,7 +300,6 @@ class MetadataSingleDialog(ResizableDialog): self.setWindowTitle(_('Edit Meta Information') + ' - ' + title) - def swap_title_author(self, *args): title = self.title.current_val self.title.current_val = authors_to_string(self.authors.current_val) @@ -371,6 +381,7 @@ class MetadataSingleDialog(ResizableDialog): pass # TODO: fetch metadata def apply_changes(self): + self.changed.add(self.book_id) for widget in self.basic_metadata_widgets: try: if not widget.commit(self.db, self.book_id): @@ -404,13 +415,58 @@ class MetadataSingleDialog(ResizableDialog): def save_state(self): gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) + def start(self, row_list, current_row, view_slot=None): + self.row_list = row_list + self.current_row = current_row + if view_slot is not None: + self.view_format.connect(view_slot) + self.do_one() + ret = self.exec_() + self.break_cycles() + return ret + + def do_one(self, delta=0): + self.current_row += delta + prev = next_ = None + if self.current_row > 0: + prev = self.db.title(self.row_list[self.current_row-1]) + if self.current_row < len(self.row_list) - 1: + next_ = self.db.title(self.row_list[self.current_row+1]) + + if next_ is not None: + tip = _('Save changes and edit the metadata of %s')%next_ + self.next_button.setToolTip(tip) + self.next_button.setVisible(next_ is not None) + if prev is not None: + tip = _('Save changes and edit the metadata of %s')%prev + self.prev_button.setToolTip(tip) + self.prev_button.setVisible(prev is not None) + self(self.db.id(self.row_list[self.current_row])) + + def break_cycles(self): + # Break any reference cycles that could prevent python + # from garbage collecting this dialog + def disconnect(signal): + try: + signal.disconnect() + except: + pass # Fails if view format was never connected + disconnect(self.view_format) + for b in ('next_button', 'prev_button'): + x = getattr(self, b, None) + if x is not None: + disconnect(x.clicked) + +def edit_metadata(db, row_list, current_row, parent=None, view_slot=None): + d = MetadataSingleDialog(db, parent) + d.start(row_list, current_row, view_slot=view_slot) + return d.changed if __name__ == '__main__': from PyQt4.Qt import QApplication app = QApplication([]) from calibre.library import db db = db() - d = MetadataSingleDialog(db) - d(db.data[0][0]) - d.exec_() + row_list = list(range(len(db.data))) + edit_metadata(db, row_list, 0) From ca3f2dafc52223d79a1cc64422965a10c86f9db2 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:23:12 -0700 Subject: [PATCH 37/39] ... --- src/calibre/gui2/metadata/single.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py index 99d10a156d..32fa6ea4f3 100644 --- a/src/calibre/gui2/metadata/single.py +++ b/src/calibre/gui2/metadata/single.py @@ -192,6 +192,8 @@ class MetadataSingleDialog(ResizableDialog): self.tabs.append(w) self.central_widget.addTab(w, _('&Custom metadata')) l.addLayout(tl) + l.addItem(QSpacerItem(10, 15, QSizePolicy.Expanding, + QSizePolicy.Fixed)) sto = QWidget.setTabOrder sto(self.button_box, self.fetch_metadata_button) From 229fe3e4d2c32830367d8ec5b2dc66bd567f0900 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 20 Jan 2011 23:58:22 -0700 Subject: [PATCH 38/39] Add a confirmation when closing the add a custom news source dialog. Fixes #8460 (Add custom new source interface) --- src/calibre/gui2/dialogs/user_profiles.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/calibre/gui2/dialogs/user_profiles.py b/src/calibre/gui2/dialogs/user_profiles.py index 04c41f0c5e..fe64deb430 100644 --- a/src/calibre/gui2/dialogs/user_profiles.py +++ b/src/calibre/gui2/dialogs/user_profiles.py @@ -356,6 +356,13 @@ class %(classname)s(%(base_class)s): self.populate_options(AutomaticNewsRecipe) self.source_code.setText('') + def reject(self): + if question_dialog(self, _('Are you sure?'), + _('You will lose any unsaved changes. To save your' + ' changes, click the Add/Update recipe button.' + ' Continue?'), show_copy_button=False): + ResizableDialog.reject(self) + if __name__ == '__main__': from calibre.gui2 import is_ok_to_use_qt is_ok_to_use_qt() From 7f2373c6bea1702447631ddcce12197dc2679da9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 21 Jan 2011 00:06:47 -0700 Subject: [PATCH 39/39] ... --- src/calibre/trac/bzr_commit_plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/trac/bzr_commit_plugin.py b/src/calibre/trac/bzr_commit_plugin.py index 6c36115cae..2f91804315 100644 --- a/src/calibre/trac/bzr_commit_plugin.py +++ b/src/calibre/trac/bzr_commit_plugin.py @@ -104,12 +104,12 @@ class cmd_commit(_cmd_commit): def close_bug(self, bug, action, url, config): print 'Closing bug #%s'% bug - nick = config.get_nickname() + #nick = config.get_nickname() suffix = config.get_user_option('bug_close_comment') if suffix is None: suffix = 'The fix will be in the next release.' action = action+'ed' - msg = '%s in branch %s. %s'%(action, nick, suffix) + msg = '%s in branch %s. %s'%(action, 'lp:calibre', suffix) msg = msg.replace('Fixesed', 'Fixed') server = xmlrpclib.ServerProxy(url) server.ticket.update(int(bug), msg,