diff --git a/src/calibre/customize/__init__.py b/src/calibre/customize/__init__.py index a6bf55eec4..5ab9ac6d1c 100644 --- a/src/calibre/customize/__init__.py +++ b/src/calibre/customize/__init__.py @@ -2,10 +2,11 @@ from __future__ import with_statement __license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal ' -import sys +import atexit, os, shutil, sys, tempfile, zipfile -from calibre.ptempfile import PersistentTemporaryFile from calibre.constants import numeric_version +from calibre.ptempfile import PersistentTemporaryFile + class Plugin(object): ''' @@ -225,12 +226,14 @@ class MetadataWriterPlugin(Plugin): ''' pass - + class CatalogPlugin(Plugin): ''' A plugin that implements a catalog generator. ''' + resources_path = None + #: Output file type for which this plugin should be run #: For example: 'epub' or 'xml' file_types = set([]) @@ -248,15 +251,19 @@ class CatalogPlugin(Plugin): #: '%default' + "'"))] cli_options = [] + def search_sort_db(self, db, opts): - if opts.search_text: + + # If declared, --ids overrides any declared search criteria + if not opts.ids and 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() + + return db.get_data_as_dict(ids=opts.ids) def get_output_fields(self, opts): # Return a list of requested fields, with opts.sort_by first @@ -272,11 +279,40 @@ class CatalogPlugin(Plugin): fields = list(all_fields & requested_fields) else: fields = list(all_fields) + fields.sort() - fields.insert(0,fields.pop(int(fields.index(opts.sort_by)))) + if opts.sort_by: + fields.insert(0,fields.pop(int(fields.index(opts.sort_by)))) return fields - def run(self, path_to_output, opts, db): + def initialize(self): + ''' + If plugin is not a built-in, copy the plugin's .ui and .py files from + the zip file to $TMPDIR. + Tab will be dynamically generated and added to the Catalog Options dialog in + calibre.gui2.dialogs.catalog.py:Catalog + ''' + from calibre.customize.builtins import plugins as builtin_plugins + from calibre.customize.ui import config + from calibre.ptempfile import PersistentTemporaryDirectory + + if not type(self) in builtin_plugins and \ + not self.name in config['disabled_plugins']: + files_to_copy = ["%s.%s" % (self.name.lower(),ext) for ext in ["ui","py"]] + resources = zipfile.ZipFile(self.plugin_path,'r') + + if self.resources_path is None: + self.resources_path = PersistentTemporaryDirectory('_plugin_resources', prefix='') + + for file in files_to_copy: + try: + resources.extract(file, self.resources_path) + except: + print " customize:__init__.initialize(): %s not found in %s" % (file, os.path.basename(self.plugin_path)) + continue + resources.close() + + def run(self, path_to_output, opts, db, ids): ''' Run the plugin. Must be implemented in subclasses. It should generate the catalog in the format specified diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py index 32cd883727..b951244e71 100644 --- a/src/calibre/gui2/convert/gui_conversion.py +++ b/src/calibre/gui2/convert/gui_conversion.py @@ -4,9 +4,14 @@ __license__ = 'GPL 3' __copyright__ = '2009, John Schember ' __docformat__ = 'restructuredtext en' -from calibre.ebooks.conversion.plumber import Plumber -from calibre.utils.logging import Log +import os +from optparse import OptionParser + from calibre.customize.conversion import OptionRecommendation, DummyReporter +from calibre.ebooks.conversion.plumber import Plumber +from calibre.customize.ui import plugin_for_catalog_format +from calibre.utils.logging import Log +from calibre.gui2 import choose_dir, Application def gui_convert(input, output, recommendations, notification=DummyReporter(), abort_after_input_dump=False, log=None): @@ -20,7 +25,7 @@ def gui_convert(input, output, recommendations, notification=DummyReporter(), plumber.run() -def gui_catalog(fmt, title, dbspec, ids, out_file_name, +def gui_catalog(fmt, title, dbspec, ids, out_file_name, fmt_options, notification=DummyReporter(), log=None): if log is None: log = Log() @@ -31,8 +36,28 @@ def gui_catalog(fmt, title, dbspec, ids, out_file_name, db = LibraryDatabase2(dbpath) else: # To be implemented in the future pass - # Implement the interface to the catalog generating code here - db + + # Create a minimal OptionParser that we can append to + parser = OptionParser() + args = [] + parser.add_option("--verbose", action="store_true", dest="verbose", default=True) + opts, args = parser.parse_args() + + # Populate opts + opts.ids = ids + opts.search_text = None + opts.sort_by = None + + # Extract the option dictionary to comma-separated lists + for option in fmt_options: + setattr(opts,option, ','.join(fmt_options[option])) + + # Fetch and run the plugin for fmt + plugin = plugin_for_catalog_format(fmt) + plugin.run(out_file_name, opts, db) + + + diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py index 29b6ef972d..8407e2c426 100644 --- a/src/calibre/gui2/dialogs/catalog.py +++ b/src/calibre/gui2/dialogs/catalog.py @@ -6,39 +6,131 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import QDialog +import os, shutil, sys, tempfile +from PyQt4.Qt import QDialog, QWidget + +from calibre.customize.ui import config from calibre.gui2.dialogs.catalog_ui import Ui_Dialog -from calibre.gui2 import dynamic -from calibre.customize.ui import available_catalog_formats +from calibre.gui2 import gprefs, dynamic +from calibre.customize.ui import available_catalog_formats, catalog_plugins +from calibre.gui2.catalog.catalog_csv_xml import PluginWidget class Catalog(QDialog, Ui_Dialog): + ''' Catalog Dialog builder''' + widgets = [] def __init__(self, parent, dbspec, ids): + import re, cStringIO + from calibre import prints as info + from calibre.gui2 import dynamic + from PyQt4.uic import compileUi + QDialog.__init__(self, parent) + + # Run the dialog setup generated from catalog.ui self.setupUi(self) self.dbspec, self.ids = dbspec, ids + # Display the number of books we've been passed self.count.setText(unicode(self.count.text()).format(len(ids))) + + # Display the last-used title self.title.setText(dynamic.get('catalog_last_used_title', _('My Books'))) - fmts = sorted([x.upper() for x in available_catalog_formats()]) + # GwR *** Add option tabs for built-in formats + # This code models #69 in calibre/gui2/dialogs/config/__init__.py + + self.fmts = [] + + from calibre.customize.builtins import plugins as builtin_plugins + from calibre.customize import CatalogPlugin + + for plugin in catalog_plugins(): + if plugin.name in config['disabled_plugins']: + continue + + name = plugin.name.lower().replace(' ', '_') + if type(plugin) in builtin_plugins: + #info("Adding widget for builtin Catalog plugin %s" % plugin.name) + try: + catalog_widget = __import__('calibre.gui2.catalog.'+name, + fromlist=[1]) + pw = catalog_widget.PluginWidget() + pw.initialize(name) + pw.ICON = I('forward.svg') + self.widgets.append(pw) + [self.fmts.append([file_type.upper(), pw.sync_enabled,pw]) for file_type in plugin.file_types] + except ImportError: + info("ImportError with %s" % name) + continue + else: + # Load dynamic tab + form = os.path.join(plugin.resources_path,'%s.ui' % name) + klass = os.path.join(plugin.resources_path,'%s.py' % name) + compiled_form = os.path.join(plugin.resources_path,'%s_ui.py' % name) + + if os.path.exists(form) and os.path.exists(klass): + #info("Adding widget for user-installed Catalog plugin %s" % plugin.name) + + # Compile the .ui form provided in plugin.zip + if not os.path.exists(compiled_form): + # info('\tCompiling form', form) + buf = cStringIO.StringIO() + compileUi(form, buf) + dat = buf.getvalue() + dat = re.compile(r'QtGui.QApplication.translate\(.+?,\s+"(.+?)(? -1: self.format.setCurrentIndex(idx) if self.sync.isEnabled(): self.sync.setChecked(dynamic.get('catalog_sync_to_device', True)) - + def format_changed(self, idx): cf = unicode(self.format.currentText()) - if cf in ('EPUB', 'MOBI'): + if cf in self.sync_enabled_formats: self.sync.setEnabled(True) else: self.sync.setDisabled(True) diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui index aa47f3c0c3..c18e08ef65 100644 --- a/src/calibre/gui2/dialogs/catalog.ui +++ b/src/calibre/gui2/dialogs/catalog.ui @@ -6,105 +6,121 @@ 0 0 - 628 - 503 + 611 + 514 Generate catalog - + :/images/library.png:/images/library.png - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - 0 - - - - Catalog options - - - - - - Catalog &format: - - - format - - - - - - - - - - Catalog &title (existing catalog with the same title will be replaced): - - - true - - - title - - - - - - - Qt::Vertical - - - - 20 - 299 - - - - - - - - &Send catalog to device automatically - - - - - - - - - - - - - - - 75 - true - - - - Generate catalog for {0} books - - - - + + + + 430 + 470 + 164 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 12 + 39 + 579 + 411 + + + + 0 + + + + Catalog options + + + + + + Catalog &format: + + + format + + + + + + + + + + Catalog &title (existing catalog with the same title will be replaced): + + + true + + + title + + + + + + + + + + &Send catalog to device automatically + + + + + + + Qt::Vertical + + + + 20 + 299 + + + + + + + + + + + 12 + 12 + 205 + 17 + + + + + 75 + true + + + + Generate catalog for {0} books + + diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 2bb891d36b..b23e0b6259 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -238,19 +238,36 @@ def fetch_scheduled_recipe(arg): def generate_catalog(parent, dbspec, ids): from calibre.gui2.dialogs.catalog import Catalog + + # Build the Catalog dialog in gui2.dialogs.catalog d = Catalog(parent, dbspec, ids) + if d.exec_() != d.Accepted: return None + + # Create the output file out = PersistentTemporaryFile(suffix='_catalog_out.'+d.catalog_format.lower()) + + # Retrieve plugin options + fmt_options = {} + for x in range(d.tabs.count()): + if str(d.tabs.tabText(x)).find(str(d.catalog_format)) > -1: + for fmt in d.fmts: + if fmt[0] == d.catalog_format: + fmt_options = fmt[2].options() + # print "gui2.tools:generate_catalog(): options for %s: %s" % (fmt[0], fmt_options) + args = [ d.catalog_format, d.catalog_title, dbspec, ids, out.name, + fmt_options ] out.close() + # This calls gui2.convert.gui_conversion:gui_catalog() return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \ d.catalog_title diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 98b416eaa3..ccff7ccdc8 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en' '''The main GUI''' -import os, sys, textwrap, collections, time +import atexit, os, shutil, sys, tempfile, textwrap, collections, time from xml.parsers.expat import ExpatError from Queue import Queue, Empty from threading import Thread @@ -357,7 +357,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): cm.addAction(_('Bulk convert')) cm.addSeparator() ac = cm.addAction( - _('Create catalog of the books in your calibre library')) + _('Create catalog of books in your calibre library')) ac.triggered.connect(self.generate_catalog) self.action_convert.setMenu(cm) self._convert_single_hook = partial(self.convert_ebook, bulk=False) @@ -1359,26 +1359,32 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): ############################### Generate catalog ########################### - def generate_catalog(self): + def generate_catalog(self): rows = self.library_view.selectionModel().selectedRows() - if not rows or len(rows) < 3: + if not rows or len(rows) < 2: rows = xrange(self.library_view.model().rowCount(QModelIndex())) ids = map(self.library_view.model().id, rows) + dbspec = None if not ids: return error_dialog(self, _('No books selected'), _('No books selected to generate catalog for'), show=True) + + # Calling gui2.tools:generate_catalog() ret = generate_catalog(self, dbspec, ids) if ret is None: return + func, args, desc, out, sync, title = ret + fmt = os.path.splitext(out)[1][1:].upper() job = self.job_manager.run_job( Dispatcher(self.catalog_generated), func, args=args, description=desc) job.catalog_file_path = out - job.catalog_sync, job.catalog_title = sync, title + job.fmt = fmt + job.catalog_sync, job.catalog_title = sync, title self.status_bar.showMessage(_('Generating %s catalog...')%fmt) def catalog_generated(self, job): @@ -1392,8 +1398,13 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI): dynamic.set('catalogs_to_be_synced', sync) self.status_bar.showMessage(_('Catalog generated.'), 3000) self.sync_catalogs() - - + if job.fmt in ['CSV','XML']: + export_dir = choose_dir(self, 'Export Catalog Directory', + 'Select destination for %s.%s' % (job.catalog_title, job.fmt.lower())) + if export_dir: + destination = os.path.join(export_dir, '%s.%s' % (job.catalog_title, job.fmt.lower())) + shutil.copyfile(job.catalog_file_path, destination) + ############################### Fetch news ################################# def download_scheduled_recipe(self, arg): diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py index 42f0139cd8..32f2503b2c 100644 --- a/src/calibre/library/catalog.py +++ b/src/calibre/library/catalog.py @@ -40,8 +40,9 @@ class CSV_XML(CatalogPlugin): from calibre.utils.logging import Log log = Log() - self.fmt = path_to_output[path_to_output.rfind('.') + 1:] - if opts.verbose: + self.fmt = path_to_output.rpartition('.')[2] + + if False and opts.verbose: log("%s:run" % self.name) log(" path_to_output: %s" % path_to_output) log(" Output format: %s" % self.fmt) @@ -53,7 +54,7 @@ class CSV_XML(CatalogPlugin): 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(db, opts) diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 6e2d672202..ddfb96704c 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -644,6 +644,10 @@ def catalog_option_parser(args): output, fmt = validate_command_line(parser, args, log) # Add options common to all catalog plugins + parser.add_option('-i', '--ids', default=None, dest='ids', + help=_("Comma-separated list of database IDs to catalog.\n" + "If declared, --search is ignored.\n" + "Default: all")) 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 " @@ -656,31 +660,6 @@ def catalog_option_parser(args): # Add options specific to fmt plugin plugin = add_plugin_parser_options(fmt, parser, log) - # Merge options from GUI Preferences - ''' - # Placeholder sample code until we implement 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): @@ -693,6 +672,9 @@ def command_catalog(args, dbpath): return 1 if opts.verbose: log("library.cli:command_catalog dispatching to plugin %s" % plugin.name) + if opts.ids: + opts.ids = [int(id) for id in opts.ids.split(',')] + with plugin: plugin.run(args[1], opts, get_db(dbpath, opts)) return 0