mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-06-23 15:30:45 -04:00
Framework for GUI catalog generation
This commit is contained in:
parent
392b1bf2d1
commit
e010921c55
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
54
src/calibre/gui2/dialogs/catalog.py
Normal file
54
src/calibre/gui2/dialogs/catalog.py
Normal 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)
|
146
src/calibre/gui2/dialogs/catalog.ui
Normal file
146
src/calibre/gui2/dialogs/catalog.ui
Normal 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 &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 &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>&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>
|
@ -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())
|
||||
|
||||
|
@ -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 = []
|
||||
|
@ -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:
|
||||
|
@ -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')
|
||||
|
@ -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'),
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user