Framework for GUI catalog generation

This commit is contained in:
Kovid Goyal 2010-01-16 13:29:32 -07:00
parent 392b1bf2d1
commit e010921c55
9 changed files with 391 additions and 1 deletions

View File

@ -20,3 +20,20 @@ def gui_convert(input, output, recommendations, notification=DummyReporter(),
plumber.run()
def gui_catalog(fmt, title, dbspec, ids, out_file_name,
notification=DummyReporter(), log=None):
if log is None:
log = Log()
if dbspec is None:
from calibre.utils.config import prefs
from calibre.library.database2 import LibraryDatabase2
dbpath = prefs['library_path']
db = LibraryDatabase2(dbpath)
else: # To be implemented in the future
pass
# Implement the interface to the catalog generating code here
db

View File

@ -676,6 +676,65 @@ class DeviceGUI(object):
self.status_bar.showMessage(_('Sent news to')+' '+\
', '.join(sent_mails), 3000)
def sync_catalogs(self, send_ids=None, do_auto_convert=True):
if self.device_connected:
settings = self.device_manager.device.settings()
ids = list(dynamic.get('catalogs_to_be_synced', set([]))) if send_ids is None else send_ids
ids = [id for id in ids if self.library_view.model().db.has_id(id)]
files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(
ids, settings.format_map,
exclude_auto=do_auto_convert)
auto = []
if do_auto_convert and _auto_ids:
for id in _auto_ids:
dbfmts = self.library_view.model().db.formats(id, index_is_id=True)
formats = [] if dbfmts is None else \
[f.lower() for f in dbfmts.split(',')]
if set(formats).intersection(available_input_formats()) \
and set(settings.format_map).intersection(available_output_formats()):
auto.append(id)
if auto:
format = None
for fmt in settings.format_map:
if fmt in list(set(settings.format_map).intersection(set(available_output_formats()))):
format = fmt
break
if format is not None:
autos = [self.library_view.model().db.title(id, index_is_id=True) for id in auto]
autos = '\n'.join('%s'%i for i in autos)
if question_dialog(self, _('No suitable formats'),
_('Auto convert the following books before uploading to '
'the device?'), det_msg=autos):
self.auto_convert_catalogs(auto, format)
files = [f for f in files if f is not None]
if not files:
dynamic.set('catalogs_to_be_synced', set([]))
return
metadata = self.library_view.model().metadata_for(ids)
names = []
for mi in metadata:
prefix = ascii_filename(mi.title)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id,
os.path.splitext(f.name)[1]))
if mi.cover and os.access(mi.cover, os.R_OK):
mi.thumbnail = self.cover_to_thumbnail(open(mi.cover,
'rb').read())
dynamic.set('catalogs_to_be_synced', set([]))
if files:
remove = []
space = { self.location_view.model().free[0] : None,
self.location_view.model().free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, names, metadata,
on_card=on_card,
memory=[[f.name for f in files], remove])
self.status_bar.showMessage(_('Sending catalogs to device.'), 5000)
def sync_news(self, send_ids=None, do_auto_convert=True):
if self.device_connected:

View File

@ -0,0 +1,54 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QDialog
from calibre.gui2.dialogs.catalog_ui import Ui_Dialog
from calibre.gui2 import dynamic
from calibre.customize.ui import available_catalog_formats
class Catalog(QDialog, Ui_Dialog):
def __init__(self, parent, dbspec, ids):
QDialog.__init__(self, parent)
self.setupUi(self)
self.dbspec, self.ids = dbspec, ids
self.count.setText(unicode(self.count.text()).format(len(ids)))
self.title.setText(dynamic.get('catalog_last_used_title',
_('My Books')))
fmts = sorted([x.upper() for x in available_catalog_formats()])
self.format.currentIndexChanged.connect(self.format_changed)
self.format.addItems(fmts)
pref = dynamic.get('catalog_preferred_format', 'EPUB')
idx = self.format.findText(pref)
if idx > -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'):
self.sync.setEnabled(True)
else:
self.sync.setDisabled(True)
self.sync.setChecked(False)
def accept(self):
self.catalog_format = unicode(self.format.currentText())
dynamic.set('catalog_preferred_format', self.catalog_format)
self.catalog_title = unicode(self.title.text())
dynamic.set('catalog_last_used_title', self.catalog_title)
self.catalog_sync = bool(self.sync.isChecked())
dynamic.set('catalog_sync_to_device', self.catalog_sync)
QDialog.accept(self)

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>628</width>
<height>503</height>
</rect>
</property>
<property name="windowTitle">
<string>Generate catalog</string>
</property>
<property name="windowIcon">
<iconset resource="../../../work/calibre/resources/images.qrc">
<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="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QTabWidget" name="tabs">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Catalog options</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Catalog &amp;format:</string>
</property>
<property name="buddy">
<cstring>format</cstring>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QComboBox" name="format"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Catalog &amp;title (existing catalog with the same title will be replaced):</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="buddy">
<cstring>title</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>299</height>
</size>
</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="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Generate catalog for {0} books</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../work/calibre/resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -232,6 +232,11 @@ class BooksModel(QAbstractTableModel):
self.count_changed()
return ret
def add_catalog(self, path, title):
ret = self.db.add_catalog(path, title)
self.count_changed()
return ret
def count_changed(self, *args):
self.emit(SIGNAL('count_changed(int)'), self.db.count())

