mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-10-31 10:37:00 -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