View Single Post
Old 08-30-2021, 05:08 AM   #17
capink
Wizard
capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.capink ought to be getting tired of karma fortunes by now.
 
Posts: 1,118
Karma: 1954138
Join Date: Aug 2015
Device: Kindle
Save to disk action (non-interactive)

Code:
import os, numbers
import copy

from qt.core import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
                     QGroupBox, QComboBox, QRadioButton, QCheckBox, QToolButton, QLineEdit)

from calibre import prints
from calibre.constants import iswindows, isosx, islinux, DEBUG
from calibre.gui2 import error_dialog, Dispatcher, choose_dir
from calibre.gui2.actions.save_to_disk import SaveToDiskAction
from calibre.utils.formatter_functions import formatter_functions
from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from polyglot.builtins import itervalues

from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import DragDropComboBox, get_icon
from calibre_plugins.action_chains.templates.dialogs import TemplateBox
from calibre_plugins.action_chains.templates import check_template

class ModifiedSaveToDiskAction(SaveToDiskAction):

    def __init__(self, gui):
        self.gui = gui

    def save_to_disk(self, single_dir=False, single_format=None,
            rows=None, write_opf=None, save_cover=None, path=None, opts=None):
        if rows is None:
            rows = self.gui.current_view().selectionModel().selectedRows()
        if not rows or len(rows) == 0:
            return error_dialog(self.gui, _('Cannot save to disk'),
                    _('No books selected'), show=True)
        if not path:
            path = choose_dir(self.gui, 'save to disk dialog',
                    _('Choose destination folder'))
        if not path:
            return
        dpath = os.path.abspath(path).replace('/', os.sep)+os.sep
        lpath = self.gui.library_view.model().db.library_path.replace('/',
                os.sep)+os.sep
        if dpath.startswith(lpath):
            return error_dialog(self.gui, _('Not allowed'),
                    _('You are trying to save files into the calibre '
                      'library. This can cause corruption of your '
                      'library. Save to disk is meant to export '
                      'files from your calibre library elsewhere.'), show=True)

        if self.gui.current_view() is self.gui.library_view:
            from calibre.gui2.save import Saver
            from calibre.library.save_to_disk import config
            if opts is None:
                opts = config().parse()
            #print('debug1: opts.template: {}, opts.send_template: {}, opts.send_timefmt: {}, opts.timefmt: {}, opts.update_metadata: {}'.format(opts.template, opts.send_template, opts.send_timefmt, opts.timefmt, opts.update_metadata))
            if single_format is not None:
                opts.formats = single_format
                # Special case for Kindle annotation files
                if single_format.lower() in ['mbp','pdr','tan']:
                    opts.to_lowercase = False
                    opts.save_cover = False
                    opts.write_opf = False
                    opts.template = opts.send_template
            opts.single_dir = single_dir
            if write_opf is not None:
                opts.write_opf = write_opf
            if save_cover is not None:
                opts.save_cover = save_cover
            book_ids = set(map(self.gui.library_view.model().id, rows))
            Saver(book_ids, self.gui.current_db, opts, path, parent=self.gui, pool=self.gui.spare_pool())
        else:
            paths = self.gui.current_view().model().paths(rows)
            self.gui.device_manager.save_books(
                    Dispatcher(self.books_saved), paths, path)

class ConfigWidget(QWidget):
    def __init__(self, plugin_action):
        QWidget.__init__(self)
        self.plugin_action = plugin_action
        self.gui = plugin_action.gui
        self.db = self.gui.current_db
        self._init_controls()

    def _init_controls(self):

        l = self.l = QVBoxLayout()
        self.setLayout(l)

        self.path_box = QGroupBox(_('&Choose path:'))
        l.addWidget(self.path_box)
        path_layout = QVBoxLayout()
        self.path_box.setLayout(path_layout)
        self.path_combo = DragDropComboBox(self, drop_mode='file')
        path_layout.addWidget(self.path_combo)
        hl1 = QHBoxLayout()
        path_layout.addLayout(hl1)
        hl1.addWidget(self.path_combo, 1)
        self.choose_path_button = QToolButton(self)
        self.choose_path_button.setToolTip(_('Choose path'))
        self.choose_path_button.setIcon(get_icon('document_open.png'))
        self.choose_path_button.clicked.connect(self._choose_path)
        hl1.addWidget(self.choose_path_button)

        formats_gb = QGroupBox(_('Formats: '), self)
        l.addWidget(formats_gb)
        formats_l = QVBoxLayout()
        formats_gb.setLayout(formats_l)
        self.all_fmts_opt = QRadioButton(_('All formats'))
        self.all_fmts_opt.setChecked(True)
        formats_l.addWidget(self.all_fmts_opt)
        self.fmts_list_layout = QHBoxLayout()
        self.fmts_list_opt = QRadioButton(_('Save only specified formats'))
        self.fmts_list_edit = QLineEdit()
        self.fmts_list_edit.setToolTip(_('Comma separated list of formsts you want to save'))
        self.fmts_list_layout.addWidget(self.fmts_list_opt, 1)
        self.fmts_list_layout.addWidget(self.fmts_list_edit)
        formats_l.addLayout(self.fmts_list_layout)

        template_gb = QGroupBox(_('Template'))
        template_gb_l = QVBoxLayout()
        template_gb.setLayout(template_gb_l)
        self.default_template_opt = QRadioButton(_('Use template define in calibre preferences'))
        template_gb_l.addWidget(self.default_template_opt)
        self.default_template_opt.setChecked(True)
        self.user_template_opt = QRadioButton(_('Define a custom template for this action'))
        template_gb_l.addWidget(self.user_template_opt)
        l.addWidget(template_gb)
        self.user_template_opt.toggled.connect(self._on_template_opt_toggled)
        self.template_edit = TemplateLineEditor(self)
        template_gb_l.addWidget(self.template_edit)

        opts_gb = QGroupBox(_('Options: '), self)
        l.addWidget(opts_gb)
        opts_l = QVBoxLayout()
        opts_gb.setLayout(opts_l)
        self.single_folder_chk = QCheckBox(_('Save in a single folder'))
        opts_l.addWidget(self.single_folder_chk)
        self.save_cover_chk = QCheckBox(_('Save cover'))
        self.save_cover_chk.setChecked(True)
        opts_l.addWidget(self.save_cover_chk)
        self.save_opf_chk = QCheckBox(_('Save opf'))
        self.save_opf_chk.setChecked(True)
        opts_l.addWidget(self.save_opf_chk)
        
        l.addStretch(1)        

