コード例 #1
0
class Polish(QDialog):  # {{{
    def __init__(self, db, book_id_map, parent=None):
        from calibre.ebooks.oeb.polish.main import HELP
        QDialog.__init__(self, parent)
        self.db, self.book_id_map = weakref.ref(db), book_id_map
        self.setWindowIcon(QIcon(I('polish.png')))
        title = _('Polish book')
        if len(book_id_map) > 1:
            title = _('Polish %d books') % len(book_id_map)
        self.setWindowTitle(title)

        self.help_text = {
            'polish':
            _('<h3>About Polishing books</h3>%s') % HELP['about'].format(
                _('''<p>If you have both EPUB and ORIGINAL_EPUB in your book,
                  then polishing will run on ORIGINAL_EPUB (the same for other
                  ORIGINAL_* formats).  So if you
                  want Polishing to not run on the ORIGINAL_* format, delete the
                  ORIGINAL_* format before running it.</p>''')),
            'embed':
            _('<h3>Embed referenced fonts</h3>%s') % HELP['embed'],
            'subset':
            _('<h3>Subsetting fonts</h3>%s') % HELP['subset'],
            'smarten_punctuation':
            _('<h3>Smarten punctuation</h3>%s') % HELP['smarten_punctuation'],
            'metadata':
            _('<h3>Updating metadata</h3>'
              '<p>This will update all metadata <i>except</i> the cover in the'
              ' e-book files to match the current metadata in the'
              ' calibre library.</p>'
              ' <p>Note that most e-book'
              ' formats are not capable of supporting all the'
              ' metadata in calibre.</p><p>There is a separate option to'
              ' update the cover.</p>'),
            'do_cover':
            _('<h3>Update cover</h3><p>Update the covers in the e-book files to match the'
              ' current cover in the calibre library.</p>'
              '<p>If the e-book file does not have'
              ' an identifiable cover, a new cover is inserted.</p>'),
            'jacket':
            _('<h3>Book jacket</h3>%s') % HELP['jacket'],
            'remove_jacket':
            _('<h3>Remove book jacket</h3>%s') % HELP['remove_jacket'],
            'remove_unused_css':
            _('<h3>Remove unused CSS rules</h3>%s') %
            HELP['remove_unused_css'],
            'compress_images':
            _('<h3>Losslessly compress images</h3>%s') %
            HELP['compress_images'],
            'add_soft_hyphens':
            _('<h3>Add soft-hyphens</h3>%s') % HELP['add_soft_hyphens'],
            'remove_soft_hyphens':
            _('<h3>Remove soft-hyphens</h3>%s') % HELP['remove_soft_hyphens'],
            'upgrade_book':
            _('<h3>Upgrade book internals</h3>%s') % HELP['upgrade_book'],
        }

        self.l = l = QGridLayout()
        self.setLayout(l)

        self.la = la = QLabel('<b>' + _('Select actions to perform:'))
        l.addWidget(la, 0, 0, 1, 2)

        count = 0
        self.all_actions = OrderedDict([
            ('embed', _('&Embed all referenced fonts')),
            ('subset', _('&Subset all embedded fonts')),
            ('smarten_punctuation', _('Smarten &punctuation')),
            ('metadata', _('Update &metadata in the book files')),
            ('do_cover', _('Update the &cover in the book files')),
            ('jacket', _('Add/replace metadata as a "book &jacket" page')),
            ('remove_jacket', _('&Remove a previously inserted book jacket')),
            ('remove_unused_css', _('Remove &unused CSS rules from the book')),
            ('compress_images', _('Losslessly &compress images')),
            ('add_soft_hyphens', _('Add s&oft hyphens')),
            ('remove_soft_hyphens', _('Remove so&ft hyphens')),
            ('upgrade_book', _('&Upgrade book internals')),
        ])
        prefs = gprefs.get('polishing_settings', {})
        for name, text in iteritems(self.all_actions):
            count += 1
            x = QCheckBox(text, self)
            x.setChecked(prefs.get(name, False))
            x.setObjectName(name)
            connect_lambda(
                x.stateChanged, self, lambda self, state: self.option_toggled(
                    self.sender().objectName(), state))
            l.addWidget(x, count, 0, 1, 1)
            setattr(self, 'opt_' + name, x)
            la = QLabel(' <a href="#%s">%s</a>' % (name, _('About')))
            setattr(self, 'label_' + name, x)
            la.linkActivated.connect(self.help_link_activated)
            l.addWidget(la, count, 1, 1, 1)

        count += 1
        l.addItem(QSpacerItem(10, 10, vPolicy=QSizePolicy.Policy.Expanding),
                  count, 1, 1, 2)

        la = self.help_label = QLabel('')
        self.help_link_activated('#polish')
        la.setWordWrap(True)
        la.setTextFormat(Qt.TextFormat.RichText)
        la.setFrameShape(QFrame.Shape.StyledPanel)
        la.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop)
        la.setLineWidth(2)
        la.setStyleSheet('QLabel { margin-left: 75px }')
        l.addWidget(la, 0, 2, count + 1, 1)
        l.setColumnStretch(2, 1)

        self.show_reports = sr = QCheckBox(_('Show &report'), self)
        sr.setChecked(gprefs.get('polish_show_reports', True))
        sr.setToolTip(
            textwrap.fill(
                _('Show a report of all the actions performed'
                  ' after polishing is completed')))
        l.addWidget(sr, count + 1, 0, 1, 1)
        self.bb = bb = QDialogButtonBox(
            QDialogButtonBox.StandardButton.Ok
            | QDialogButtonBox.StandardButton.Cancel)
        bb.accepted.connect(self.accept)
        bb.rejected.connect(self.reject)
        self.save_button = sb = bb.addButton(
            _('&Save settings'), QDialogButtonBox.ButtonRole.ActionRole)
        sb.clicked.connect(self.save_settings)
        self.load_button = lb = bb.addButton(
            _('&Load settings'), QDialogButtonBox.ButtonRole.ActionRole)
        self.load_menu = QMenu(lb)
        lb.setMenu(self.load_menu)
        self.all_button = b = bb.addButton(
            _('Select &all'), QDialogButtonBox.ButtonRole.ActionRole)
        connect_lambda(b.clicked, self, lambda self: self.select_all(True))
        self.none_button = b = bb.addButton(
            _('Select &none'), QDialogButtonBox.ButtonRole.ActionRole)
        connect_lambda(b.clicked, self, lambda self: self.select_all(False))
        l.addWidget(bb, count + 1, 1, 1, -1)
        self.setup_load_button()

        self.resize(QSize(950, 600))

    def select_all(self, enable):
        for action in self.all_actions:
            x = getattr(self, 'opt_' + action)
            x.blockSignals(True)
            x.setChecked(enable)
            x.blockSignals(False)

    def save_settings(self):
        if not self.something_selected:
            return error_dialog(
                self,
                _('No actions selected'),
                _('You must select at least one action before saving'),
                show=True)
        name, ok = QInputDialog.getText(self, _('Choose name'),
                                        _('Choose a name for these settings'))
        if ok:
            name = str(name).strip()
            if name:
                settings = {
                    ac: getattr(self, 'opt_' + ac).isChecked()
                    for ac in self.all_actions
                }
                saved = gprefs.get('polish_settings', {})
                saved[name] = settings
                gprefs.set('polish_settings', saved)
                self.setup_load_button()

    def setup_load_button(self):
        saved = gprefs.get('polish_settings', {})
        m = self.load_menu
        m.clear()
        self.__actions = []
        a = self.__actions.append
        for name in sorted(saved):
            a(m.addAction(name, partial(self.load_settings, name)))
        m.addSeparator()
        a(m.addAction(_('Remove saved settings'), self.clear_settings))
        self.load_button.setEnabled(bool(saved))

    def clear_settings(self):
        gprefs.set('polish_settings', {})
        self.setup_load_button()

    def load_settings(self, name):
        saved = gprefs.get('polish_settings', {}).get(name, {})
        for action in self.all_actions:
            checked = saved.get(action, False)
            x = getattr(self, 'opt_' + action)
            x.blockSignals(True)
            x.setChecked(checked)
            x.blockSignals(False)

    def option_toggled(self, name, state):
        if state == Qt.CheckState.Checked:
            self.help_label.setText(self.help_text[name])

    def help_link_activated(self, link):
        link = str(link)[1:]
        self.help_label.setText(self.help_text[link])

    @property
    def something_selected(self):
        for action in self.all_actions:
            if getattr(self, 'opt_' + action).isChecked():
                return True
        return False

    def accept(self):
        self.actions = ac = {}
        saved_prefs = {}
        gprefs['polish_show_reports'] = bool(self.show_reports.isChecked())
        something = False
        for action in self.all_actions:
            ac[action] = saved_prefs[action] = bool(
                getattr(self, 'opt_' + action).isChecked())
            if ac[action]:
                something = True
        if ac['jacket'] and not ac['metadata']:
            if not question_dialog(
                    self, _('Must update metadata'),
                    _('You have selected the option to add metadata as '
                      'a "book jacket". For this option to work, you '
                      'must also select the option to update metadata in'
                      ' the book files. Do you want to select it?')):
                return
            ac['metadata'] = saved_prefs['metadata'] = True
            self.opt_metadata.setChecked(True)
        if ac['jacket'] and ac['remove_jacket']:
            if not question_dialog(
                    self, _('Add or remove jacket?'),
                    _('You have chosen to both add and remove the metadata jacket.'
                      ' This will result in the final book having no jacket. Is this'
                      ' what you want?')):
                return
        if not something:
            return error_dialog(
                self,
                _('No actions selected'),
                _('You must select at least one action, or click Cancel.'),
                show=True)
        gprefs['polishing_settings'] = saved_prefs
        self.queue_files()
        return super().accept()

    def queue_files(self):
        self.tdir = PersistentTemporaryDirectory('_queue_polish')
        self.jobs = []
        if len(self.book_id_map) <= 5:
            for i, (book_id,
                    formats) in enumerate(iteritems(self.book_id_map)):
                self.do_book(i + 1, book_id, formats)
        else:
            self.queue = [(i + 1, id_)
                          for i, id_ in enumerate(self.book_id_map)]
            self.pd = ProgressDialog(_('Queueing books for polishing'),
                                     max=len(self.queue),
                                     parent=self)
            QTimer.singleShot(0, self.do_one)
            self.pd.exec()

    def do_one(self):
        if not self.queue:
            self.pd.accept()
            return
        if self.pd.canceled:
            self.jobs = []
            self.pd.reject()
            return
        num, book_id = self.queue.pop(0)
        try:
            self.do_book(num, book_id, self.book_id_map[book_id])
        except:
            self.pd.reject()
            raise
        else:
            self.pd.set_value(num)
            QTimer.singleShot(0, self.do_one)

    def do_book(self, num, book_id, formats):
        base = os.path.join(self.tdir, str(book_id))
        os.mkdir(base)
        db = self.db()
        opf = os.path.join(base, 'metadata.opf')
        with open(opf, 'wb') as opf_file:
            mi = create_opf_file(db, book_id, opf_file=opf_file)[0]
        data = {'opf': opf, 'files': []}
        for action in self.actions:
            data[action] = bool(getattr(self, 'opt_' + action).isChecked())
        cover = os.path.join(base, 'cover.jpg')
        if db.copy_cover_to(book_id, cover, index_is_id=True):
            data['cover'] = cover
        is_orig = {}
        for fmt in formats:
            ext = fmt.replace('ORIGINAL_', '').lower()
            is_orig[ext.upper()] = 'ORIGINAL_' in fmt
            with open(os.path.join(base, '%s.%s' % (book_id, ext)), 'wb') as f:
                db.copy_format_to(book_id, fmt, f, index_is_id=True)
                data['files'].append(f.name)

        nums = num
        if hasattr(self, 'pd'):
            nums = self.pd.max - num

        desc = ngettext(
            _('Polish %s') % mi.title,
            _('Polish book %(nums)s of %(tot)s (%(title)s)') %
            dict(nums=nums, tot=len(self.book_id_map), title=mi.title),
            len(self.book_id_map))
        if hasattr(self, 'pd'):
            self.pd.set_msg(
                _('Queueing book %(nums)s of %(tot)s (%(title)s)') %
                dict(nums=num, tot=len(self.book_id_map), title=mi.title))

        self.jobs.append((desc, data, book_id, base, is_orig))
