1#!/usr/local/bin/python3.8 2 3 4__license__ = 'GPL v3' 5__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' 6__docformat__ = 'restructuredtext en' 7 8''' 9Logic for setting up conversion jobs 10''' 11 12import os 13 14from qt.core import QDialog, QProgressDialog, QTimer 15 16from calibre.ptempfile import PersistentTemporaryFile 17from calibre.gui2 import warning_dialog, question_dialog 18from calibre.gui2.convert.single import Config as SingleConfig 19from calibre.gui2.convert.bulk import BulkConfig 20from calibre.gui2.convert.metadata import create_opf_file, create_cover_file 21from calibre.customize.conversion import OptionRecommendation 22from calibre.utils.config import prefs 23from calibre.ebooks.conversion.config import ( 24 GuiRecommendations, load_defaults, load_specifics, save_specifics, 25 get_input_format_for_book, NoSupportedInputFormats) 26from calibre.gui2.convert import bulk_defaults_for_input_format 27from polyglot.builtins import as_bytes 28 29 30def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{ 31 out_format=None, show_no_format_warning=True): 32 changed = False 33 jobs = [] 34 bad = [] 35 36 total = len(book_ids) 37 if total == 0: 38 return None, None, None 39 40 for i, book_id in enumerate(book_ids): 41 temp_files = [] 42 43 try: 44 d = SingleConfig(parent, db, book_id, None, out_format) 45 46 if auto_conversion: 47 d.accept() 48 result = QDialog.DialogCode.Accepted 49 else: 50 result = d.exec() 51 52 if result == QDialog.DialogCode.Accepted: 53 # if not convert_existing(parent, db, [book_id], d.output_format): 54 # continue 55 56 mi = db.get_metadata(book_id, True) 57 in_file = PersistentTemporaryFile('.'+d.input_format) 58 with in_file: 59 input_fmt = db.original_fmt(book_id, d.input_format).lower() 60 same_fmt = input_fmt == d.output_format.lower() 61 db.copy_format_to(book_id, input_fmt, in_file, 62 index_is_id=True) 63 64 out_file = PersistentTemporaryFile('.' + d.output_format) 65 out_file.write(as_bytes(d.output_format)) 66 out_file.close() 67 temp_files = [in_file] 68 69 try: 70 dtitle = str(mi.title) 71 except: 72 dtitle = repr(mi.title) 73 desc = _('Convert book %(num)d of %(total)d (%(title)s)') % \ 74 {'num':i + 1, 'total':total, 'title':dtitle} 75 76 recs = d.recommendations 77 if d.opf_file is not None: 78 recs.append(('read_metadata_from_opf', d.opf_file.name, 79 OptionRecommendation.HIGH)) 80 temp_files.append(d.opf_file) 81 if d.cover_file is not None: 82 recs.append(('cover', d.cover_file.name, 83 OptionRecommendation.HIGH)) 84 temp_files.append(d.cover_file) 85 args = [in_file.name, out_file.name, recs] 86 temp_files.append(out_file) 87 func = 'gui_convert_override' 88 parts = [] 89 if not auto_conversion and d.manually_fine_tune_toc: 90 parts.append('manually_fine_tune_toc') 91 if same_fmt: 92 parts.append('same_fmt') 93 if parts: 94 func += ':%s'%(';'.join(parts)) 95 jobs.append((func, args, desc, d.output_format.upper(), book_id, temp_files)) 96 97 changed = True 98 d.break_cycles() 99 except NoSupportedInputFormats as nsif: 100 bad.append((book_id, nsif.available_formats)) 101 102 if bad and show_no_format_warning: 103 if len(bad) == 1 and not bad[0][1]: 104 title = db.title(bad[0][0], True) 105 warning_dialog(parent, _('Could not convert'), '<p>'+ _( 106 'Could not convert <b>%s</b> as it has no e-book files. If you ' 107 'think it should have files, but calibre is not finding ' 108 'them, that is most likely because you moved the book\'s ' 109 'files around outside of calibre. You will need to find those files ' 110 'and re-add them to calibre.')%title, show=True) 111 else: 112 res = [] 113 for id, available_formats in bad: 114 title = db.title(id, True) 115 if available_formats: 116 msg = _('No supported formats (Available formats: %s)')%( 117 ', '.join(available_formats)) 118 else: 119 msg = _('This book has no actual e-book files') 120 res.append('%s - %s'%(title, msg)) 121 122 msg = '%s' % '\n'.join(res) 123 warning_dialog(parent, _('Could not convert some books'), 124 ( 125 _('Could not convert the book because no supported source format was found') 126 if len(res) == 1 else 127 _('Could not convert {num} of {tot} books, because no supported source formats were found.') 128 ).format(num=len(res), tot=total), 129 msg).exec() 130 131 return jobs, changed, bad 132# }}} 133 134# Bulk convert {{{ 135 136 137def convert_bulk_ebook(parent, queue, db, book_ids, out_format=None, args=[]): 138 total = len(book_ids) 139 if total == 0: 140 return None, None, None 141 142 has_saved_settings = db.has_conversion_options(book_ids) 143 144 d = BulkConfig(parent, db, out_format, 145 has_saved_settings=has_saved_settings, book_ids=book_ids) 146 if d.exec() != QDialog.DialogCode.Accepted: 147 return None 148 149 output_format = d.output_format 150 user_recs = d.recommendations 151 152 book_ids = convert_existing(parent, db, book_ids, output_format) 153 use_saved_single_settings = d.opt_individual_saved_settings.isChecked() 154 return QueueBulk(parent, book_ids, output_format, queue, db, user_recs, 155 args, use_saved_single_settings=use_saved_single_settings) 156 157 158class QueueBulk(QProgressDialog): 159 160 def __init__(self, parent, book_ids, output_format, queue, db, user_recs, 161 args, use_saved_single_settings=True): 162 QProgressDialog.__init__(self, '', 163 None, 0, len(book_ids), parent) 164 self.setWindowTitle(_('Queueing books for bulk conversion')) 165 self.book_ids, self.output_format, self.queue, self.db, self.args, self.user_recs = \ 166 book_ids, output_format, queue, db, args, user_recs 167 self.parent = parent 168 self.use_saved_single_settings = use_saved_single_settings 169 self.i, self.bad, self.jobs, self.changed = 0, [], [], False 170 QTimer.singleShot(0, self.do_book) 171 self.exec() 172 173 def do_book(self): 174 if self.i >= len(self.book_ids): 175 return self.do_queue() 176 book_id = self.book_ids[self.i] 177 self.i += 1 178 179 temp_files = [] 180 181 try: 182 input_format = get_input_format_for_book(self.db, book_id, None)[0] 183 input_fmt = self.db.original_fmt(book_id, input_format).lower() 184 same_fmt = input_fmt == self.output_format.lower() 185 mi, opf_file = create_opf_file(self.db, book_id) 186 in_file = PersistentTemporaryFile('.'+input_format) 187 with in_file: 188 self.db.copy_format_to(book_id, input_fmt, in_file, 189 index_is_id=True) 190 191 out_file = PersistentTemporaryFile('.' + self.output_format) 192 out_file.write(as_bytes(self.output_format)) 193 out_file.close() 194 temp_files = [in_file] 195 196 combined_recs = GuiRecommendations() 197 default_recs = bulk_defaults_for_input_format(input_format) 198 for key in default_recs: 199 combined_recs[key] = default_recs[key] 200 if self.use_saved_single_settings: 201 specific_recs = load_specifics(self.db, book_id) 202 for key in specific_recs: 203 combined_recs[key] = specific_recs[key] 204 for item in self.user_recs: 205 combined_recs[item[0]] = item[1] 206 save_specifics(self.db, book_id, combined_recs) 207 lrecs = list(combined_recs.to_recommendations()) 208 from calibre.customize.ui import plugin_for_output_format 209 op = plugin_for_output_format(self.output_format) 210 if op and op.recommendations: 211 prec = {x[0] for x in op.recommendations} 212 for i, r in enumerate(list(lrecs)): 213 if r[0] in prec: 214 lrecs[i] = (r[0], r[1], OptionRecommendation.HIGH) 215 216 cover_file = create_cover_file(self.db, book_id) 217 218 if opf_file is not None: 219 lrecs.append(('read_metadata_from_opf', opf_file.name, 220 OptionRecommendation.HIGH)) 221 temp_files.append(opf_file) 222 if cover_file is not None: 223 lrecs.append(('cover', cover_file.name, 224 OptionRecommendation.HIGH)) 225 temp_files.append(cover_file) 226 227 for x in list(lrecs): 228 if x[0] == 'debug_pipeline': 229 lrecs.remove(x) 230 try: 231 dtitle = str(mi.title) 232 except: 233 dtitle = repr(mi.title) 234 if len(dtitle) > 50: 235 dtitle = dtitle[:50].rpartition(' ')[0]+'...' 236 self.setLabelText(_('Queueing ')+dtitle) 237 desc = _('Convert book %(num)d of %(tot)d (%(title)s)') % dict( 238 num=self.i, tot=len(self.book_ids), title=dtitle) 239 240 args = [in_file.name, out_file.name, lrecs] 241 temp_files.append(out_file) 242 func = 'gui_convert_override' 243 if same_fmt: 244 func += ':same_fmt' 245 self.jobs.append((func, args, desc, self.output_format.upper(), book_id, temp_files)) 246 247 self.changed = True 248 self.setValue(self.i) 249 except NoSupportedInputFormats: 250 self.bad.append(book_id) 251 QTimer.singleShot(0, self.do_book) 252 253 def do_queue(self): 254 self.hide() 255 if self.bad != []: 256 res = [] 257 for id in self.bad: 258 title = self.db.title(id, True) 259 res.append('%s'%title) 260 261 msg = '%s' % '\n'.join(res) 262 warning_dialog(self.parent, _('Could not convert some books'), 263 _('Could not convert %(num)d of %(tot)d books, because no suitable ' 264 'source format was found.') % dict(num=len(res), tot=len(self.book_ids)), 265 msg).exec() 266 self.parent = None 267 self.jobs.reverse() 268 self.queue(self.jobs, self.changed, self.bad, *self.args) 269 270# }}} 271 272 273def fetch_scheduled_recipe(arg): # {{{ 274 fmt = prefs['output_format'].lower() 275 # Never use AZW3 for periodicals... 276 if fmt == 'azw3': 277 fmt = 'mobi' 278 pt = PersistentTemporaryFile(suffix='_recipe_out.%s'%fmt.lower()) 279 pt.close() 280 recs = [] 281 ps = load_defaults('page_setup') 282 if 'output_profile' in ps: 283 recs.append(('output_profile', ps['output_profile'], 284 OptionRecommendation.HIGH)) 285 for edge in ('left', 'top', 'bottom', 'right'): 286 edge = 'margin_' + edge 287 if edge in ps: 288 recs.append((edge, ps[edge], OptionRecommendation.HIGH)) 289 290 lf = load_defaults('look_and_feel') 291 if lf.get('base_font_size', 0.0) != 0.0: 292 recs.append(('base_font_size', lf['base_font_size'], 293 OptionRecommendation.HIGH)) 294 recs.append(('keep_ligatures', lf.get('keep_ligatures', False), 295 OptionRecommendation.HIGH)) 296 297 lr = load_defaults('lrf_output') 298 if lr.get('header', False): 299 recs.append(('header', True, OptionRecommendation.HIGH)) 300 recs.append(('header_format', '%t', OptionRecommendation.HIGH)) 301 302 epub = load_defaults('epub_output') 303 if epub.get('epub_flatten', False): 304 recs.append(('epub_flatten', True, OptionRecommendation.HIGH)) 305 306 if fmt == 'pdf': 307 pdf = load_defaults('pdf_output') 308 from calibre.customize.ui import plugin_for_output_format 309 p = plugin_for_output_format('pdf') 310 for opt in p.options: 311 recs.append((opt.option.name, pdf.get(opt.option.name, opt.recommended_value), OptionRecommendation.HIGH)) 312 313 args = [arg['urn'], pt.name, recs] 314 if arg['username'] is not None: 315 recs.append(('username', arg['username'], OptionRecommendation.HIGH)) 316 if arg['password'] is not None: 317 recs.append(('password', arg['password'], OptionRecommendation.HIGH)) 318 319 return 'gui_convert_recipe', args, _('Fetch news from %s')%arg['title'], fmt.upper(), [pt] 320 321# }}} 322 323 324def generate_catalog(parent, dbspec, ids, device_manager, db): # {{{ 325 from calibre.gui2.dialogs.catalog import Catalog 326 327 # Build the Catalog dialog in gui2.dialogs.catalog 328 d = Catalog(parent, dbspec, ids, db) 329 330 if d.exec() != QDialog.DialogCode.Accepted: 331 return None 332 333 # Create the output file 334 out = PersistentTemporaryFile(suffix='_catalog_out.'+d.catalog_format.lower()) 335 336 # Profile the connected device 337 # Parallel initialization in calibre.db.cli.cmd_catalog 338 connected_device = { 339 'is_device_connected': device_manager.is_device_present, 340 'kind': device_manager.connected_device_kind, 341 'name': None, 342 'save_template': None, 343 'serial': None, 344 'storage': None 345 } 346 347 if device_manager.is_device_present: 348 device = device_manager.device 349 connected_device['name'] = device.get_gui_name() 350 try: 351 storage = [] 352 if device._main_prefix: 353 storage.append(os.path.join(device._main_prefix, device.EBOOK_DIR_MAIN)) 354 if device._card_a_prefix: 355 storage.append(os.path.join(device._card_a_prefix, device.EBOOK_DIR_CARD_A)) 356 if device._card_b_prefix: 357 storage.append(os.path.join(device._card_b_prefix, device.EBOOK_DIR_CARD_B)) 358 connected_device['storage'] = storage 359 connected_device['serial'] = device.detected_device.serial if \ 360 hasattr(device.detected_device,'serial') else None 361 connected_device['save_template'] = device.save_template() 362 except: 363 pass 364 365 # These args are passed inline to gui2.convert.gui_conversion:gui_catalog 366 args = [ 367 d.catalog_format, 368 d.catalog_title, 369 dbspec, 370 ids, 371 out.name, 372 d.catalog_sync, 373 d.fmt_options, 374 connected_device 375 ] 376 out.close() 377 378 # This returns to gui2.actions.catalog:generate_catalog() 379 # Which then calls gui2.convert.gui_conversion:gui_catalog() with the args inline 380 return 'gui_catalog', args, _('Generate catalog'), out.name, d.catalog_sync, \ 381 d.catalog_title 382# }}} 383 384 385def convert_existing(parent, db, book_ids, output_format): # {{{ 386 already_converted_ids = [] 387 already_converted_titles = [] 388 for book_id in book_ids: 389 if db.has_format(book_id, output_format, index_is_id=True): 390 already_converted_ids.append(book_id) 391 already_converted_titles.append(db.get_metadata(book_id, True).title) 392 393 if already_converted_ids: 394 if not question_dialog(parent, _('Convert existing'), 395 _('The following books have already been converted to the %s format. ' 396 'Do you wish to reconvert them?') % output_format.upper(), 397 det_msg='\n'.join(already_converted_titles), skip_dialog_name='confirm_bulk_reconvert'): 398 book_ids = [x for x in book_ids if x not in already_converted_ids] 399 400 return book_ids 401# }}} 402