#        self.all_formats = self.gui.library_view.model().db.all_formats()

        self._on_template_opt_toggled()
        self.setMinimumSize(400,500) 

    def _on_template_opt_toggled(self):
        self.template_edit.setEnabled(self.user_template_opt.isChecked())

    def _choose_path(self):
        path = choose_dir(self.gui, 'save to disk dialog',
                _('Choose destination folder'))
        if not path:
            return

        if iswindows:
            path = os.path.normpath(path)

        self.block_events = True
        existing_index = self.path_combo.findText(path, Qt.MatchExactly)
        if existing_index >= 0:
            self.path_combo.setCurrentIndex(existing_index)
        else:
            self.path_combo.insertItem(0, path)
            self.path_combo.setCurrentIndex(0)
        self.block_events = False

    def load_settings(self, settings):
        if settings:
            if settings['fmt_opt'] == 'all_formats':
                self.all_fmts_opt.setChecked(True)
            elif settings['fmt_opt'] == 'selected_formats':
                self.fmts_list_opt.setChecked(True)
                fmt = settings['formats']
                self.fmts_list_edit.setText(fmt)

            self.path_combo.setCurrentText(settings['path'])

            template_opt = settings.get('template_opt', 'default')
            if template_opt == 'default':
                self.default_template_opt.setChecked(True)
            elif template_opt == 'user':
                self.user_template_opt.setChecked(True)
                self.template_edit.setText(settings['template'])
            self.single_folder_chk.setChecked(settings['single_folder'])
            self.save_cover_chk.setChecked(settings['save_cover'])
            self.save_opf_chk.setChecked(settings['save_opf'])            

    def save_settings(self):
        settings = {}
        settings['path'] = self.path_combo.currentText().strip()
        if self.fmts_list_opt.isChecked():
            settings['fmt_opt'] = 'selected_formats'
            settings['formats'] = self.fmts_list_edit.text()
        elif self.all_fmts_opt.isChecked():
            settings['fmt_opt'] = 'all_formats'
        if self.default_template_opt.isChecked():
            settings['template_opt'] = 'default'
        elif self.user_template_opt.isChecked():
            settings['template_opt'] = 'user'
            settings['template'] = self.template_edit.text()
        settings['single_folder'] = self.single_folder_chk.isChecked()
        settings['save_cover'] = self.save_cover_chk.isChecked()
        settings['save_opf'] = self.save_opf_chk.isChecked()
        return settings

class SaveToAction(ChainAction):

    name = 'Save to disk'

    def run(self, gui, settings, chain):
        from calibre.library.save_to_disk import config
        opts = copy.deepcopy(config().parse())
        path = settings['path']
        single_dir = settings['single_folder']
        if settings['fmt_opt'] == 'selected_formats':
            opts.formats = settings['formats'].lower()
        if settings.get('template_opt', 'default') == 'user':
            opts.template = settings['template']
        save_cover = settings['save_cover']
        write_opf = settings['save_opf']
        action = ModifiedSaveToDiskAction(gui)
        action.save_to_disk(single_dir=single_dir, write_opf=write_opf,
                            save_cover=save_cover, path=path, opts=opts)

    def validate(self, settings):
        if not settings:
            return (_('Settings Error'), _('You must configure this action before running it'))
        path = settings['path']
        if not path:
            return (_('No Directory'), _('You must specify a path to valid path'))
        if not (os.access(path, os.W_OK) and os.path.isdir(path)):
            return (_('Invalid direcotry'), _('Path is not a writable directory: {}'.format(path)))
        if not settings.get('fmt_opt'):
            return (_('No Path'), _('You must choose a format option'))
        if settings['fmt_opt'] == 'selected_formats':
            if not settings['formats']:
                return (_('No format'), _('You must choose a valid format value'))
        template_opt = settings.get('template_opt', 'default')
        if template_opt == 'user':
            template = settings['template']
            if not template:
                return (_('No template'), _('You must specify a template'))
            else:
                # only calibre template functions, exclude Action Chains defined functions
                template_functions = formatter_functions().get_functions()
                is_template_valid = check_template(template, self.plugin_action, print_error=False, template_functions=template_functions)
                if is_template_valid is not True:
                    return is_template_valid

        return True

    def config_widget(self):
        return ConfigWidget
Edit: This action has been modified in a way the is not backward compatible with previous iterations. If using the new code, you are advised to re-create the action from scratch.

Last edited by capink; 01-07-2022 at 06:15 AM. Reason: Allow specifying multiple formats to save
capink is offline   Reply With Quote