コード例 #2
0
class AddAction(InterfaceAction):

    name = 'Add Books'
    action_spec = (
        _('Add books'), 'add_book.png',
        _('Add books to the calibre library/device from files on your computer'
          ), _('A'))
    action_type = 'current'
    action_add_menu = True
    action_menu_clone_qaction = _('Add books from a single folder')

    def genesis(self):
        self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
        self.add_menu = self.qaction.menu()
        ma = partial(self.create_menu_action, self.add_menu)
        ma('recursive-add',
           _('Add from folders and sub-folders')).triggered.connect(
               self.add_recursive_question)
        ma('archive-add-book',
           _('Add multiple books from archive (ZIP/RAR)')).triggered.connect(
               self.add_from_archive)
        self.add_menu.addSeparator()
        ma('add-empty',
           _('Add empty book (Book entry with no formats)'),
           shortcut='Shift+Ctrl+E').triggered.connect(self.add_empty)
        ma('add-isbn',
           _('Add from ISBN')).triggered.connect(self.add_from_isbn)
        self.add_menu.addSeparator()
        ma('add-formats',
           _('Add files to selected book records'),
           triggered=self.add_formats,
           shortcut='Shift+A')
        ma('add-formats-clipboard',
           _('Add files to selected book records from clipboard'),
           triggered=self.add_formats_from_clipboard,
           shortcut='Shift+Alt+A')
        ma('add-empty-format-to-books',
           _('Add an empty file to selected book records')).triggered.connect(
               self.add_empty_format_choose)
        self.add_menu.addSeparator()
        ma('add-config',
           _('Control the adding of books'),
           triggered=self.add_config)
        self.qaction.triggered.connect(self.add_books)

    def location_selected(self, loc):
        enabled = loc == 'library'
        for action in list(self.add_menu.actions())[1:]:
            action.setEnabled(enabled)

    def add_config(self):
        self.gui.iactions['Preferences'].do_config(
            initial_plugin=('Import/Export', 'Adding'),
            close_after_initial=True)

    def _check_add_formats_ok(self):
        if self.gui.current_view() is not self.gui.library_view:
            return []
        view = self.gui.library_view
        rows = view.selectionModel().selectedRows()
        if not rows:
            error_dialog(self.gui,
                         _('No books selected'),
                         _('Cannot add files as no books are selected'),
                         show=True)
        ids = [view.model().id(r) for r in rows]
        return ids

    def add_formats_from_clipboard(self):
        ids = self._check_add_formats_ok()
        if not ids:
            return
        md = QApplication.instance().clipboard().mimeData()
        files_to_add = []
        images = []
        if md.hasUrls():
            for url in md.urls():
                if url.isLocalFile():
                    path = url.toLocalFile()
                    if os.access(path, os.R_OK):
                        mt = guess_type(path)[0]
                        if mt and mt.startswith('image/'):
                            images.append(path)
                        else:
                            files_to_add.append(path)
        if not files_to_add and not images:
            return error_dialog(
                self.gui,
                _('No files in clipboard'),
                _('No files have been copied to the clipboard'),
                show=True)
        if files_to_add:
            self._add_formats(files_to_add, ids)
        if images:
            if len(ids) > 1 and not question_dialog(
                    self.gui, _('Are you sure?'),
                    _('Are you sure you want to set the same'
                      ' cover for all %d books?') % len(ids)):
                return
            with lopen(images[0], 'rb') as f:
                cdata = f.read()
            self.gui.current_db.new_api.set_cover(
                {book_id: cdata
                 for book_id in ids})
            self.gui.refresh_cover_browser()
            m = self.gui.library_view.model()
            current = self.gui.library_view.currentIndex()
            m.current_changed(current, current)

    def add_formats(self, *args):
        ids = self._check_add_formats_ok()
        if not ids:
            return
        books = choose_files_and_remember_all_files(self.gui,
                                                    'add formats dialog dir',
                                                    _('Select book files'),
                                                    filters=get_filters())
        if books:
            self._add_formats(books, ids)

    def _add_formats(self, paths, ids):
        if len(ids) > 1 and not question_dialog(
                self.gui, _('Are you sure?'),
                _('Are you sure you want to add the same'
                  ' files to all %d books? If the format'
                  ' already exists for a book, it will be replaced.') %
                len(ids)):
            return
        paths = list(map(make_long_path_useable, paths))

        db = self.gui.current_db
        if len(ids) == 1:
            formats = db.formats(ids[0], index_is_id=True)
            if formats:
                formats = {x.upper() for x in formats.split(',')}
                nformats = {f.rpartition('.')[-1].upper() for f in paths}
                override = formats.intersection(nformats)
                if override:
                    title = db.title(ids[0], index_is_id=True)
                    msg = ngettext(
                        'The {0} format will be replaced in the book {1}. Are you sure?',
                        'The {0} formats will be replaced in the book {1}. Are you sure?',
                        len(override)).format(', '.join(override), title)
                    if not confirm(msg,
                                   'confirm_format_override_on_add',
                                   title=_('Are you sure?'),
                                   parent=self.gui):
                        return

        fmt_map = {
            os.path.splitext(fpath)[1][1:].upper(): fpath
            for fpath in paths
        }

        for id_ in ids:
            for fmt, fpath in iteritems(fmt_map):
                if fmt:
                    db.add_format_with_hooks(id_,
                                             fmt,
                                             fpath,
                                             index_is_id=True,
                                             notify=True)
        current_idx = self.gui.library_view.currentIndex()
        if current_idx.isValid():
            self.gui.library_view.model().current_changed(
                current_idx, current_idx)

    def is_ok_to_add_empty_formats(self):
        if self.gui.stack.currentIndex() != 0:
            return
        view = self.gui.library_view
        rows = view.selectionModel().selectedRows()
        if not rows:
            return error_dialog(self.gui,
                                _('No books selected'),
                                _('Cannot add files as no books are selected'),
                                show=True)

        ids = [view.model().id(r) for r in rows]

        if len(ids) > 1 and not question_dialog(
                self.gui, _('Are you sure?'),
                _('Are you sure you want to add the same'
                  ' empty file to all %d books? If the format'
                  ' already exists for a book, it will be replaced.') %
                len(ids)):
            return
        return True

    def add_empty_format_choose(self):
        if not self.is_ok_to_add_empty_formats():
            return
        from calibre.ebooks.oeb.polish.create import valid_empty_formats
        from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
        d = ChooseFormatDialog(self.gui, _('Choose format of empty file'),
                               sorted(valid_empty_formats))
        if d.exec() != QDialog.DialogCode.Accepted or not d.format():
            return
        self._add_empty_format(d.format())

    def add_empty_format(self, format_):
        if not self.is_ok_to_add_empty_formats():
            return
        self._add_empty_format(format_)

    def _add_empty_format(self, format_):
        view = self.gui.library_view
        rows = view.selectionModel().selectedRows()
        ids = [view.model().id(r) for r in rows]
        db = self.gui.library_view.model().db
        if len(ids) == 1:
            formats = db.formats(ids[0], index_is_id=True)
            if formats:
                formats = {x.lower() for x in formats.split(',')}
                if format_ in formats:
                    title = db.title(ids[0], index_is_id=True)
                    msg = _(
                        'The {0} format will be replaced in the book: {1}. Are you sure?'
                    ).format(format_, title)
                    if not confirm(msg,
                                   'confirm_format_override_on_add',
                                   title=_('Are you sure?'),
                                   parent=self.gui):
                        return

        for id_ in ids:
            self.add_empty_format_to_book(id_, format_)

        current_idx = self.gui.library_view.currentIndex()
        if current_idx.isValid():
            view.model().current_changed(current_idx, current_idx)

    def add_empty_format_to_book(self, book_id, fmt):
        from calibre.ebooks.oeb.polish.create import create_book
        db = self.gui.current_db
        pt = PersistentTemporaryFile(suffix='.' + fmt.lower())
        pt.close()
        try:
            mi = db.new_api.get_metadata(book_id,
                                         get_cover=False,
                                         get_user_categories=False,
                                         cover_as_data=False)
            create_book(mi, pt.name, fmt=fmt.lower())
            db.add_format_with_hooks(book_id,
                                     fmt,
                                     pt.name,
                                     index_is_id=True,
                                     notify=True)
        finally:
            os.remove(pt.name)

    def add_archive(self, single):
        paths = choose_files(self.gui,
                             'recursive-archive-add',
                             _('Choose archive file'),
                             filters=[(_('Archives'), ('zip', 'rar'))],
                             all_files=False,
                             select_only_single_file=False)
        if paths:
            self.do_add_recursive(paths, single, list_of_archives=True)

    def add_from_archive(self):
        single = question_dialog(
            self.gui, _('Type of archive'),
            _('Will the archive have a single book per internal folder?'))
        paths = choose_files(self.gui,
                             'recursive-archive-add',
                             _('Choose archive file'),
                             filters=[(_('Archives'), ('zip', 'rar'))],
                             all_files=False,
                             select_only_single_file=False)
        if paths:
            self.do_add_recursive(paths, single, list_of_archives=True)

    def add_recursive(self, single):
        root = choose_dir(self.gui, 'recursive book import root dir dialog',
                          _('Select root folder'))
        if not root:
            return
        lp = os.path.normcase(os.path.abspath(
            self.gui.current_db.library_path))
        if lp.startswith(os.path.normcase(os.path.abspath(root)) + os.pathsep):
            return error_dialog(
                self.gui,
                _('Cannot add'),
                _('Cannot add books from the folder: %s as it contains the currently opened calibre library'
                  ) % root,
                show=True)
        self.do_add_recursive(root, single)

    def do_add_recursive(self, root, single, list_of_archives=False):
        from calibre.gui2.add import Adder
        Adder(root,
              single_book_per_directory=single,
              db=self.gui.current_db,
              list_of_archives=list_of_archives,
              callback=self._files_added,
              parent=self.gui,
              pool=self.gui.spare_pool())

    def add_recursive_single(self, *args):
        '''
        Add books from the local filesystem to either the library or the device
        recursively assuming one book per folder.
        '''
        self.add_recursive(True)

    def add_recursive_multiple(self, *args):
        '''
        Add books from the local filesystem to either the library or the device
        recursively assuming multiple books per folder.
        '''
        self.add_recursive(False)

    def add_recursive_question(self):
        single = question_dialog(
            self.gui, _('Multi-file books?'),
            _('Assume all e-book files in a single folder are multiple formats of the same book?'
              ))
        self.add_recursive(single)

    def add_empty(self, *args):
        '''
        Add an empty book item to the library. This does not import any formats
        from a book file.
        '''
        author = series = title = None
        index = self.gui.library_view.currentIndex()
        if index.isValid():
            raw = index.model().db.authors(index.row())
            if raw:
                authors = [a.strip().replace('|', ',') for a in raw.split(',')]
                if authors:
                    author = authors[0]
            series = index.model().db.series(index.row())
            title = index.model().db.title(index.row())
        dlg = AddEmptyBookDialog(self.gui,
                                 self.gui.library_view.model().db,
                                 author,
                                 series,
                                 dup_title=title)
        if dlg.exec() == QDialog.DialogCode.Accepted:
            temp_files = []
            num = dlg.qty_to_add
            series = dlg.selected_series
            title = dlg.selected_title or _('Unknown')
            db = self.gui.library_view.model().db
            ids, orig_fmts = [], []
            if dlg.duplicate_current_book:
                origmi = db.get_metadata(index.row(),
                                         get_cover=True,
                                         cover_as_data=True)
                if dlg.copy_formats.isChecked():
                    book_id = db.id(index.row())
                    orig_fmts = tuple(
                        db.new_api.format(book_id, fmt, as_path=True)
                        for fmt in db.new_api.formats(book_id))

            for x in range(num):
                if dlg.duplicate_current_book:
                    mi = origmi
                else:
                    mi = MetaInformation(title, dlg.selected_authors)
                    if series:
                        mi.series = series
                        mi.series_index = db.get_next_series_num_for(series)
                fmts = []
                empty_format = gprefs.get('create_empty_format_file', '')
                if dlg.duplicate_current_book and dlg.copy_formats.isChecked():
                    fmts = orig_fmts
                elif empty_format:
                    from calibre.ebooks.oeb.polish.create import create_book
                    pt = PersistentTemporaryFile(suffix='.' + empty_format)
                    pt.close()
                    temp_files.append(pt.name)
                    create_book(mi, pt.name, fmt=empty_format)
                    fmts = [pt.name]
                ids.append(db.import_book(mi, fmts))
            for path in orig_fmts:
                os.remove(path)
            self.refresh_gui(num)
            if ids:
                ids.reverse()
                self.gui.library_view.select_rows(ids)
            for path in temp_files:
                os.remove(path)

    def check_for_existing_isbns(self, books):
        db = self.gui.current_db.new_api
        book_id_identifiers = db.all_field_for('identifiers',
                                               db.all_book_ids(tuple))
        existing_isbns = {
            normalize_isbn(ids.get('isbn', '')): book_id
            for book_id, ids in book_id_identifiers.items()
        }
        existing_isbns.pop('', None)
        ok = []
        duplicates = []
        for book in books:
            q = normalize_isbn(book['isbn'])
            if q and q in existing_isbns:
                duplicates.append((book, existing_isbns[q]))
            else:
                ok.append(book)
        if duplicates:
            det_msg = '\n'.join(
                f'{book["isbn"]}: {db.field_for("title", book_id)}'
                for book, book_id in duplicates)
            if question_dialog(
                    self.gui,
                    _('Duplicates found'),
                    _('Books with some of the specified ISBNs already exist in the calibre library.'
                      ' Click "Show details" for the full list. Do you want to add them anyway?'
                      ),
                    det_msg=det_msg):
                ok += [x[0] for x in duplicates]
        return ok

    def add_isbns(self, books, add_tags=[], check_for_existing=False):
        books = list(books)
        if check_for_existing:
            books = self.check_for_existing_isbns(books)
            if not books:
                return
        self.isbn_books = books
        self.add_by_isbn_ids = set()
        self.isbn_add_tags = add_tags
        QTimer.singleShot(10, self.do_one_isbn_add)
        self.isbn_add_dialog = ProgressDialog(
            _('Adding'),
            _('Creating book records from ISBNs'),
            max=len(books),
            cancelable=False,
            parent=self.gui)
        self.isbn_add_dialog.exec()

    def do_one_isbn_add(self):
        try:
            db = self.gui.library_view.model().db

            try:
                x = self.isbn_books.pop(0)
            except IndexError:
                self.gui.library_view.model().books_added(
                    self.isbn_add_dialog.value)
                self.isbn_add_dialog.accept()
                self.gui.iactions['Edit Metadata'].download_metadata(
                    ids=self.add_by_isbn_ids,
                    ensure_fields=frozenset(['title', 'authors']))
                return

            mi = MetaInformation(None)
            mi.isbn = x['isbn']
            if self.isbn_add_tags:
                mi.tags = list(self.isbn_add_tags)
            fmts = [] if x['path'] is None else [x['path']]
            self.add_by_isbn_ids.add(db.import_book(mi, fmts))
            self.isbn_add_dialog.value += 1
            QTimer.singleShot(10, self.do_one_isbn_add)
        except:
            self.isbn_add_dialog.accept()
            raise

    def files_dropped(self, paths):
        to_device = self.gui.stack.currentIndex() != 0
        self._add_books(paths, to_device)

    def remote_file_dropped_on_book(self, url, fname):
        if self.gui.current_view() is not self.gui.library_view:
            return
        db = self.gui.library_view.model().db
        current_idx = self.gui.library_view.currentIndex()
        if not current_idx.isValid():
            return
        cid = db.id(current_idx.row())
        from calibre.gui2.dnd import DownloadDialog
        d = DownloadDialog(url, fname, self.gui)
        d.start_download()
        if d.err is None:
            self.files_dropped_on_book(None, [d.fpath], cid=cid)

    def files_dropped_on_book(self, event, paths, cid=None, do_confirm=True):
        accept = False
        if self.gui.current_view() is not self.gui.library_view:
            return
        db = self.gui.library_view.model().db
        cover_changed = False
        current_idx = self.gui.library_view.currentIndex()
        if cid is None:
            if not current_idx.isValid():
                return
            cid = db.id(current_idx.row()) if cid is None else cid
        formats = []
        from calibre.gui2.dnd import image_extensions
        image_exts = set(image_extensions()) - set(
            tweaks['cover_drop_exclude'])
        if iswindows:
            from calibre.gui2.add import resolve_windows_links
            paths = list(
                resolve_windows_links(paths,
                                      hwnd=int(self.gui.effectiveWinId())))
        for path in paths:
            ext = os.path.splitext(path)[1].lower()
            if ext:
                ext = ext[1:]
            if ext in image_exts:
                pmap = QPixmap()
                pmap.load(path)
                if not pmap.isNull():
                    accept = True
                    db.set_cover(cid, pmap)
                    cover_changed = True
            else:
                formats.append((ext, path))
                accept = True
        if accept and event is not None:
            event.accept()
        add_as_book = False
        if do_confirm and formats:
            ok, add_as_book = confirm(_(
                'You have dropped some files onto the book <b>%s</b>. This will'
                ' add or replace the files for this book. Do you want to proceed?'
            ) % db.title(cid, index_is_id=True),
                                      'confirm_drop_on_book',
                                      parent=self.gui,
                                      extra_button=ngettext(
                                          'Add as new book',
                                          'Add as new books', len(formats)))
            if ok and add_as_book:
                add_as_book = [path for ext, path in formats]
            if not ok or add_as_book:
                formats = []
        for ext, path in formats:
            db.add_format_with_hooks(cid, ext, path, index_is_id=True)
        if current_idx.isValid():
            self.gui.library_view.model().current_changed(
                current_idx, current_idx)
        if cover_changed:
            self.gui.refresh_cover_browser()
        if add_as_book:
            self.files_dropped(add_as_book)

    def __add_filesystem_book(self, paths, allow_device=True):
        if isinstance(paths, string_or_bytes):
            paths = [paths]
        books = [
            path for path in map(os.path.abspath, paths)
            if os.access(path, os.R_OK)
        ]

        if books:
            to_device = allow_device and self.gui.stack.currentIndex() != 0
            self._add_books(books, to_device)
            if to_device:
                self.gui.status_bar.show_message(
                    _('Uploading books to device.'), 2000)

    def add_filesystem_book(self, paths, allow_device=True):
        self._add_filesystem_book(paths, allow_device=allow_device)

    def add_from_isbn(self, *args):
        from calibre.gui2.dialogs.add_from_isbn import AddFromISBN
        d = AddFromISBN(self.gui)
        if d.exec() == QDialog.DialogCode.Accepted and d.books:
            self.add_isbns(d.books,
                           add_tags=d.set_tags,
                           check_for_existing=d.check_for_existing)

    def add_books(self, *args):
        '''
        Add books from the local filesystem to either the library or the device.
        '''
        filters = get_filters()
        to_device = self.gui.stack.currentIndex() != 0
        if to_device:
            fmts = self.gui.device_manager.device.settings().format_map
            filters = [(_('Supported books'), fmts)]

        books = choose_files_and_remember_all_files(self.gui,
                                                    'add books dialog dir',
                                                    _('Select books'),
                                                    filters=filters)
        if not books:
            return
        self._add_books(books, to_device)

    def _add_books(self, paths, to_device, on_card=None):
        if on_card is None:
            on_card = 'carda' if self.gui.stack.currentIndex() == 2 else \
                      'cardb' if self.gui.stack.currentIndex() == 3 else None
        if not paths:
            return
        from calibre.gui2.add import Adder
        Adder(paths,
              db=None if to_device else self.gui.current_db,
              parent=self.gui,
              callback=partial(self._files_added, on_card=on_card),
              pool=self.gui.spare_pool())

    def refresh_gui(self, num, set_current_row=-1, recount=True):
        self.gui.library_view.model().books_added(num)
        if set_current_row > -1:
            self.gui.library_view.set_current_row(0)
        self.gui.refresh_cover_browser()
        if recount:
            self.gui.tags_view.recount()

    def _files_added(self, adder, on_card=None):
        if adder.items:
            paths, infos, names = [], [], []
            for mi, cover_path, format_paths in adder.items:
                mi.cover = cover_path
                paths.append(format_paths[0]), infos.append(mi)
                names.append(ascii_filename(os.path.basename(paths[-1])))
            self.gui.upload_books(paths, names, infos, on_card=on_card)
            self.gui.status_bar.show_message(_('Uploading books to device.'),
                                             2000)
            return

        if adder.number_of_books_added > 0:
            self.refresh_gui(adder.number_of_books_added, set_current_row=0)

        if adder.merged_books:
            merged = defaultdict(list)
            for title, author in adder.merged_books:
                merged[author].append(title)
            lines = []
            for author in sorted(merged, key=sort_key):
                lines.append(
                    f'<b><i>{prepare_string_for_xml(author)}</i></b><ol style="margin-top: 0">'
                )
                for title in sorted(merged[author]):
                    lines.append(f'<li>{prepare_string_for_xml(title)}</li>')
                lines.append('</ol>')
            pm = ngettext('The following duplicate book was found.',
                          'The following {} duplicate books were found.',
                          len(adder.merged_books)).format(
                              len(adder.merged_books))
            info_dialog(
                self.gui,
                _('Merged some books'),
                pm + ' ' +
                _('Incoming book formats were processed and merged into your '
                  'calibre database according to your auto-merge '
                  'settings. Click "Show details" to see the list of merged books.'
                  ),
                det_msg='\n'.join(lines),
                show=True,
                only_copy_details=True)

        if adder.number_of_books_added > 0 or adder.merged_books:
            # The formats of the current book could have changed if
            # automerge is enabled
            current_idx = self.gui.library_view.currentIndex()
            if current_idx.isValid():
                self.gui.library_view.model().current_changed(
                    current_idx, current_idx)

    def _add_from_device_adder(self, adder, on_card=None, model=None):
        self._files_added(adder, on_card=on_card)
        # set the in-library flags, and as a consequence send the library's
        # metadata for this book to the device. This sets the uuid to the
        # correct value. Note that set_books_in_library might sync_booklists
        self.gui.set_books_in_library(booklists=[model.db], reset=True)
        self.gui.refresh_ondevice()

    def add_books_from_device(self, view, paths=None):
        backloading_err = self.gui.device_manager.device.BACKLOADING_ERROR_MESSAGE
        if backloading_err is not None:
            return error_dialog(self.gui,
                                _('Add to library'),
                                backloading_err,
                                show=True)
        if paths is None:
            rows = view.selectionModel().selectedRows()
            if not rows or len(rows) == 0:
                d = error_dialog(self.gui, _('Add to library'),
                                 _('No book selected'))
                d.exec()
                return
            paths = [p for p in view.model().paths(rows) if p is not None]
        ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS

        def ext(x):
            ans = os.path.splitext(x)[1]
            ans = ans[1:] if len(ans) > 1 else ans
            return ans.lower()

        remove = {p for p in paths if ext(p) in ve}
        if remove:
            paths = [p for p in paths if p not in remove]
            vmsg = getattr(
                self.gui.device_manager.device,
                'VIRTUAL_BOOK_EXTENSION_MESSAGE', None) or _(
                    'The following books are virtual and cannot be added'
                    ' to the calibre library:')
            info_dialog(self.gui,
                        _('Not Implemented'),
                        vmsg,
                        '\n'.join(remove),
                        show=True)
            if not paths:
                return
        if not paths or len(paths) == 0:
            d = error_dialog(self.gui, _('Add to library'),
                             _('No book files found'))
            d.exec()
            return

        self.gui.device_manager.prepare_addable_books(
            self.Dispatcher(partial(self.books_prepared, view)), paths)
        self.bpd = ProgressDialog(_('Downloading books'),
                                  msg=_('Downloading books from device'),
                                  parent=self.gui,
                                  cancelable=False)
        QTimer.singleShot(1000, self.show_bpd)

    def show_bpd(self):
        if self.bpd is not None:
            self.bpd.show()

    def books_prepared(self, view, job):
        self.bpd.hide()
        self.bpd = None
        if job.exception is not None:
            self.gui.device_job_exception(job)
            return
        paths = job.result
        ok_paths = [x for x in paths if isinstance(x, string_or_bytes)]
        failed_paths = [x for x in paths if isinstance(x, tuple)]
        if failed_paths:
            if not ok_paths:
                msg = _('Could not download files from the device')
                typ = error_dialog
            else:
                msg = _('Could not download some files from the device')
                typ = warning_dialog
            det_msg = [
                x[0] + '\n    ' + as_unicode(x[1]) for x in failed_paths
            ]
            det_msg = '\n\n'.join(det_msg)
            typ(self.gui,
                _('Could not download files'),
                msg,
                det_msg=det_msg,
                show=True)

        if ok_paths:
            from calibre.gui2.add import Adder
            callback = partial(self._add_from_device_adder,
                               on_card=None,
                               model=view.model())
            Adder(ok_paths,
                  db=self.gui.current_db,
                  parent=self.gui,
                  callback=callback,
                  pool=self.gui.spare_pool())
