From 00b4b1194a88a93bbabd05dd27417b66498c881a Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 13 Jan 2010 07:15:52 -0700 Subject: [PATCH 1/9] GR Catalog Plugin implementation --- src/calibre/customize/__init__.py | 53 +++++++++++++- src/calibre/customize/ui.py | 27 ++++++- src/calibre/library/cli.py | 115 +++++++++++++++++++++++++++++- 3 files changed, 191 insertions(+), 4 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 736ee2a940..21a4868f77 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -48,7 +48,7 @@ class Plugin(object): #: the plugins are run in order of decreasing priority #: i.e. plugins with higher priority will be run first. #: The highest possible priority is ``sys.maxint``. - #: Default pririty is 1. + #: Default priority is 1. priority = 1 #: The earliest version of calibre this plugin requires @@ -226,4 +226,55 @@ class MetadataWriterPlugin(Plugin): ''' pass +class CatalogPlugin(Plugin): + ''' + A plugin that implements a catalog generator. + ''' + + #: Output file type for which this plugin should be run + #: For example: 'epub' or 'xml' + file_types = set([]) + type = _('Catalog generator') + + #: CLI parser options specific to this plugin, declared as namedtuple Option + #: + #: from collections import namedtuple + #: Option = namedtuple('Option', 'option, default, dest, help') + #: cli_options = [Option('--catalog-title', + #: default = 'My Catalog', + #: dest = 'catalog_title', + #: help = (_('Title of generated catalog. \nDefault:') + " '" + + #: '%default' + "'"))] + + cli_options = [] + + def run(self, path_to_output, opts, log): + ''' + Run the plugin. Must be implemented in subclasses. + It should generate the catalog in the format specified + in file_types, returning the absolute path to the + generated catalog file. If an error is encountered + it should raise an Exception and return None. The default + implementation simply returns None. + + The generated catalog file should be created with the + :meth:`temporary_file` method. + + :param path_to_output: Absolute path to the generated catalog file. + :param opts: A dictionary of keyword arguments + + :return: Absolute path to the modified ebook. + + Access the opts object as a dictionary with this code: + opts_dict = vars(opts) + keys = opts_dict.keys() + keys.sort() + print "opts:" + for key in keys: + print "\t%s: %s" % (key, opts_dict[key]) + + ''' + # Default implementation does nothing + print "CatalogPlugin.generate_catalog() default method, should be overridden in subclass" + return None diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py index 9627ac4853..65a574c3e4 100644 --- a/src/calibre/customize/ui.py +++ b/src/calibre/customize/ui.py @@ -5,8 +5,8 @@ __copyright__ = '2008, Kovid Goyal ' import os, shutil, traceback, functools, sys, re from contextlib import closing -from calibre.customize import Plugin, FileTypePlugin, MetadataReaderPlugin, \ - MetadataWriterPlugin +from calibre.customize import Plugin, CatalogPlugin, FileTypePlugin, \ + MetadataReaderPlugin, MetadataWriterPlugin from calibre.customize.conversion import InputFormatPlugin, OutputFormatPlugin from calibre.customize.profiles import InputProfile, OutputProfile from calibre.customize.builtins import plugins as builtin_plugins @@ -300,6 +300,7 @@ def find_plugin(name): if plugin.name == name: return plugin + def input_format_plugins(): for plugin in _initialized_plugins: if isinstance(plugin, InputFormatPlugin): @@ -328,6 +329,7 @@ def available_input_formats(): formats.add('zip'), formats.add('rar') return formats + def output_format_plugins(): for plugin in _initialized_plugins: if isinstance(plugin, OutputFormatPlugin): @@ -347,6 +349,27 @@ def available_output_formats(): formats.add(plugin.file_type) return formats + +def catalog_plugins(): + for plugin in _initialized_plugins: + if isinstance(plugin, CatalogPlugin): + yield plugin + +def available_catalog_formats(): + formats = set([]) + for plugin in catalog_plugins(): + if not is_disabled(plugin): + for format in plugin.file_types: + formats.add(format) + return formats + +def plugin_for_catalog_format(fmt): + for plugin in catalog_plugins(): + if fmt.lower() in plugin.file_types: + return plugin + else: + return None + def device_plugins(): for plugin in _initialized_plugins: if isinstance(plugin, DevicePlugin): diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 22f0542879..76ae55d16a 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -583,8 +583,121 @@ def command_export(args, dbpath): do_export(get_db(dbpath, opts), ids, dir, opts) return 0 + +# GR additions + +def catalog_option_parser(args): + from calibre.customize.ui import available_catalog_formats, plugin_for_catalog_format + from calibre.utils.logging import Log + + def add_plugin_parser_options(fmt, parser, log): + + # Fetch the extension-specific CLI options from the plugin + plugin = plugin_for_catalog_format(fmt) + for option in plugin.cli_options: + parser.add_option(option.option, + default=option.default, + dest=option.dest, + help=option.help) + + # print_help(parser, log) + return plugin + + def print_help(parser, log): + help = parser.format_help().encode(preferred_encoding, 'replace') + log(help) + + def validate_command_line(parser, args, log): + # calibredb catalog path/to/destination.[epub|csv|xml|...] [options] + + # Validate form + if not len(args) or args[0].startswith('-'): + print_help(parser, log) + log.error("\n\nYou must specify a catalog output file of the form 'path/to/destination.extension'\n" + "To review options for an output format, type 'calibredb catalog <.extension> --help'\n" + "For example, 'calibredb catalog .xml --help'\n") + raise SystemExit(1) + + # Validate plugin exists for specified output format + output = os.path.abspath(args[0]) + file_extension = output[output.rfind('.') + 1:] + + if not file_extension in available_catalog_formats(): + print_help(parser, log) + log.error("No catalog plugin available for extension '%s'.\n" % file_extension + + "Catalog plugins available for %s\n" % ', '.join(available_catalog_formats()) ) + raise SystemExit(1) + + return output, file_extension + + # Entry point + log = Log() + parser = get_parser(_( + ''' + %prog catalog /path/to/destination.(epub|csv|xml|...) [options] + + Export a catalog in format specified by path/to/destination extension. + Options control how entries are displayed in the generated catalog ouput. + ''')) + + # Confirm that a plugin handler exists for specified output file extension + # Will raise SystemExit(1) if no plugin matching file_extension + output, fmt = validate_command_line(parser, args, log) + + # Add options common to all catalog plugins + parser.add_option('-s', '--search', default=None, dest='search_text', + help=_("Filter the results by the search query. For the format of the search query, please see the search-related documentation in the User Manual.\n"+ + "Default: no filtering")) + parser.add_option('-v','--verbose', default=False, action='store_true', + dest='verbose', + help=_('Show detailed output information. Useful for debugging')) + + # Add options specific to fmt plugin + plugin = add_plugin_parser_options(fmt, parser, log) + + # Merge options from GUI Preferences + ''' + from calibre.library.save_to_disk import config + c = config() + for pref in ['asciiize', 'update_metadata', 'write_opf', 'save_cover']: + opt = c.get_option(pref) + switch = '--dont-'+pref.replace('_', '-') + parser.add_option(switch, default=True, action='store_false', + help=opt.help+' '+_('Specifying this switch will turn ' + 'this behavior off.'), dest=pref) + + for pref in ['timefmt', 'template', 'formats']: + opt = c.get_option(pref) + switch = '--'+pref + parser.add_option(switch, default=opt.default, + help=opt.help, dest=pref) + + for pref in ('replace_whitespace', 'to_lowercase'): + opt = c.get_option(pref) + switch = '--'+pref.replace('_', '-') + parser.add_option(switch, default=False, action='store_true', + help=opt.help) + ''' + + return parser, plugin, log + +def command_catalog(args, dbpath): + parser, plugin, log = catalog_option_parser(args) + opts, args = parser.parse_args(sys.argv[1:]) + if len(args) < 2: + parser.print_help() + print + print >>sys.stderr, _('Error: You must specify a catalog output file') + return 1 + if opts.verbose: + log("library.cli:command_catalog dispatching to plugin %s" % plugin.name) + plugin.run(args[1], opts) + return 0 + +# end of GR additions + COMMANDS = ('list', 'add', 'remove', 'add_format', 'remove_format', - 'show_metadata', 'set_metadata', 'export') + 'show_metadata', 'set_metadata', 'export', 'catalog') def option_parser(): From 60a88578595af66d25ce1797230cf1320d137d75 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 09:26:12 -0700 Subject: [PATCH 2/9] Fix #4552 (LRF Import failed) and workaround bad import plugins when adding formats in the edit meta information dialog --- src/calibre/ebooks/lrf/objects.py | 5 ++++- src/calibre/gui2/dialogs/metadata_single.py | 4 +++- src/calibre/manual/faq.rst | 10 ++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/calibre/ebooks/lrf/objects.py b/src/calibre/ebooks/lrf/objects.py index a46417aa1a..0045e679a3 100644 --- a/src/calibre/ebooks/lrf/objects.py +++ b/src/calibre/ebooks/lrf/objects.py @@ -881,7 +881,10 @@ class Text(LRFStream): open_containers.append(c) if len(open_containers) > 0: - raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],)) + if len(open_containers) == 1: + s += u''%(open_containers[0].name,) + else: + raise LRFParseError('Malformed text stream %s'%([i.name for i in open_containers if isinstance(i, Text.TextTag)],)) return s def to_html(self): diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py index 4198fc5684..63dc54df18 100644 --- a/src/calibre/gui2/dialogs/metadata_single.py +++ b/src/calibre/gui2/dialogs/metadata_single.py @@ -148,7 +148,9 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): bad_perms.append(_file) continue - _file = run_plugins_on_import(_file) + nfile = run_plugins_on_import(_file) + if nfile is not None: + _file = nfile size = os.stat(_file).st_size ext = os.path.splitext(_file)[1].lower().replace('.', '') for row in range(self.formats.count()): diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst index 37bd6f289b..9bdd9aaa6b 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -270,11 +270,13 @@ Why does |app| show only some of my fonts on OS X? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There can be several causes for this: - * **Any windows version**: Try running it as Administrator (Right click on the icon ans select "Run as Administrator") - * **Any windows version**: If this happens during an initial run of calibre, try deleting the folder you chose for your ebooks and restarting calibre. + * If you get an error about a Python function terminating unexpectedly after upgrading calibre, first uninstall calibre, then delete the folders (if they exists) + :file:`C:\\Program Files\\Calibre` and :file:`C:\\Program Files\\Calibre2`. Now re-install and you should be fine. + * If you get an error in the welcome wizard on an initial run of calibre, try choosing a folder like :file:`C:\\library` as the calibre library (calibre sometimes + has trouble with library locations if the path contains non-English characters, or only numbers, etc.) + * Try running it as Administrator (Right click on the icon and select "Run as Administrator") * **Windows Vista**: If the folder :file:`C:\\Users\\Your User Name\\AppData\\Local\\VirtualStore\\Program Files\\calibre` exists, delete it. Uninstall |app|. Reboot. Re-install. - * **Any windows version**: Search your computer for a folder named :file:`_ipython`. Delete it and try again. - * **Any windows version**: Try disabling any antivirus program you have running and see if that fixes it. Also try diabling any firewall software that prevents connections to the local computer. + * **Any windows version**: Try disabling any antivirus program you have running and see if that fixes it. Also try disabling any firewall software that prevents connections to the local computer. If it still wont launch, start a command prompt (press the windows key and R; then type :command:`cmd.exe` in the Run dialog that appears). At the command prompt type the following command and press Enter:: From 19163e55ca2b64aebcc2fef728ae5eea12f7125b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 09:42:37 -0700 Subject: [PATCH 3/9] New recipe for the News and Observer by Krittika Goyal --- resources/recipes/observer.recipe | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 resources/recipes/observer.recipe diff --git a/resources/recipes/observer.recipe b/resources/recipes/observer.recipe new file mode 100644 index 0000000000..139d1ff7d4 --- /dev/null +++ b/resources/recipes/observer.recipe @@ -0,0 +1,31 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class NewsandObserver(BasicNewsRecipe): + title = u'News and Observer' + description = 'News from Raleigh, North Carolina' + language = 'en' + __author__ = 'Krittika Goyal' + oldest_article = 5 #days + max_articles_per_feed = 25 + + remove_stylesheets = True + remove_tags_before = dict(name='h1', attrs={'id':'story_headline'}) + remove_tags_after = dict(name='div', attrs={'id':'story_text_remaining'}) + remove_tags = [ + dict(name='iframe'), + dict(name='div', attrs={'id':['right-rail', 'story_tools']}), + dict(name='ul', attrs={'class':'bold_tabs_nav'}), + ] + + + feeds = [ + ('Cover', 'http://www.newsobserver.com/100/index.rss'), + ('News', 'http://www.newsobserver.com/102/index.rss'), + ('Politics', 'http://www.newsobserver.com/105/index.rss'), + ('Business', 'http://www.newsobserver.com/104/index.rss'), + ('Sports', 'http://www.newsobserver.com/103/index.rss'), + ('College Sports', 'http://www.newsobserver.com/119/index.rss'), + ('Lifestyles', 'http://www.newsobserver.com/106/index.rss'), + ('Editorials', 'http://www.newsobserver.com/158/index.rss')] + + From f91a3040510cfd34f61702e158844de34246c415 Mon Sep 17 00:00:00 2001 From: GRiker Date: Wed, 13 Jan 2010 11:07:14 -0700 Subject: [PATCH 4/9] GR Catalog Plugin implementation --- src/calibre/customize/__init__.py | 40 +++++++++++++++++++++++-------- src/calibre/library/cli.py | 3 +-- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 21a4868f77..b08a4b4ab9 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -249,7 +249,34 @@ class CatalogPlugin(Plugin): cli_options = [] - def run(self, path_to_output, opts, log): + def search_sort_db_as_dict(self, db, opts): + if opts.search_text: + db.search(opts.search_text) + if opts.sort_by: + # 2nd arg = ascending + db.sort(opts.sort_by, True) + + return db.get_data_as_dict() + + def get_output_fields(self, opts): + # Return a list of requested fields, with opts.sort_by first + all_fields = set( + ['author_sort','authors','comments','cover','formats', 'id','isbn','pubdate','publisher','rating', + 'series_index','series','size','tags','timestamp', + 'title','uuid']) + + fields = all_fields + if opts.fields != 'all': + # Make a list from opts.fields + requested_fields = set(opts.fields.split(',')) + fields = list(all_fields & requested_fields) + else: + fields = list(all_fields) + fields.sort() + fields.insert(0,fields.pop(int(fields.index(opts.sort_by)))) + return fields + + def run(self, path_to_output, opts, db): ''' Run the plugin. Must be implemented in subclasses. It should generate the catalog in the format specified @@ -263,17 +290,10 @@ class CatalogPlugin(Plugin): :param path_to_output: Absolute path to the generated catalog file. :param opts: A dictionary of keyword arguments + :param db: A LibraryDatabase2 object - :return: Absolute path to the modified ebook. + :return: None - Access the opts object as a dictionary with this code: - opts_dict = vars(opts) - keys = opts_dict.keys() - keys.sort() - print "opts:" - for key in keys: - print "\t%s: %s" % (key, opts_dict[key]) - ''' # Default implementation does nothing print "CatalogPlugin.generate_catalog() default method, should be overridden in subclass" diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 76ae55d16a..b2f80e9ac6 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -600,7 +600,6 @@ def catalog_option_parser(args): dest=option.dest, help=option.help) - # print_help(parser, log) return plugin def print_help(parser, log): @@ -691,7 +690,7 @@ def command_catalog(args, dbpath): return 1 if opts.verbose: log("library.cli:command_catalog dispatching to plugin %s" % plugin.name) - plugin.run(args[1], opts) + plugin.run(args[1], opts, get_db(dbpath, opts)) return 0 # end of GR additions From 06da7762643a5201721a7bde573e21e64635b80f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 11:16:51 -0700 Subject: [PATCH 5/9] ... --- resources/recipes/atlantic.recipe | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/recipes/atlantic.recipe b/resources/recipes/atlantic.recipe index 19f3b112e2..4bf8237e16 100644 --- a/resources/recipes/atlantic.recipe +++ b/resources/recipes/atlantic.recipe @@ -65,6 +65,9 @@ class TheAtlantic(BasicNewsRecipe): date = self.tag_to_string(byline) if byline else '' description = '' + self.log('\tFound article:', title) + self.log('\t\t', url) + articles.append({ 'title':title, 'date':date, From 02e92ca8712967eeb8f5d8580c9a758aef1e18ad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 13:45:58 -0700 Subject: [PATCH 6/9] ... --- src/calibre/library/catalog.py | 209 +++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 src/calibre/library/catalog.py diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py new file mode 100644 index 0000000000..8efb36a88f --- /dev/null +++ b/src/calibre/library/catalog.py @@ -0,0 +1,209 @@ +import os + +from calibre.customize import CatalogPlugin + +class CSV_XML(CatalogPlugin): + 'CSV/XML catalog generator' + + from collections import namedtuple + + Option = namedtuple('Option', 'option, default, dest, help') + + name = 'Catalog_CSV_XML' + description = 'CSV/XML catalog generator' + supported_platforms = ['windows', 'osx', 'linux'] + author = 'Greg Riker' + version = (1, 0, 0) + file_types = set(['csv','xml']) + + cli_options = [ + Option('--fields', + default = 'all', + dest = 'fields', + help = _('The fields to output when cataloging books in the ' + 'database. Should be a comma-separated list of fields.\n' + 'Available fields: all, author_sort, authors, comments, ' + 'cover, formats, id, isbn, pubdate, publisher, rating, ' + 'series_index, series, size, tags, timestamp, title, uuid.\n' + "Default: '%default'\n" + "Applies to: CSV, XML output formats")), + + Option('--sort-by', + default = 'id', + dest = 'sort_by', + help = _('Output field to sort on.\n' + 'Available fields: author_sort, id, rating, size, timestamp, title.\n' + "Default: '%default'\n" + "Applies to: CSV, XML output formats"))] + + def run(self, path_to_output, opts, db): + from calibre.utils.logging import Log + + log = Log() + self.fmt = path_to_output[path_to_output.rfind('.') + 1:] + if opts.verbose: + log("%s:run" % self.name) + log(" path_to_output: %s" % path_to_output) + log(" Output format: %s" % self.fmt) + + # Display opts + opts_dict = vars(opts) + keys = opts_dict.keys() + keys.sort() + log(" opts:") + for key in keys: + log(" %s: %s" % (key, opts_dict[key])) + + # Get the sorted, filtered database as a dictionary + data = self.search_sort_db_as_dict(db, opts) + + if not len(data): + log.error("\nNo matching database entries for search criteria '%s'" % opts.search_text) + raise SystemExit(1) + + # Get the requested output fields as a list + fields = self.get_output_fields(opts) + + if self.fmt == 'csv': + outfile = open(path_to_output, 'w') + + # Output the field headers + outfile.write('%s\n' % ','.join(fields)) + + # Output the entry fields + for entry in data: + outstr = '' + for (x, field) in enumerate(fields): + item = entry[field] + if field in ['authors','tags','formats']: + item = ', '.join(item) + if x < len(fields) - 1: + if item is not None: + outstr += '"%s",' % str(item).replace('"','""') + else: + outstr += '"",' + else: + if item is not None: + outstr += '"%s"\n' % str(item).replace('"','""') + else: + outstr += '""\n' + outfile.write(outstr) + outfile.close() + + elif self.fmt == 'xml': + from lxml import etree + + from calibre.utils.genshi.template import MarkupTemplate + + PY_NAMESPACE = "http://genshi.edgewall.org/" + PY = "{%s}" % PY_NAMESPACE + NSMAP = {'py' : PY_NAMESPACE} + root = etree.Element('calibredb', nsmap=NSMAP) + py_for = etree.SubElement(root, PY + 'for', each="record in data") + record = etree.SubElement(py_for, 'record') + + if 'id' in fields: + record_child = etree.SubElement(record, 'id') + record_child.set(PY + "if", "record['id']") + record_child.text = "${record['id']}" + + if 'uuid' in fields: + record_child = etree.SubElement(record, 'uuid') + record_child.set(PY + "if", "record['uuid']") + record_child.text = "${record['uuid']}" + + if 'title' in fields: + record_child = etree.SubElement(record, 'title') + record_child.set(PY + "if", "record['title']") + record_child.text = "${record['title']}" + + if 'authors' in fields: + record_child = etree.SubElement(record, 'authors', sort="${record['author_sort']}") + record_subchild = etree.SubElement(record_child, PY + 'for', each="author in record['authors']") + record_subsubchild = etree.SubElement(record_subchild, 'author') + record_subsubchild.text = '$author' + + if 'publisher' in fields: + record_child = etree.SubElement(record, 'publisher') + record_child.set(PY + "if", "record['publisher']") + record_child.text = "${record['publisher']}" + + if 'rating' in fields: + record_child = etree.SubElement(record, 'rating') + record_child.set(PY + "if", "record['rating']") + record_child.text = "${record['rating']}" + + if 'date' in fields: + record_child = etree.SubElement(record, 'date') + record_child.set(PY + "if", "record['date']") + record_child.text = "${record['date']}" + + if 'pubdate' in fields: + record_child = etree.SubElement(record, 'pubdate') + record_child.set(PY + "if", "record['pubdate']") + record_child.text = "${record['pubdate']}" + + if 'size' in fields: + record_child = etree.SubElement(record, 'size') + record_child.set(PY + "if", "record['size']") + record_child.text = "${record['size']}" + + if 'tags' in fields: + # + # + # $tag + # + # + record_child = etree.SubElement(record, 'tags') + record_child.set(PY + "if", "record['tags']") + record_subchild = etree.SubElement(record_child, PY + 'for', each="tag in record['tags']") + record_subsubchild = etree.SubElement(record_subchild, 'tag') + record_subsubchild.text = '$tag' + + if 'comments' in fields: + record_child = etree.SubElement(record, 'comments') + record_child.set(PY + "if", "record['comments']") + record_child.text = "${record['comments']}" + + if 'series' in fields: + # + # ${record['series']} + # + record_child = etree.SubElement(record, 'series') + record_child.set(PY + "if", "record['series']") + record_child.set('index', "${record['series_index']}") + record_child.text = "${record['series']}" + + if 'isbn' in fields: + record_child = etree.SubElement(record, 'isbn') + record_child.set(PY + "if", "record['isbn']") + record_child.text = "${record['isbn']}" + + if 'cover' in fields: + # + # ${record['cover'].replace(os.sep, '/')} + # + record_child = etree.SubElement(record, 'cover') + record_child.set(PY + "if", "record['cover']") + record_child.text = "${record['cover']}" + + if 'formats' in fields: + # + # + # ${path.replace(os.sep, '/')} + # + # + record_child = etree.SubElement(record, 'formats') + record_child.set(PY + "if", "record['formats']") + record_subchild = etree.SubElement(record_child, PY + 'for', each="path in record['formats']") + record_subsubchild = etree.SubElement(record_subchild, 'format') + record_subsubchild.text = "${path.replace(os.sep, '/')}" + + outfile = open(path_to_output, 'w') + template = MarkupTemplate(etree.tostring(root, xml_declaration=True, + encoding="UTF-8", pretty_print=True)) + outfile.write(template.generate(data=data, os=os).render('xml')) + outfile.close() + + return None + From 5b4f5ec0d57a51e41f79180e8bb9520cc2c4ac72 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 13:51:03 -0700 Subject: [PATCH 7/9] New recipe for ThinkProgress by Xantan Gum --- resources/recipes/starwars.recipe | 5 ++--- resources/recipes/think_progress.recipe | 12 ++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 resources/recipes/think_progress.recipe diff --git a/resources/recipes/starwars.recipe b/resources/recipes/starwars.recipe index bb04e1ff6b..d42b88f696 100644 --- a/resources/recipes/starwars.recipe +++ b/resources/recipes/starwars.recipe @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from calibre.web.feeds.news import BasicNewsRecipe -from calibre.ebooks.BeautifulSoup import BeautifulSoup class TheForce(BasicNewsRecipe): title = u'The Force' @@ -21,11 +20,11 @@ class TheForce(BasicNewsRecipe): #dict(name='div', attrs={'class':['pt-box-title', 'pt-box-content', 'blog-entry-footer', 'item-list', 'article-sub-meta']}), #dict(name='div', attrs={'id':['block-td_search_160', 'block-cam_search_160']}), #dict(name='table', attrs={'cellspacing':'0'}), - #dict(name='ul', attrs={'class':'articleTools'}), + #dict(name='ul', attrs={'class':'articleTools'}), ] feeds = [ -('The Force', +('The Force', 'http://www.theforce.net/outnews/tfnrdf.xml'), ] diff --git a/resources/recipes/think_progress.recipe b/resources/recipes/think_progress.recipe new file mode 100644 index 0000000000..2358c6592c --- /dev/null +++ b/resources/recipes/think_progress.recipe @@ -0,0 +1,12 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1263409732(BasicNewsRecipe): + title = u'Think Progress' + description = u'A compilation of progressive articles on social and economic justice, healthy communities, media accountability, global and domestic security.' + __author__ = u'Xanthan Gum' + language = 'en' + + oldest_article = 7 + max_articles_per_feed = 100 + + feeds = [(u'News Articles', u'http://thinkprogress.org/feed/')] From 8477c4313e7977e9bde2f33e12de5b742f4b9c8b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 14:24:28 -0700 Subject: [PATCH 8/9] New recipe for El Universal (Venezuela) by Darko Miletic --- resources/images/news/eluniversal_ve.png | Bin 0 -> 521 bytes resources/recipes/eluniversal_ve.recipe | 52 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 resources/images/news/eluniversal_ve.png create mode 100644 resources/recipes/eluniversal_ve.recipe diff --git a/resources/images/news/eluniversal_ve.png b/resources/images/news/eluniversal_ve.png new file mode 100644 index 0000000000000000000000000000000000000000..9211ee2739be1baf2cd9c60d15163d5421f2e835 GIT binary patch literal 521 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b zK-vS0-A-oPfdtD69Mgd`SU*F|v9*U87#NK_T^vI!PG6n8(VMAI@&!&Uc;un9L&M9{@mC*aT z?0EH_^Y8b5ueF!l#&eWw+1j8#<_w)#v5&X?n>+cm)~98UKKUK~yfBG%%G!qG%L?k2 zKK_{?%A9h;=*qvsrnM#_XB60O_&$7C=k_w>cqX@Owbb)P?$+O?xQLutBAS}bXkj(w zsawaKixP9D8GJhE;q)%SvNYLYyZV%KB?@!CUvG#B3*0^P;ZK84)3fF4CKyg_In#FN z^Fdi1hxxU;HJ40^w' +''' +www.eluniversal.com +''' + +from calibre import strftime +from calibre.web.feeds.recipes import BasicNewsRecipe + +class ElUniversal(BasicNewsRecipe): + title = 'El Universal' + __author__ = 'Darko Miletic' + description = 'Noticias de Venezuela' + oldest_article = 2 + max_articles_per_feed = 100 + no_stylesheets = True + use_embedded_content = False + encoding = 'cp1252' + publisher = 'El Universal' + category = 'news, Caracas, Venezuela, world' + language = 'es' + cover_url = strftime('http://static.eluniversal.com/%Y/%m/%d/portada.jpg') + + conversion_options = { + 'comments' : description + ,'tags' : category + ,'language' : language + ,'publisher' : publisher + } + + keep_only_tags = [dict(name='div', attrs={'class':'Nota'})] + remove_tags = [ + dict(name=['object','link','script','iframe']) + ,dict(name='div',attrs={'class':'Herramientas'}) + ] + + feeds = [ + (u'Ultimas Noticias', u'http://www.eluniversal.com/rss/avances.xml' ) + ,(u'Economia' , u'http://www.eluniversal.com/rss/eco_avances.xml') + ,(u'Internacionales' , u'http://www.eluniversal.com/rss/int_avances.xml') + ,(u'Deportes' , u'http://www.eluniversal.com/rss/dep_avances.xml') + ,(u'Cultura' , u'http://www.eluniversal.com/rss/cul_avances.xml') + ,(u'Nacional y politica' , u'http://www.eluniversal.com/rss/pol_avances.xml') + ,(u'Ciencia y tecnologia', u'http://www.eluniversal.com/rss/cyt_avances.xml') + ,(u'Universo empresarial', u'http://www.eluniversal.com/rss/uni_avances.xml') + ,(u'Caracas' , u'http://www.eluniversal.com/rss/ccs_avances.xml') + ] + + def print_version(self, url): + rp,sep,rest = url.rpartition('/') + return rp + sep + 'imp_' + rest + From edf38c51f1f29d2f821714698810fbe81857a63b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 13 Jan 2010 15:33:01 -0700 Subject: [PATCH 9/9] Implement #4536 (Auto fit-to-view of cover display in details view) --- src/calibre/gui2/dialogs/book_info.py | 65 ++++++++++++++++-------- src/calibre/gui2/dialogs/book_info.ui | 72 +++++++++++++++------------ 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/src/calibre/gui2/dialogs/book_info.py b/src/calibre/gui2/dialogs/book_info.py index 72b305fd78..8e7d8ee178 100644 --- a/src/calibre/gui2/dialogs/book_info.py +++ b/src/calibre/gui2/dialogs/book_info.py @@ -7,34 +7,47 @@ __docformat__ = 'restructuredtext en' ''' import textwrap, os -from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl +from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo +from calibre.gui2 import dynamic +from calibre import fit_image class BookInfo(QDialog, Ui_BookInfo): - + def __init__(self, parent, view, row): QDialog.__init__(self, parent) Ui_BookInfo.__init__(self) self.setupUi(self) + self.cover_pixmap = None desktop = QCoreApplication.instance().desktop() screen_height = desktop.availableGeometry().height() - 100 self.resize(self.size().width(), screen_height) - - + + self.view = view self.current_row = None + self.fit_cover.setChecked(dynamic.get('book_info_dialog_fit_cover', + False)) self.refresh(row) self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave) self.connect(self.next_button, SIGNAL('clicked()'), self.next) self.connect(self.previous_button, SIGNAL('clicked()'), self.previous) self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path) - + self.fit_cover.stateChanged.connect(self.toggle_cover_fit) + self.cover.resizeEvent = self.cover_view_resized + + def toggle_cover_fit(self, state): + dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked()) + self.resize_cover() + + def cover_view_resized(self, event): + QTimer.singleShot(1, self.resize_cover) def slave(self, current, previous): row = current.row() self.refresh(row) - + def open_book_path(self, path): if os.sep in unicode(path): QDesktopServices.openUrl(QUrl('file:'+path)) @@ -43,41 +56,53 @@ class BookInfo(QDialog, Ui_BookInfo): path = self.view.model().db.format_abspath(self.current_row, format) if path is not None: QDesktopServices.openUrl(QUrl('file:'+path)) - - + + def next(self): row = self.view.currentIndex().row() ni = self.view.model().index(row+1, 0) if ni.isValid(): self.view.setCurrentIndex(ni) - + def previous(self): row = self.view.currentIndex().row() ni = self.view.model().index(row-1, 0) if ni.isValid(): self.view.setCurrentIndex(ni) - + + def resize_cover(self): + if self.cover_pixmap is None: + return + self.setWindowIcon(QIcon(self.cover_pixmap)) + self.scene = QGraphicsScene() + pixmap = self.cover_pixmap + if self.fit_cover.isChecked(): + scaled, new_width, new_height = fit_image(pixmap.width(), + pixmap.height(), self.cover.size().width()-10, + self.cover.size().height()-10) + if scaled: + pixmap = pixmap.scaled(new_width, new_height, + Qt.KeepAspectRatio, Qt.SmoothTransformation) + self.scene.addPixmap(pixmap) + self.cover.setScene(self.scene) + def refresh(self, row): if isinstance(row, QModelIndex): row = row.row() if row == self.current_row: return self.previous_button.setEnabled(False if row == 0 else True) - self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) + self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True) self.current_row = row info = self.view.model().get_book_info(row) self.setWindowTitle(info[_('Title')]) self.title.setText(''+info.pop(_('Title'))) self.comments.setText(info.pop(_('Comments'), '')) - + cdata = info.pop('cover', '') - pixmap = QPixmap.fromImage(cdata) - self.setWindowIcon(QIcon(pixmap)) - - self.scene = QGraphicsScene() - self.scene.addPixmap(pixmap) - self.cover.setScene(self.scene) - + self.cover_pixmap = QPixmap.fromImage(cdata) + self.resize_cover() + rows = u'' self.text.setText('') self.data = info @@ -94,4 +119,4 @@ class BookInfo(QDialog, Ui_BookInfo): txt = info[key] txt = u'
\n'.join(textwrap.wrap(txt, 120)) rows += u'%s:%s'%(key, txt) - self.text.setText(u''+rows+'
') \ No newline at end of file + self.text.setText(u''+rows+'
') diff --git a/src/calibre/gui2/dialogs/book_info.ui b/src/calibre/gui2/dialogs/book_info.ui index 27e15f96a1..02dae12281 100644 --- a/src/calibre/gui2/dialogs/book_info.ui +++ b/src/calibre/gui2/dialogs/book_info.ui @@ -1,7 +1,8 @@ - + + BookInfo - - + + 0 0 @@ -9,70 +10,77 @@ 783 - + Dialog - - - - + + + + TextLabel - + Qt::AlignCenter - - + + - - + + - - + + TextLabel - + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - + true - - + + Comments - - - + + + - + + + Fit &cover to view + + + + + - - + + &Previous - - + + :/images/previous.svg:/images/previous.svg - - + + &Next - - + + :/images/next.svg:/images/next.svg @@ -84,7 +92,7 @@ - +