1#!/usr/local/bin/python3.8 2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai 3 4 5__license__ = 'GPL v3' 6__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' 7__docformat__ = 'restructuredtext en' 8 9import os 10from functools import partial 11 12from qt.core import QModelIndex, QTimer 13 14from calibre.gui2 import error_dialog, Dispatcher, gprefs 15from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook 16from calibre.utils.config import prefs, tweaks 17from calibre.gui2.actions import InterfaceAction 18from calibre.customize.ui import plugin_for_input_format 19 20 21class ConvertAction(InterfaceAction): 22 23 name = 'Convert Books' 24 action_spec = (_('Convert books'), 'convert.png', _('Convert books between different e-book formats'), _('C')) 25 dont_add_to = frozenset(('context-menu-device',)) 26 action_type = 'current' 27 action_add_menu = True 28 29 accepts_drops = True 30 31 def accept_enter_event(self, event, mime_data): 32 if mime_data.hasFormat("application/calibre+from_library"): 33 return True 34 return False 35 36 def accept_drag_move_event(self, event, mime_data): 37 if mime_data.hasFormat("application/calibre+from_library"): 38 return True 39 return False 40 41 def drop_event(self, event, mime_data): 42 mime = 'application/calibre+from_library' 43 if mime_data.hasFormat(mime): 44 self.dropped_ids = tuple(map(int, mime_data.data(mime).data().split())) 45 QTimer.singleShot(1, self.do_drop) 46 return True 47 return False 48 49 def do_drop(self): 50 book_ids = self.dropped_ids 51 del self.dropped_ids 52 self.do_convert(book_ids) 53 54 def genesis(self): 55 m = self.convert_menu = self.qaction.menu() 56 cm = partial(self.create_menu_action, self.convert_menu) 57 cm('convert-individual', _('Convert individually'), 58 icon=self.qaction.icon(), triggered=partial(self.convert_ebook, 59 False, bulk=False)) 60 cm('convert-bulk', _('Bulk convert'), 61 triggered=partial(self.convert_ebook, False, bulk=True)) 62 m.addSeparator() 63 cm('create-catalog', 64 _('Create a catalog of the books in your calibre library'), 65 icon='catalog.png', shortcut=False, 66 triggered=self.gui.iactions['Generate Catalog'].generate_catalog) 67 self.qaction.triggered.connect(self.convert_ebook) 68 self.conversion_jobs = {} 69 70 def location_selected(self, loc): 71 enabled = loc == 'library' 72 self.qaction.setEnabled(enabled) 73 self.menuless_qaction.setEnabled(enabled) 74 for action in list(self.convert_menu.actions()): 75 action.setEnabled(enabled) 76 77 def auto_convert(self, book_ids, on_card, format): 78 previous = self.gui.library_view.currentIndex() 79 rows = [x.row() for x in 80 self.gui.library_view.selectionModel().selectedRows()] 81 jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format) 82 if jobs == []: 83 return 84 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 85 self.book_auto_converted, extra_job_args=[on_card]) 86 87 def auto_convert_auto_add(self, book_ids): 88 previous = self.gui.library_view.currentIndex() 89 db = self.gui.current_db 90 needed = set() 91 of = prefs['output_format'].lower() 92 for book_id in book_ids: 93 fmts = db.formats(book_id, index_is_id=True) 94 fmts = {x.lower() for x in fmts.split(',')} if fmts else set() 95 if gprefs['auto_convert_same_fmt'] or of not in fmts: 96 needed.add(book_id) 97 if needed: 98 jobs, changed, bad = convert_single_ebook(self.gui, 99 self.gui.library_view.model().db, needed, True, of, 100 show_no_format_warning=False) 101 if not jobs: 102 return 103 self.queue_convert_jobs(jobs, changed, bad, list(needed), previous, 104 self.book_converted, rows_are_ids=True) 105 106 def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format, subject): 107 previous = self.gui.library_view.currentIndex() 108 rows = [x.row() for x in 109 self.gui.library_view.selectionModel().selectedRows()] 110 jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format) 111 if jobs == []: 112 return 113 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 114 self.book_auto_converted_mail, 115 extra_job_args=[delete_from_library, to, fmts, subject]) 116 117 def auto_convert_multiple_mail(self, book_ids, data, ofmt, delete_from_library): 118 previous = self.gui.library_view.currentIndex() 119 rows = [x.row() for x in self.gui.library_view.selectionModel().selectedRows()] 120 jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, ofmt) 121 if jobs == []: 122 return 123 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 124 self.book_auto_converted_multiple_mail, 125 extra_job_args=[delete_from_library, data]) 126 127 def auto_convert_news(self, book_ids, format): 128 previous = self.gui.library_view.currentIndex() 129 rows = [x.row() for x in 130 self.gui.library_view.selectionModel().selectedRows()] 131 jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format) 132 if jobs == []: 133 return 134 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 135 self.book_auto_converted_news) 136 137 def auto_convert_catalogs(self, book_ids, format): 138 previous = self.gui.library_view.currentIndex() 139 rows = [x.row() for x in 140 self.gui.library_view.selectionModel().selectedRows()] 141 jobs, changed, bad = convert_single_ebook(self.gui, self.gui.library_view.model().db, book_ids, True, format) 142 if jobs == []: 143 return 144 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 145 self.book_auto_converted_catalogs) 146 147 def get_books_for_conversion(self): 148 rows = [r.row() for r in 149 self.gui.library_view.selectionModel().selectedRows()] 150 if not rows or len(rows) == 0: 151 d = error_dialog(self.gui, _('Cannot convert'), 152 _('No books selected')) 153 d.exec() 154 return None 155 return [self.gui.library_view.model().db.id(r) for r in rows] 156 157 def convert_ebook(self, checked, bulk=None): 158 book_ids = self.get_books_for_conversion() 159 if book_ids is None: 160 return 161 self.do_convert(book_ids, bulk=bulk) 162 163 def convert_ebooks_to_format(self, book_ids, to_fmt): 164 from calibre.customize.ui import available_output_formats 165 to_fmt = to_fmt.upper() 166 if to_fmt.lower() not in available_output_formats(): 167 return error_dialog(self.gui, _('Cannot convert'), _( 168 'Conversion to the {} format is not supported').format(to_fmt), show=True) 169 self.do_convert(book_ids, output_fmt=to_fmt, auto_conversion=True) 170 171 def do_convert(self, book_ids, bulk=None, auto_conversion=False, output_fmt=None): 172 previous = self.gui.library_view.currentIndex() 173 rows = [x.row() for x in 174 self.gui.library_view.selectionModel().selectedRows()] 175 num = 0 176 if bulk or (bulk is None and len(book_ids) > 1): 177 self.__bulk_queue = convert_bulk_ebook(self.gui, self.queue_convert_jobs, 178 self.gui.library_view.model().db, book_ids, 179 out_format=output_fmt or prefs['output_format'], args=(rows, previous, 180 self.book_converted)) 181 if self.__bulk_queue is None: 182 return 183 num = len(self.__bulk_queue.book_ids) 184 else: 185 jobs, changed, bad = convert_single_ebook(self.gui, 186 self.gui.library_view.model().db, book_ids, out_format=output_fmt or prefs['output_format'], auto_conversion=auto_conversion) 187 self.queue_convert_jobs(jobs, changed, bad, rows, previous, 188 self.book_converted) 189 num = len(jobs) 190 191 if num > 0: 192 self.gui.jobs_pointer.start() 193 self.gui.status_bar.show_message(ngettext( 194 'Starting conversion of the book', 'Starting conversion of {} books', num).format(num), 2000) 195 196 def queue_convert_jobs(self, jobs, changed, bad, rows, previous, 197 converted_func, extra_job_args=[], rows_are_ids=False): 198 for func, args, desc, fmt, id, temp_files in jobs: 199 func, _, parts = func.partition(':') 200 parts = {x for x in parts.split(';')} 201 input_file = args[0] 202 input_fmt = os.path.splitext(input_file)[1] 203 core_usage = 1 204 if input_fmt: 205 input_fmt = input_fmt[1:] 206 plugin = plugin_for_input_format(input_fmt) 207 if plugin is not None: 208 core_usage = plugin.core_usage 209 210 if id not in bad: 211 job = self.gui.job_manager.run_job(Dispatcher(converted_func), 212 func, args=args, description=desc, 213 core_usage=core_usage) 214 job.conversion_of_same_fmt = 'same_fmt' in parts 215 job.manually_fine_tune_toc = 'manually_fine_tune_toc' in parts 216 args = [temp_files, fmt, id]+extra_job_args 217 self.conversion_jobs[job] = tuple(args) 218 219 if changed: 220 m = self.gui.library_view.model() 221 if rows_are_ids: 222 m.refresh_ids(rows) 223 else: 224 m.refresh_rows(rows) 225 current = self.gui.library_view.currentIndex() 226 self.gui.library_view.model().current_changed(current, previous) 227 228 def book_auto_converted(self, job): 229 temp_files, fmt, book_id, on_card = self.conversion_jobs[job] 230 self.book_converted(job) 231 self.gui.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) 232 233 def book_auto_converted_mail(self, job): 234 temp_files, fmt, book_id, delete_from_library, to, fmts, subject = self.conversion_jobs[job] 235 self.book_converted(job) 236 self.gui.send_by_mail(to, fmts, delete_from_library, subject=subject, 237 specific_format=fmt, send_ids=[book_id], do_auto_convert=False) 238 239 def book_auto_converted_multiple_mail(self, job): 240 temp_files, fmt, book_id, delete_from_library, data = self.conversion_jobs[job] 241 self.book_converted(job) 242 for to, subject in data: 243 self.gui.send_by_mail(to, (fmt,), delete_from_library, subject=subject, 244 specific_format=fmt, send_ids=[book_id], do_auto_convert=False) 245 246 def book_auto_converted_news(self, job): 247 temp_files, fmt, book_id = self.conversion_jobs[job] 248 self.book_converted(job) 249 self.gui.sync_news(send_ids=[book_id], do_auto_convert=False) 250 251 def book_auto_converted_catalogs(self, job): 252 temp_files, fmt, book_id = self.conversion_jobs[job] 253 self.book_converted(job) 254 self.gui.sync_catalogs(send_ids=[book_id], do_auto_convert=False) 255 256 def book_converted(self, job): 257 temp_files, fmt, book_id = self.conversion_jobs.pop(job)[:3] 258 try: 259 if job.failed: 260 self.gui.job_exception(job) 261 return 262 db = self.gui.current_db 263 if not db.new_api.has_id(book_id): 264 return error_dialog(self.gui, _('Book deleted'), _( 265 'The book you were trying to convert has been deleted from the calibre library.'), show=True) 266 same_fmt = getattr(job, 'conversion_of_same_fmt', False) 267 manually_fine_tune_toc = getattr(job, 'manually_fine_tune_toc', False) 268 fmtf = temp_files[-1].name 269 if os.stat(fmtf).st_size < 1: 270 raise Exception(_('Empty output file, ' 271 'probably the conversion process crashed')) 272 273 if same_fmt and tweaks['save_original_format']: 274 db.save_original_format(book_id, fmt, notify=False) 275 276 with open(temp_files[-1].name, 'rb') as data: 277 db.add_format(book_id, fmt, data, index_is_id=True) 278 self.gui.book_converted.emit(book_id, fmt) 279 self.gui.status_bar.show_message(job.description + ' ' + 280 _('completed'), 2000) 281 finally: 282 for f in temp_files: 283 try: 284 if os.path.exists(f.name): 285 os.remove(f.name) 286 except: 287 pass 288 self.gui.tags_view.recount() 289 if self.gui.current_view() is self.gui.library_view: 290 lv = self.gui.library_view 291 lv.model().refresh_ids((book_id,)) 292 current = lv.currentIndex() 293 if current.isValid(): 294 lv.model().current_changed(current, QModelIndex()) 295 if manually_fine_tune_toc: 296 self.gui.iactions['Edit ToC'].do_one(book_id, fmt.upper()) 297