コード例 #3
0
ファイル: copy_to_library.py プロジェクト: qykth-git/calibre
class CopyToLibraryAction(InterfaceAction):

    name = 'Copy To Library'
    action_spec = (_('Copy to library'), 'copy-to-library.png',
                   _('Copy selected books to the specified library'), None)
    popup_type = QToolButton.ToolButtonPopupMode.InstantPopup
    dont_add_to = frozenset(['context-menu-device'])
    action_type = 'current'
    action_add_menu = True

    def genesis(self):
        self.menu = self.qaction.menu()

    @property
    def stats(self):
        return self.gui.iactions['Choose Library'].stats

    def library_changed(self, db):
        self.build_menus()

    def initialization_complete(self):
        self.library_changed(self.gui.library_view.model().db)

    def location_selected(self, loc):
        enabled = loc == 'library'
        self.qaction.setEnabled(enabled)
        self.menuless_qaction.setEnabled(enabled)

    def build_menus(self):
        self.menu.clear()
        if os.environ.get('CALIBRE_OVERRIDE_DATABASE_PATH', None):
            self.menu.addAction('disabled', self.cannot_do_dialog)
            return
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))
        if len(locations) > 5:
            self.menu.addAction(_('Choose library...'), self.choose_library)
            self.menu.addSeparator()
        for name, loc in locations:
            name = name.replace('&', '&&')
            self.menu.addAction(name, partial(self.copy_to_library, loc))
            self.menu.addAction(
                name + ' ' + _('(delete after copy)'),
                partial(self.copy_to_library, loc, delete_after=True))
            self.menu.addSeparator()
        if len(locations) <= 5:
            self.menu.addAction(_('Choose library...'), self.choose_library)

        self.qaction.setVisible(bool(locations))
        if ismacos:
            # The cloned action has to have its menu updated
            self.qaction.changed.emit()

    def choose_library(self):
        db = self.gui.library_view.model().db
        locations = list(self.stats.locations(db))
        d = ChooseLibrary(self.gui, locations)
        if d.exec() == QDialog.DialogCode.Accepted:
            path, delete_after = d.args
            if not path:
                return
            db = self.gui.library_view.model().db
            current = os.path.normcase(os.path.abspath(db.library_path))
            if current == os.path.normcase(os.path.abspath(path)):
                return error_dialog(self.gui,
                                    _('Cannot copy'),
                                    _('Cannot copy to current library.'),
                                    show=True)
            self.copy_to_library(path, delete_after)

    def _column_is_compatible(self, source_metadata, dest_metadata):
        return (source_metadata['datatype'] == dest_metadata['datatype']
                and (source_metadata['datatype'] != 'text'
                     or source_metadata['is_multiple']
                     == dest_metadata['is_multiple']))

    def copy_to_library(self, loc, delete_after=False):
        rows = self.gui.library_view.selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui,
                                _('Cannot copy'),
                                _('No books selected'),
                                show=True)
        ids = list(map(self.gui.library_view.model().id, rows))
        db = self.gui.library_view.model().db
        if not db.exists_at(loc):
            return error_dialog(self.gui,
                                _('No library'),
                                _('No library found at %s') % loc,
                                show=True)

        # Open the new db so we can check the custom columns. We use only the
        # backend since we only need the custom column definitions, not the
        # rest of the data in the db. We also do not want the user defined
        # formatter functions because loading them can poison the template cache
        global libraries_with_checked_columns

        from calibre.db.legacy import create_backend
        newdb = create_backend(loc, load_user_formatter_functions=False)

        continue_processing = True
        with closing(newdb):
            if newdb.library_id not in libraries_with_checked_columns[
                    db.library_id]:

                newdb_meta = newdb.field_metadata.custom_field_metadata()
                incompatible_columns = []
                missing_columns = []
                for k, m in db.field_metadata.custom_iteritems():
                    if k not in newdb_meta:
                        missing_columns.append(k)
                    elif not self._column_is_compatible(m, newdb_meta[k]):
                        # Note that composite columns are always assumed to be
                        # compatible. No attempt is made to copy the template
                        # from the source to the destination.
                        incompatible_columns.append(k)

                if missing_columns or incompatible_columns:
                    continue_processing = ask_about_cc_mismatch(
                        self.gui, db, newdb, missing_columns,
                        incompatible_columns)
                if continue_processing:
                    libraries_with_checked_columns[db.library_id].add(
                        newdb.library_id)

        newdb.close()
        del newdb
        if not continue_processing:
            return
        duplicate_ids = self.do_copy(ids, db, loc, delete_after, False)
        if duplicate_ids:
            d = DuplicatesQuestion(self.gui, duplicate_ids, loc)
            if d.exec() == QDialog.DialogCode.Accepted:
                ids = d.ids
                if ids:
                    self.do_copy(list(ids),
                                 db,
                                 loc,
                                 delete_after,
                                 add_duplicates=True)

    def do_copy(self, ids, db, loc, delete_after, add_duplicates=False):
        aname = _('Moving to') if delete_after else _('Copying to')
        dtitle = '%s %s' % (aname, os.path.basename(loc))
        self.pd = ProgressDialog(dtitle,
                                 min=0,
                                 max=len(ids) - 1,
                                 parent=self.gui,
                                 cancelable=True,
                                 icon='lt.png')

        def progress(idx, title):
            self.pd.set_msg(title)
            self.pd.set_value(idx)

        self.worker = Worker(ids, db, loc, Dispatcher(progress),
                             Dispatcher(self.pd.accept), delete_after,
                             add_duplicates)
        self.worker.start()
        self.pd.canceled_signal.connect(self.worker.cancel_processing)

        self.pd.exec()
        self.pd.canceled_signal.disconnect()

        if self.worker.left_after_cancel:
            msg = _(
                'The copying process was interrupted. {} books were copied.'
            ).format(len(self.worker.processed))
            if delete_after:
                msg += ' ' + _('No books were deleted from this library.')
            msg += ' ' + _(
                'The best way to resume this operation is to re-copy all the books with the option to'
                ' "Check for duplicates when copying to library" in Preferences->Import/export->Adding books turned on.'
            )
            warning_dialog(self.gui, _('Canceled'), msg, show=True)
            return

        if self.worker.error is not None:
            e, tb = self.worker.error
            error_dialog(self.gui,
                         _('Failed'),
                         _('Could not copy books: ') + e,
                         det_msg=tb,
                         show=True)
            return

        if delete_after:
            donemsg = _('Moved the book to {loc}') if len(
                self.worker.processed) == 1 else _(
                    'Moved {num} books to {loc}')
        else:
            donemsg = _('Copied the book to {loc}') if len(
                self.worker.processed) == 1 else _(
                    'Copied {num} books to {loc}')

        self.gui.status_bar.show_message(
            donemsg.format(num=len(self.worker.processed), loc=loc), 2000)
        if self.worker.auto_merged_ids:
            books = '\n'.join(itervalues(self.worker.auto_merged_ids))
            info_dialog(
                self.gui,
                _('Auto merged'),
                _('Some books were automatically merged into existing '
                  'records in the target library. Click "Show '
                  'details" to see which ones. This behavior is '
                  'controlled by the Auto-merge option in '
                  'Preferences->Import/export->Adding books->Adding actions.'),
                det_msg=books,
                show=True)
        done_ids = frozenset(self.worker.processed) - frozenset(
            self.worker.duplicate_ids)
        if delete_after and done_ids:
            v = self.gui.library_view
            ci = v.currentIndex()
            row = None
            if ci.isValid():
                row = ci.row()

            v.model().delete_books_by_id(done_ids, permanent=True)
            self.gui.iactions['Remove Books'].library_ids_deleted(
                done_ids, row)

        if self.worker.failed_books:

            def fmt_err(book_id):
                err, tb = self.worker.failed_books[book_id]
                title = db.title(book_id, index_is_id=True)
                return _('Copying: {0} failed, with error:\n{1}').format(
                    title, tb)

            title, msg = _('Failed to copy some books'), _(
                'Could not copy some books, click "Show details" for more information.'
            )
            tb = '\n\n'.join(map(fmt_err, self.worker.failed_books))
            tb = ngettext('Failed to copy a book, see below for details',
                          'Failed to copy {} books, see below for details',
                          len(self.worker.failed_books)).format(
                              len(self.worker.failed_books)) + '\n\n' + tb
            if len(ids) == len(self.worker.failed_books):
                title, msg = _('Failed to copy books'), _(
                    'Could not copy any books, click "Show details" for more information.'
                )
            error_dialog(self.gui, title, msg, det_msg=tb, show=True)
        return self.worker.duplicate_ids

    def cannot_do_dialog(self):
        warning_dialog(
            self.gui,
            _('Not allowed'),
            _('You cannot use other libraries while using the environment'
              ' variable CALIBRE_OVERRIDE_DATABASE_PATH.'),
            show=True)