Catalog generation for the calibre GUI

This commit is contained in:
Kovid Goyal 2010-01-21 09:44:06 -07:00
commit 22035b8e07
8 changed files with 326 additions and 146 deletions

View File

@ -2,10 +2,11 @@ from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
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):
'''
@ -231,6 +232,8 @@ 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([])
@ -249,14 +252,18 @@ class CatalogPlugin(Plugin):
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()
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

View File

@ -4,9 +4,14 @@ __license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__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)

View File

@ -6,29 +6,121 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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+"(.+?)(?<!\\)",.+?\)',
re.DOTALL).sub(r'_("\1")', dat)
open(compiled_form, 'wb').write(dat)
# Import the dynamic PluginWidget() from .py file provided in plugin.zip
try:
sys.path.insert(0, plugin.resources_path)
catalog_widget = __import__(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
finally:
sys.path.remove(plugin.resources_path)
else:
info("No dynamic tab resources found for %s" % name)
self.widgets = sorted(self.widgets, key=lambda x:(x.TITLE, x.TITLE))
for pw in self.widgets:
page = self.tabs.addTab(pw,pw.TITLE)
# Generate a sorted list of installed catalog formats/sync_enabled pairs
fmts = sorted([x[0] for x in self.fmts])
self.sync_enabled_formats = []
for fmt in self.fmts:
if fmt[1]:
self.sync_enabled_formats.append(fmt[0])
# Callback when format changes
self.format.currentIndexChanged.connect(self.format_changed)
# Add the installed catalog format list to the format QComboBox
self.format.addItems(fmts)
pref = dynamic.get('catalog_preferred_format', 'EPUB')
pref = dynamic.get('catalog_preferred_format', 'CSV')
idx = self.format.findText(pref)
if idx > -1:
self.format.setCurrentIndex(idx)
@ -38,7 +130,7 @@ class Catalog(QDialog, Ui_Dialog):
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)

View File

@ -6,20 +6,26 @@
<rect>
<x>0</x>
<y>0</y>
<width>628</width>
<height>503</height>
<width>611</width>
<height>514</height>
</rect>
</property>
<property name="windowTitle">
<string>Generate catalog</string>
</property>
<property name="windowIcon">
<iconset resource="../../../work/calibre/resources/images.qrc">
<iconset>
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>430</x>
<y>470</y>
<width>164</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
@ -27,9 +33,15 @@
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTabWidget" name="tabs">
<property name="geometry">
<rect>
<x>12</x>
<y>39</y>
<width>579</width>
<height>411</height>
</rect>
</property>
<property name="currentIndex">
<number>0</number>
</property>
@ -64,6 +76,16 @@
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="title"/>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="sync">
<property name="text">
<string>&amp;Send catalog to device automatically</string>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
@ -77,22 +99,18 @@
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="sync">
<property name="text">
<string>&amp;Send catalog to device automatically</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLineEdit" name="title"/>
</item>
</layout>
</widget>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="count">
<property name="geometry">
<rect>
<x>12</x>
<y>12</y>
<width>205</width>
<height>17</height>
</rect>
</property>
<property name="font">
<font>
<weight>75</weight>
@ -103,8 +121,6 @@
<string>Generate catalog for {0} books</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../work/calibre/resources/images.qrc"/>

View File

@ -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

View File

@ -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)
@ -1361,23 +1361,29 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
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.fmt = fmt
job.catalog_sync, job.catalog_title = sync, title
self.status_bar.showMessage(_('Generating %s catalog...')%fmt)
@ -1392,7 +1398,12 @@ 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 #################################

View File

@ -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)

View File

@ -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