diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index 736ee2a940..571ba542d0 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,75 @@ 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 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 + 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 + :param db: A LibraryDatabase2 object + + :return: None + + ''' + # Default implementation does nothing + raise NotImplementedError('CatalogPlugin.generate_catalog() default ' + 'method, should be overridden in subclass') diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index f280aff456..81e9021817 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -421,7 +421,8 @@ from calibre.devices.binatone.driver import README from calibre.devices.hanvon.driver import N516 from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon -plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon] +from calibre.library.catalog import CSV_XML +plugins = [HTML2ZIP, PML2PMLZ, GoogleBooks, ISBNDB, Amazon, CSV_XML] plugins += [ ComicInput, EPUBInput, 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..b2f80e9ac6 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -583,8 +583,120 @@ 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) + + 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, get_db(dbpath, 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():