View File

@ -236,6 +236,24 @@ def fetch_scheduled_recipe(arg):
return 'gui_convert', args, _('Fetch news from ')+arg['title'], fmt.upper(), [pt]
def generate_catalog(parent, dbspec, ids):
from calibre.gui2.dialogs.catalog import Catalog
d = Catalog(parent, dbspec, ids)
if d.exec_() != d.Accepted:
return None
out = PersistentTemporaryFile(suffix='_catalog_out.'+d.catalog_format.lower())
args = [
d.catalog_format,
d.catalog_title,
dbspec,
ids,
out.name,
]
out.close()
return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \
d.catalog_title
def convert_existing(parent, db, book_ids, output_format):
already_converted_ids = []
already_converted_titles = []

View File

@ -48,7 +48,7 @@ from calibre.gui2.jobs import JobManager, JobsDialog
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
fetch_scheduled_recipe
fetch_scheduled_recipe, generate_catalog
from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.search import SearchDialog
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
@ -355,6 +355,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
cm = QMenu()
cm.addAction(_('Convert individually'))
cm.addAction(_('Bulk convert'))
cm.addSeparator()
ac = cm.addAction(
_('Create catalog of the 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)
QObject.connect(cm.actions()[0],
@ -894,6 +898,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
view.resizeRowsToContents()
view.resize_on_select = not view.isVisible()
self.sync_news()
self.sync_catalogs()
############################################################################
@ -1339,6 +1344,43 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
############################################################################
############################### Generate catalog ###########################
def generate_catalog(self):
rows = self.library_view.selectionModel().selectedRows()
if not rows:
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)
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
self.status_bar.showMessage(_('Generating %s catalog...')%fmt)
def catalog_generated(self, job):
if job.failed:
return self.job_exception(job)
id = self.library_view.model().add_catalog(job.catalog_file_path, job.catalog_title)
self.library_view.model().reset()
if job.catalog_sync:
sync = dynamic.get('catalogs_to_be_synced', set([]))
sync.add(id)
dynamic.set('catalogs_to_be_synced', sync)
self.status_bar.showMessage(_('Catalog generated.'), 3000)
self.sync_catalogs()
############################### Fetch news #################################
def download_scheduled_recipe(self, arg):
@ -1398,6 +1440,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted_news)
def auto_convert_catalogs(self, book_ids, format):
previous = self.library_view.currentIndex()
rows = [x.row() for x in \
self.library_view.selectionModel().selectedRows()]
jobs, changed, bad = convert_single_ebook(self, self.library_view.model().db, book_ids, True, format)
if jobs == []: return
self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted_catalogs)
def get_books_for_conversion(self):
rows = [r.row() for r in \
self.library_view.selectionModel().selectedRows()]
@ -1463,6 +1516,11 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.book_converted(job)
self.sync_news(send_ids=[book_id], do_auto_convert=False)
def book_auto_converted_catalogs(self, job):
temp_files, fmt, book_id = self.conversion_jobs[job]
self.book_converted(job)
self.sync_catalogs(send_ids=[book_id], do_auto_convert=False)
def book_converted(self, job):
temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3]
try:

View File

@ -1407,6 +1407,36 @@ class LibraryDatabase2(LibraryDatabase):
if notify:
self.notify('metadata', [id])
def add_catalog(self, path, title):
format = os.path.splitext(path)[1][1:].lower()
stream = path if hasattr(path, 'read') else open(path, 'rb')
stream.seek(0)
matches = self.data.get_matches('title', title)
if matches:
tag_matches = self.data.get_matches('tags', _('Catalog'))
matches = matches.intersection(tag_matches)
db_id = None
if matches:
db_id = list(matches)[0]
if db_id is None:
obj = self.conn.execute('INSERT INTO books(title, author_sort) VALUES (?, ?)',
(title, 'calibre'))
db_id = obj.lastrowid
self.data.books_added([db_id], self)
self.set_path(db_id, index_is_id=True)
self.conn.commit()
mi = MetaInformation(title, ['calibre'])
mi.tags = [_('Catalog')]
self.set_metadata(db_id, mi)
self.add_format(db_id, format, stream, index_is_id=True)
if not hasattr(path, 'read'):
stream.close()
self.conn.commit()
self.data.refresh_ids(self, [db_id]) # Needed to update format list and size
return db_id
def add_news(self, path, arg):
format = os.path.splitext(path)[1][1:].lower()
stream = path if hasattr(path, 'read') else open(path, 'rb')

View File

@ -27,6 +27,9 @@ PARALLEL_FUNCS = {
'gui_convert' :
('calibre.gui2.convert.gui_conversion', 'gui_convert', 'notification'),
'gui_catalog' :
('calibre.gui2.convert.gui_conversion', 'gui_catalog', 'notification'),
'move_library' :
('calibre.library.move', 'move_library', 'notification'),