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