diff --git a/src/calibre/gui2/convert/gui_conversion.py b/src/calibre/gui2/convert/gui_conversion.py index 54b3e17de4..32cd883727 100644 --- a/src/calibre/gui2/convert/gui_conversion.py +++ b/src/calibre/gui2/convert/gui_conversion.py @@ -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 + + + + diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 7c2ff498b8..72229f6c19 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -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: diff --git a/src/calibre/gui2/dialogs/catalog.py b/src/calibre/gui2/dialogs/catalog.py new file mode 100644 index 0000000000..29b6ef972d --- /dev/null +++ b/src/calibre/gui2/dialogs/catalog.py @@ -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 ' +__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) diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui new file mode 100644 index 0000000000..aa47f3c0c3 --- /dev/null +++ b/src/calibre/gui2/dialogs/catalog.ui @@ -0,0 +1,146 @@ + + + Dialog + + + + 0 + 0 + 628 + 503 + + + + 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 + + + + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/calibre/gui2/library.py b/src/calibre/gui2/library.py index 488d535c73..fd4f8999b4 100644 --- a/src/calibre/gui2/library.py +++ b/src/calibre/gui2/library.py @@ -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()) diff --git a/src/calibre/gui2/tools.py b/src/calibre/gui2/tools.py index 3aa91bff87..2bb891d36b 100644 --- a/src/calibre/gui2/tools.py +++ b/src/calibre/gui2/tools.py @@ -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 = [] diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 4deba1f87d..f85a19da24 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -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: diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 8dd9157def..84638410c7 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -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') diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 3d04298a65..73233840fe 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -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'),