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