|
|
#16 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Books added event
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
__license__ = 'GPL v3'
__copyright__ = '2021, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'
from functools import partial
from qt.core import (QApplication, Qt, QTimer, QWidget, QVBoxLayout, QCheckBox, pyqtSignal)
from calibre import prints
from calibre.constants import DEBUG
from calibre.db.listeners import EventType
from calibre.utils.date import now
from calibre_plugins.action_chains.events.base import ChainEvent
import calibre_plugins.action_chains.config as cfg
try:
load_translations()
except NameError:
prints("ActionChains::events/books_added.py - exception when loading translations")
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.select_chk = QCheckBox(_('Select newly added books'))
self.select_chk.setChecked(True)
l.addWidget(self.select_chk)
l.addStretch(1)
self.setMinimumSize(300,300)
def load_settings(self, settings):
if settings:
self.select_chk.setChecked(settings.get('select_books', False))
def save_settings(self):
settings = {}
settings['select_books'] = self.select_chk.isChecked()
return settings
class BooksAddedEvent(ChainEvent):
name = 'Books Added'
books_added = pyqtSignal(object)
timer_interval = 30
def __init__(self, plugin_action):
ChainEvent.__init__(self, plugin_action)
self.db = plugin_action.gui.current_db
self.gui.add_db_listener(self.process_event_in_db)
self.book_created_cache = set()
self.book_created_cache_last_updated = None
QTimer.singleShot(self.timer_interval * 1000, self._on_timeout)
def process_event_in_db(self, db, event_type, event_data):
if not db.library_id == self.gui.current_db.library_id:
return
if event_type == EventType.book_created:
book_id = event_data[0]
self.add_to_book_created_cache(book_id, now())
elif event_type == EventType.books_removed:
removed_book_ids = event_data[0]
self.book_created_cache = self.book_created_cache.difference(set(removed_book_ids))
def _on_timeout(self):
# Make sure no modal widget dialog is present (e.g. add books duplicate dialog). Otherwise, postpone
if QApplication.instance().activeModalWidget():
pass
# postpone event if another action chains is running
elif self.plugin_action.chainStack:
pass
else:
utime = self.book_created_cache_last_updated
if utime:
elapsed = now() - utime
# Wait for some seconds to make sure all books are added. If books are added by calibre
# auto-add, there will not be a modal dialog, so we have to wait a little to make sure
# auto-add finished adding all the books
wait_time = 20
if elapsed.total_seconds() > wait_time:
QTimer.singleShot(0, partial(self.books_added.emit, self.book_created_cache))
QTimer.singleShot(0, self.clean_book_created_cache)
# keep the timer runnig
QTimer.singleShot(self.timer_interval * 1000, self._on_timeout)
def add_to_book_created_cache(self, book_id, timestamp):
self.book_created_cache.add(book_id)
self.book_created_cache_last_updated = timestamp
def clean_book_created_cache(self):
self.book_created_cache = set()
self.book_created_cache_last_updated = None
def get_event_signal(self):
return self.books_added
def config_widget(self):
return ConfigWidget
def pre_chains_event_actions(self, event_args, event_opts):
if event_opts.get('select_books', False):
book_ids = event_args[0]
self.gui.library_view.select_rows(book_ids)
if DEBUG:
prints('Action Chains: Books Added Event: Selecting newly added book_ids: {}')
Notes:
Last edited by capink; 01-20-2025 at 01:12 PM. |
|
|
|
|
|
#17 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
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
Last edited by capink; 01-07-2022 at 06:15 AM. Reason: Allow specifying multiple formats to save |
|
|
|
|
|
#18 |
|
Chalut o/
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 486
Karma: 678910
Join Date: Dec 2017
Device: Kobo
|
Suggest for the forum :
A button "Copy Code", because it can clearly improve the quality of life. Especially with so many good modules. |
|
|
|
|
|
#19 |
|
null operator (he/him)
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 22,003
Karma: 30277294
Join Date: Mar 2012
Location: Sydney Australia
Device: none
|
|
|
|
|
|
|
#20 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Switch to VL view
This action allows the user to assign View Manager views to virtual libraries. When used with "VL Tab Changed" event, it will automatically apply the corresponding view whenever the user switches VL tabs.
Code:
import copy
from functools import partial
from qt.core import (QApplication, Qt, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QGroupBox, QAbstractTableModel, QModelIndex, QSizePolicy,
QToolButton, QSpacerItem, QIcon, QBrush, pyqtSignal)
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import error_dialog
from calibre.gui2.widgets2 import Dialog
from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import get_icon, ViewLog
from calibre_plugins.action_chains.database import get_valid_vls
from calibre_plugins.action_chains.gui.delegates import ComboDelegate
from calibre_plugins.action_chains.gui.models import UP, DOWN
from calibre_plugins.action_chains.gui.views import TableView
import calibre_plugins.action_chains.config as cfg
ALL_BOOKS = '_ALL_BOOKS'
KEY_TABS_VIEWS_TABLE_STATE = 'tabsViewsTableStates'
def get_vls(db):
vls = get_valid_vls(db)
vls.insert(0, ALL_BOOKS)
return vls
def view_manager_views(gui):
try:
import calibre_plugins.view_manager.config as vm_cfg
views = vm_cfg.get_library_config(gui.current_db)[vm_cfg.KEY_VIEWS]
return views
except:
import traceback
print(traceback.format_exc())
return []
class TabsModel(QAbstractTableModel):
error = pyqtSignal(str, str)
def __init__(self, plugin_action, tabs_config=[]):
QAbstractTableModel.__init__(self)
self.tabs_config = tabs_config
self.plugin_action = plugin_action
self.gui = self.plugin_action.gui
self.db = self.gui.current_db
self.col_map = ['tab_name','view_name','errors']
self.editable_columns = ['tab_name','view_name']
#self.hidden_cols = ['errors']
self.hidden_cols = []
self.col_min_width = {
'tab_name': 300,
'view_name': 300
}
all_headers = [_('VL'), _('View'), _('errors')]
self.headers = all_headers
def rowCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.tabs_config)
def columnCount(self, parent):
if parent and parent.isValid():
return 0
return len(self.headers)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.headers[section]
elif role == Qt.DisplayRole and orientation == Qt.Vertical:
return section + 1
return None
def data(self, index, role):
if not index.isValid():
return None;
row, col = index.row(), index.column()
if row < 0 or row >= len(self.tabs_config):
return None
tab_config = self.tabs_config[row]
col_name = self.col_map[col]
value = tab_config.get(col_name, '')
error = tab_config.get('errors', '')
if role in [Qt.DisplayRole, Qt.UserRole, Qt.EditRole]:
if col_name == 'errors':
if error:
return error
else:
return value
elif role == Qt.DecorationRole:
if col_name == 'errors':
if error:
return QIcon(get_icon('dialog_error.png'))
elif role == Qt.ToolTipRole:
if col_name == 'errors':
if error:
return error
elif role == Qt.ForegroundRole:
color = None
if error:
color = Qt.red
if color is not None:
return QBrush(color)
return None
def setData(self, index, value, role):
done = False
row, col = index.row(), index.column()
tab_config = self.tabs_config[row]
val = str(value).strip()
col_name = self.col_map[col]
if role == Qt.EditRole:
# make sure no duplicate event entries
if col_name == 'tab_name':
old_name = self.data(index, Qt.DisplayRole)
names = self.get_names()
if old_name in names:
names.remove(old_name)
if val in names:
msg = _('Duplicate vls')
details = _('Name ({}) is used in more than one entry'.format(val))
self.error.emit(msg, details)
else:
tab_config[col_name] = val
else:
tab_config[col_name] = val
done = True
return done
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
if index.isValid():
tab_config = self.tabs_config[index.row()]
col_name = self.col_map[index.column()]
if col_name in self.editable_columns:
flags |= Qt.ItemIsEditable
return flags
def insertRows(self, row, count, idx):
self.beginInsertRows(QModelIndex(), row, row + count - 1)
for i in range(0, count):
tab_config = {}
tab_config['tab_name'] = ''
tab_config['view_name'] = ''
self.tabs_config.insert(row + i, tab_config)
self.endInsertRows()
return True
def removeRows(self, row, count, idx):
self.beginRemoveRows(QModelIndex(), row, row + count - 1)
for i in range(0, count):
self.tabs_config.pop(row + i)
self.endRemoveRows()
return True
def move_rows(self, rows, direction=DOWN):
srows = sorted(rows, reverse=direction == DOWN)
for row in srows:
pop = self.tabs_config.pop(row)
self.tabs_config.insert(row+direction, pop)
self.layoutChanged.emit()
def get_names(self):
names = []
col = self.col_map.index('tab_name')
for row in range(self.rowCount(QModelIndex())):
index = self.index(row, col, QModelIndex())
name = self.data(index, Qt.DisplayRole)
# empty name belong to separators, dont include
if name:
names.append(name)
return names
def validate(self):
for tab_config in self.tabs_config:
errors = []
tab_name = tab_config['tab_name']
view_name = tab_config['view_name']
if tab_name not in get_vls(self.db):
errors.append(_('VL is not available'))
if view_name not in view_manager_views(self.gui):
errors.append(_('View is not available'))
if errors:
tab_config['errors'] = ' ::: '.join(errors)
class TabsTable(TableView):
def __init__(self, parent):
TableView.__init__(self, parent)
self.plugin_action = parent.plugin_action
self.doubleClicked.connect(self._on_double_clicked)
self.gui = self.plugin_action.gui
self.db = self.gui.current_db
self.horizontalHeader().setStretchLastSection(False)
#self.setShowGrid(False)
def set_model(self, _model):
self.setModel(_model)
_model.error.connect(lambda *args: error_dialog(self, *args, show=True))
self.col_map = _model.col_map
# Hide columns
for col_name in _model.hidden_cols:
col = self.col_map.index(col_name)
self.setColumnHidden(col, True)
self.tabs_delegate = ComboDelegate(self, get_vls(self.db))
self.setItemDelegateForColumn(self.col_map.index('tab_name'), self.tabs_delegate)
self.views_delegate = ComboDelegate(self, view_manager_views(self.gui))
self.setItemDelegateForColumn(self.col_map.index('view_name'), self.views_delegate)
self.resizeColumnsToContents()
# Make sure every other column has a minimum width
for col_name, width in _model.col_min_width.items():
col = self.col_map.index(col_name)
self._set_minimum_column_width(col, width)
def _on_double_clicked(self, index):
m = self.model()
col_name = m.col_map[index.column()]
if col_name == 'errors':
tab_config = m.tabs_config[index.row()]
details = tab_config.get('errors', '')
self._view_error_details(details)
def _view_error_details(self, details):
ViewLog(_('Errors details'), details, self)
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.tabs_config = tabs_config
self._init_controls()
def _init_controls(self):
self.setWindowTitle(_('Configure views'))
self.l = l = QVBoxLayout()
self.setLayout(l)
settings_l = QGridLayout()
l.addLayout(settings_l)
_table_gb = QGroupBox(_('Views'))
_table_l = QHBoxLayout()
_table_gb.setLayout(_table_l)
l.addWidget(_table_gb)
self._table = TabsTable(self)
_table_l.addWidget(self._table)
_model = self._model = TabsModel(self.plugin_action)
_model.validate()
self._table.set_model(_model)
self._table.selectionModel().selectionChanged.connect(self._on_table_selection_change)
# restore table state
state = cfg.plugin_prefs.get(KEY_TABS_VIEWS_TABLE_STATE, None)
if state:
self._table.apply_state(state)
# Add a vertical layout containing the the buttons to move up/down etc.
button_layout = QVBoxLayout()
_table_l.addLayout(button_layout)
move_up_button = self.move_up_button = QToolButton(self)
move_up_button.setToolTip(_('Move row up'))
move_up_button.setIcon(QIcon(I('arrow-up.png')))
button_layout.addWidget(move_up_button)
spacerItem1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
button_layout.addItem(spacerItem1)
add_button = self.add_button = QToolButton(self)
add_button.setToolTip(_('Add row'))
add_button.setIcon(QIcon(I('plus.png')))
button_layout.addWidget(add_button)
spacerItem2 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
button_layout.addItem(spacerItem2)
delete_button = self.delete_button = QToolButton(self)
delete_button.setToolTip(_('Delete row'))
delete_button.setIcon(QIcon(I('minus.png')))
button_layout.addWidget(delete_button)
spacerItem4 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
button_layout.addItem(spacerItem4)
move_down_button = self.move_down_button = QToolButton(self)
move_down_button.setToolTip(_('Move row down'))
move_down_button.setIcon(QIcon(I('arrow-down.png')))
button_layout.addWidget(move_down_button)
move_up_button.clicked.connect(partial(self._table.move_rows,UP))
move_down_button.clicked.connect(partial(self._table.move_rows,DOWN))
add_button.clicked.connect(self._table.add_row)
delete_button.clicked.connect(self._table.delete_rows)
self._on_table_selection_change()
self.setMinimumSize(400, 300)
l.addStretch(1)
def _on_table_selection_change(self):
sm = self._table.selectionModel()
selection_count = len(sm.selectedRows())
self.delete_button.setEnabled(selection_count > 0)
self.move_up_button.setEnabled(selection_count > 0)
self.move_down_button.setEnabled(selection_count > 0)
def save_table_state(self):
# save table state
cfg.plugin_prefs[KEY_TABS_VIEWS_TABLE_STATE] = self._table.get_state()
def load_settings(self, settings):
self._model.tabs_config = settings['tabs_config']
self._model.validate()
self._model.layoutChanged.emit()
def save_settings(self):
self.save_table_state()
settings = {}
tabs_config = self._table.model().tabs_config
# remove error keys from event_members
for tab_config in tabs_config:
try:
del tab_config['errors']
except:
pass
settings['tabs_config'] = tabs_config
return settings
class SwitchToVLView(ChainAction):
name = 'Switch To VL View'
def run(self, gui, settings, chain):
idx = gui.vl_tabs.currentIndex()
vl = str(gui.vl_tabs.tabData(idx) or '').strip() or ALL_BOOKS
print('debug1: vl: {}'.format(vl))
if vl:
view = self.vl_view_lookup(vl, settings['tabs_config'])
print('debug2: view: {}'.format(view))
if view:
if not view in view_manager_views(gui):
if DEBUG:
prints('Action Chains: Switch To VL View: view ({}) is not available'.format(view))
return
self.switch_view(gui, view, vl)
else:
if DEBUG:
prints('Action Chains: Switch To VL View: VL Tab ({}) has no configured view'.format(vl))
def vl_view_lookup(self, vl, tabs_config):
for tab_config in tabs_config:
if vl == tab_config['tab_name']:
return tab_config['view_name']
def switch_view(self, gui, view, vl):
view_manager = gui.iactions.get('View Manager')
if view_manager:
import calibre_plugins.view_manager.config as vm_cfg
library_config = vm_cfg.get_library_config(gui.current_db)
view_info = copy.deepcopy(library_config[vm_cfg.KEY_VIEWS][view])
####
view_info[vm_cfg.KEY_APPLY_VIRTLIB] = False
####
selected_ids = gui.library_view.get_selected_ids()
# Persist this as the last selected view
if library_config.get(vm_cfg.KEY_LAST_VIEW, None) != view:
library_config[vm_cfg.KEY_LAST_VIEW] = view
vm_cfg.set_library_config(gui.current_db, library_config)
# if view_info.get(vm_cfg.KEY_APPLY_VIRTLIB,False):
# view_manager.apply_virtlib(view_info[vm_cfg.KEY_VIRTLIB])
if view_info[vm_cfg.KEY_APPLY_RESTRICTION]:
view_manager.apply_restriction(view_info[vm_cfg.KEY_RESTRICTION])
if view_info[vm_cfg.KEY_APPLY_SEARCH]:
view_manager.apply_search(view_info[vm_cfg.KEY_SEARCH])
view_manager.apply_column_and_sort(view_info)
gui.library_view.select_rows(selected_ids)
view_manager.current_view = view
view_manager.rebuild_menus()
else:
if DEBUG:
prints('Action Chains: Switch To VL View: View Manager Plugin not available')
def validate(self, settings):
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
return True
def config_widget(self):
return ConfigWidget
Last edited by capink; 01-07-2022 at 06:16 AM. |
|
|
|
|
|
#21 | |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Refresh current search
Quote:
Code:
from calibre_plugins.action_chains.actions.base import ChainAction
class RefreshSearch(ChainAction):
name = 'Refresh Current Search'
def run(self, gui, settings, chain):
gui.search.do_search()
|
|
|
|
|
|
|
#22 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Multisort action
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
__license__ = 'GPL v3'
__copyright__ = '2021, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'
from qt.core import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
QLabel, QGroupBox, QToolButton, QPushButton, QScrollArea, QComboBox,
QRadioButton, QCheckBox, QSizePolicy)
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import error_dialog
from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import get_icon
try:
load_translations()
except NameError:
prints("ActionChains::actions/sort.py - exception when loading translations")
pass
def get_cols(db):
standard = [
'title',
'authors',
'tags',
'series',
'publisher',
'pubdate',
'rating',
'languages',
'last_modified',
'timestamp',
'comments',
'author_sort',
'sort',
'marked',
'identifiers',
'cover',
'formats'
]
custom = sorted([ k for k,v in db.field_metadata.custom_field_metadata().items() if v['datatype'] not in [None,'composite'] ])
return standard + custom
class SortControl(QGroupBox):
def __init__(self, plugin_action, possible_cols):
self.plugin_action = plugin_action
self.possible_cols = possible_cols
self.gui = plugin_action.gui
self.db = self.gui.current_db
self._init_controls()
def _init_controls(self):
QGroupBox.__init__(self)
l = QGridLayout()
self.setLayout(l)
row_idx = 0
remove_label = QLabel('<a href="close">✕</a>')
remove_label.setToolTip(_('Remove'))
remove_label.linkActivated.connect(self._remove)
l.addWidget(remove_label, row_idx, 1, 1, 1, Qt.AlignRight)
row_idx += 1
gb1 = QGroupBox('')
gb1_l = QVBoxLayout()
gb1.setLayout(gb1_l)
gb1_text = _('Column:')
self.col_combo_box = QComboBox()
self.col_combo_box.addItems(self.possible_cols)
self.col_combo_box.setCurrentIndex(-1)
gb1_l.addWidget(self.col_combo_box)
gb1.setTitle(gb1_text)
l.addWidget(gb1, row_idx, 0, 1, 1)
gb2 = QGroupBox(_('Sort direction'), self)
gb2_l = QVBoxLayout()
gb2.setLayout(gb2_l)
self.button_ascend = QRadioButton(_('Ascending'), self)
gb2_l.addWidget(self.button_ascend)
self.button_ascend.setChecked(True)
self.button_descend = QRadioButton(_('Descending'), self)
gb2_l.addWidget(self.button_descend)
self.button_descend.setChecked(False)
l.addWidget(gb2, row_idx, 1, 1, 1)
row_idx += 1
l.setColumnStretch(0, 1)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum)
def apply_sort_filter(self, sort_filter):
field, is_ascending = sort_filter
self.col_combo_box.setCurrentText(field)
self.button_ascend.setChecked(is_ascending)
self.button_descend.setChecked(not is_ascending)
def _remove(self):
self.setParent(None)
self.deleteLater()
def isComplete(self):
'''returns True only if a field and direction are chosen'''
if self.col_combo_box.currentText() == '':
return False
return True
def get_sort_filter(self):
field = self.col_combo_box.currentText()
is_ascending = self.button_ascend.isChecked()
return (field, is_ascending)
class SortControlsContainer(QWidget):
def __init__(self, plugin_action, possible_cols):
self.plugin_action = plugin_action
self.gui = plugin_action.gui
self.db = self.gui.current_db
self.possible_cols = possible_cols
self._init_controls()
def _init_controls(self):
QWidget.__init__(self)
l = QVBoxLayout()
self.setLayout(l)
hl1 = QHBoxLayout()
clear_button = QPushButton(_('Clear'))
clear_button.setToolTip(_('Clear all filters'))
clear_button.setIcon(get_icon('clear_left.png'))
clear_button.clicked.connect(self.reset)
hl1.addWidget(clear_button)
hl1.addStretch(1)
hl1.addStretch(1)
add_button = QPushButton(_('Add Sort Filter'))
add_button.setToolTip(_('Add a column to sort by'))
add_button.setIcon(get_icon('plus.png'))
add_button.clicked.connect(self.add_control)
hl1.addWidget(add_button)
l.addLayout(hl1)
w = QWidget(self)
self.controls_layout = QVBoxLayout()
self.controls_layout.setSizeConstraint(self.controls_layout.SetMinAndMaxSize)
w.setLayout(self.controls_layout)
scroll = QScrollArea()
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setWidgetResizable(True)
scroll.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
scroll.setObjectName('myscrollarea')
scroll.setStyleSheet('#myscrollarea {background-color: transparent}')
scroll.setWidget(w)
l.addWidget(scroll)
self._add_control(sort_filter={})
def isComplete(self):
'''return True if all controls have fields and algorithms set'''
for idx in range(self.controls_layout.count()):
control = self.controls_layout.itemAt(idx).widget()
if not control.isComplete():
return False
return True
def _add_control(self, sort_filter=None):
control = SortControl(self.plugin_action, self.possible_cols)
if sort_filter:
control.apply_sort_filter(sort_filter)
self.controls_layout.addWidget(control)
def add_control(self):
if not self.isComplete():
error_dialog(
self,
_('Incomplete Sort Filter'),
_('You must complete the previous sort filter(s) to proceed.'),
show=True
)
return
self._add_control()
def reset(self, add_empty_control=True):
# remove controls in reverse order
for idx in reversed(range(self.controls_layout.count())):
control = self.controls_layout.itemAt(idx).widget()
control.setParent(None)
control.deleteLater()
if add_empty_control:
self._add_control()
def get_sort_filters(self):
all_filters = []
for idx in range(self.controls_layout.count()):
control = self.controls_layout.itemAt(idx).widget()
all_filters.append(control.get_sort_filter())
return all_filters
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.possible_cols = get_cols(self.db)
self._init_controls()
def _init_controls(self):
l = QVBoxLayout()
self.setLayout(l)
self.container = SortControlsContainer(self.plugin_action, self.possible_cols)
l.addWidget(self.container, 1)
self.resize(500, 600)
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
def load_settings(self, settings):
sort_filters = settings['sort_filters']
self.container.reset(add_empty_control=False)
for sort_filter in sort_filters:
self.container._add_control(sort_filter)
def save_settings(self):
settings = {'sort_filters': self.container.get_sort_filters()}
return settings
class SortAction(ChainAction):
name = 'Sort by field'
#_is_builtin = True
def run(self, gui, settings, chain):
sort_filters = settings['sort_filters']
gui.library_view.multisort(sort_filters)
def validate(self, settings):
gui = self.plugin_action.gui
db = gui.current_db
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
for sort_filter in settings['sort_filters']:
field, is_ascending = sort_filter
if not field:
return _('No field'), _('You must specify a field for all filters')
elif not field in get_cols(db):
return _('Field unavailabe'), _('Current library does not have a field called {}'.format(field))
return True
def config_widget(self):
return ConfigWidget
Last edited by capink; 01-07-2022 at 06:17 AM. |
|
|
|
|
|
#23 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,328
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Re-calc composite columns
Here is a module I use to recalc the data of a composite column — in my case, an if-then checking current_virtual_library_name() sometimes caches and doesn't change when switching VLs.
Code:
from calibre_plugins.action_chains.actions.base import ChainAction
class RefreshAction(ChainAction):
name = 'Refresh GUI'
def run(self, gui, settings, chain):
gui.current_db.data.refresh()
gui.library_view.model().resort()
Last edited by ownedbycats; 01-29-2022 at 09:28 PM. |
|
|
|
|
|
#24 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Swap title author
Code:
from collections import defaultdict
from calibre.ebooks.metadata import (
authors_to_string, authors_to_sort_string, check_isbn, string_to_authors, title_sort
)
from calibre_plugins.action_chains.actions.base import ChainAction
class SwapTitleAuthors(ChainAction):
name = 'Swap title and author'
support_scopes = True
def __init__(self, plugin_action):
ChainAction.__init__(self, plugin_action)
self.gui = plugin_action.gui
self.db = self.gui.current_db
def author_sort_from_authors(self, authors):
return self.db.new_api.author_sort_from_authors(authors, key_func=lambda x: x)
def book_lang(self, book_id):
try:
book_lang = self.db.new_api.field_for('languages', book_id)[0]
except:
book_lang = None
return book_lang
def swap_title_author(self, book_ids):
id_fields_maps = defaultdict(dict)
for book_id in book_ids:
old_title = self.db.new_api.field_for('title', book_id)
old_authors = self.db.new_api.field_for('authors', book_id)
new_authors = string_to_authors(old_title)
id_fields_maps['authors'][book_id] = new_authors
id_fields_maps['author_sort'][book_id] = self.author_sort_from_authors(new_authors)
new_title = authors_to_string(old_authors)
id_fields_maps['title'][book_id] = new_title
id_fields_maps['sort'][book_id] = title_sort(new_title, lang=self.book_lang(book_id))
for field, id_val_map in id_fields_maps.items():
self.db.new_api.set_field(field, id_val_map)
def run(self, gui, settings, chain):
book_ids = chain.scope().get_book_ids()
self.swap_title_author(book_ids)
Last edited by capink; 01-04-2022 at 04:11 PM. |
|
|
|
|
|
#25 |
|
Custom User Title
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 11,328
Karma: 79528341
Join Date: Oct 2018
Location: Canada
Device: Kobo Libra H2O, formerly Aura HD
|
Prompt for Action
Modified from 'Prompt for Confirmation' (thanks to capink for assistance) is "Prompt for Action,' to enable selected actions using a yes/no prompt.
Code:
from qt.core import QWidget, QVBoxLayout, QGroupBox, QTextEdit
from calibre.gui2 import question_dialog
from calibre_plugins.action_chains.actions.base import ChainAction
class ConfirmConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self._init_controls()
def _init_controls(self):
l = QVBoxLayout()
self.setLayout(l)
gb = QGroupBox('Confirm message')
gb_l = QVBoxLayout()
gb.setLayout(gb_l)
self.tb = QTextEdit()
self.tb.insertPlainText('Do you want to run the next action?')
gb_l.addWidget(self.tb)
l.addWidget(gb)
def load_settings(self, settings):
if settings:
self.tb.setText(settings['message'])
def save_settings(self):
settings = {}
settings['message'] = self.tb.toPlainText()
return settings
class ConfirmAction(ChainAction):
name = 'Prompt for Action'
def config_widget(self):
return ConfirmConfigWidget
def run(self, gui, settings, chain):
message = settings.get('message', 'Do you want to run the next action?')
if question_dialog(gui, _('Are you sure?'), message, show_copy_button=False):
chain.set_chain_vars({'prompt_action': 'Yes'})
Place the module action somewhere in the chain, obviously before the action you wish to prompt for, and then add the program: globals(prompt_action) text = Yes condition to the actions you wish to prompt for. I've attached a sample chain for demonstration. Last edited by capink; 01-30-2022 at 06:58 PM. Reason: Change import statement to qt.core |
|
|
|
|
|
#26 |
|
Chalut o/
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 486
Karma: 678910
Join Date: Dec 2017
Device: Kobo
|
Bulk Sort Key
The purpose of this module is to update the title and/or author sorting key, like the options in the Bulk Metadata dialog, options that are unavailable via Action Chains (I won't open the huge Bulk Metadata dialog just to check the same 2 options everytime => go on to make a non-interactive module)
Bonus: Normally translated into your language EDIT: If your have Quality Check 1.14.3 and above installed, you can use directly the same action in "Imported From Other Plugin > Quality Check > Run Quality Check fix" and select "Check and repair book size" Code:
import os, copy
try:
from qt.core import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
QGroupBox, QCheckBox)
except ImportError:
from PyQt5.Qt import (QApplication, Qt, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout,
QGroupBox, QCheckBox)
from calibre import prints
from calibre.constants import DEBUG
from calibre_plugins.action_chains.actions.base import ChainAction
class BulkSortKeyAction(ChainAction):
# the name of your action
name = 'Bulk Sort Key'
support_scopes = True
def run(self, gui, settings, chain):
cache = gui.current_db.new_api
self.ids = chain.scope().get_book_ids()
if not self.ids or len(self.ids) == 0:
return
if settings.get(KEY.SORT_TITLE, DEFAULT[KEY.SORT_TITLE]):
from calibre.ebooks.metadata import title_sort
lang_map = cache.all_field_for('languages', self.ids)
title_map = cache.all_field_for('title', self.ids)
def get_sort(book_id):
try:
lang = lang_map[book_id][0]
except (KeyError, IndexError, TypeError, AttributeError):
lang = None
return title_sort(title_map[book_id], lang=lang)
cache.set_field('sort', {bid:get_sort(bid) for bid in self.ids})
if settings.get(KEY.SORT_AUTHOR, DEFAULT[KEY.SORT_AUTHOR]):
author_sort_map = cache.author_sort_strings_for_books(self.ids)
cache.set_field('author_sort', {book_id: ' & '.join(author_sort_map[book_id]) for book_id in author_sort_map})
def config_widget(self):
return ConfigWidget
class KEY:
SORT_AUTHOR = 'sort_author'
SORT_TITLE = 'sort_title'
DEFAULT = {}
DEFAULT[KEY.SORT_AUTHOR] = True
DEFAULT[KEY.SORT_TITLE] = True
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)
gb_opts = QGroupBox(_("Options"), self)
l.addWidget(gb_opts)
l_opts = QVBoxLayout()
gb_opts.setLayout(l_opts)
self.chk_sort_author = QCheckBox(_("A&utomatically set author sort"))
self.chk_sort_author.setChecked(DEFAULT[KEY.SORT_AUTHOR])
self.chk_sort_author.setToolTip(_("This will cause the author sort field to be automatically updated\n"
" based on the authors field for each selected book. Note that if\n"
" you use the control above to set authors in bulk, the author sort\n"
" field is updated anyway, regardless of the value of this checkbox."))
l_opts.addWidget(self.chk_sort_author)
self.chk_sort_title = QCheckBox(_("Update &title sort"))
self.chk_sort_title.setChecked(DEFAULT[KEY.SORT_TITLE])
self.chk_sort_title.setToolTip(_("Update title sort based on the current title. This will be applied only after other changes to title."))
l_opts.addWidget(self.chk_sort_title)
l.addStretch(1)
self.setMinimumSize(300,100)
def load_settings(self, settings):
if not settings:
settings = copy.deepcopy(DEFAULT)
self.chk_sort_author.setChecked(settings.get(KEY.SORT_AUTHOR, DEFAULT[KEY.SORT_AUTHOR]))
self.chk_sort_title.setChecked(settings.get(KEY.SORT_TITLE, DEFAULT[KEY.SORT_TITLE]))
def save_settings(self):
settings = {}
settings[KEY.SORT_AUTHOR] = self.chk_sort_author.isChecked()
settings[KEY.SORT_TITLE] = self.chk_sort_title.isChecked()
return settings
Last edited by un_pogaz; 10-05-2025 at 03:18 AM. |
|
|
|
|
|
#27 |
|
Member
![]() Posts: 11
Karma: 10
Join Date: Dec 2021
Device: Kobo Libra 2
|
Thank you for the fix.
Ciao MP |
|
|
|
|
|
#28 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
Download metadata action
Code:
#!/usr/bin/env python
# ~*~ coding: utf-8 ~*~
__license__ = 'GPL v3'
__copyright__ = '2022, Ahmed Zaki <azaki00.dev@gmail.com>'
__docformat__ = 'restructuredtext en'
from qt.core import (QApplication, Qt, QWidget, QVBoxLayout, QCheckBox,
QGroupBox, QRadioButton, QAction)
import copy
import types
from functools import partial
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import Dispatcher, error_dialog
from calibre.ptempfile import PersistentTemporaryFile
from calibre.gui2.metadata.bulk_download import Job, download
from calibre.gui2.actions.edit_metadata import EditMetadataAction
from polyglot.builtins import iteritems
from calibre_plugins.action_chains.actions.base import ChainAction
from calibre_plugins.action_chains.common_utils import responsive_wait, responsive_wait_until
def unfinished_job_ids(gui):
return set([job.id for job in gui.job_manager.unfinished_jobs()])
class ModifiedEditMetadataAction(EditMetadataAction):
name = 'Modified Edit Metadata'
action_spec = (_('Modified Edit metadata'), 'edit_input.png', _('Change the title/author/cover etc. of books'), _(''))
action_type = 'current'
action_add_menu = True
def __init__(self, parent, site_customization):
EditMetadataAction.__init__(self, parent, site_customization)
self.do_genesis()
@property
def unique_name(self):
bn = self.__class__.__name__
return 'Interface Action: %s (%s)'%(bn, self.name)
def genesis(self):
pass
def location_selected(self, loc):
pass
def library_changed(self):
pass
def shutting_down(self):
pass
def metadata_downloaded(self, job):
if job.failed:
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
return
from calibre.gui2.metadata.bulk_download import get_job_details
(aborted, id_map, tdir, log_file, failed_ids, failed_covers, all_failed,
det_msg, lm_map) = get_job_details(job)
if aborted:
return self.cleanup_bulk_download(tdir)
if all_failed:
num = len(failed_ids | failed_covers)
self.cleanup_bulk_download(tdir)
return error_dialog(self.gui, _('Download failed'), ngettext(
'Failed to download metadata or cover for the selected book.',
'Failed to download metadata or covers for any of the {} books.', num
).format(num), det_msg=det_msg, show=True)
self.gui.status_bar.show_message(_('Metadata download completed'), 3000)
msg = '<p>' + ngettext(
'Finished downloading metadata for the selected book.',
'Finished downloading metadata for <b>{} books</b>.', len(id_map)).format(len(id_map)) + ' ' + \
_('Proceed with updating the metadata in your library?')
show_copy_button = False
checkbox_msg = None
if failed_ids or failed_covers:
show_copy_button = True
num = len(failed_ids.union(failed_covers))
msg += '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%num
checkbox_msg = _('Show the &failed books in the main book list '
'after updating metadata')
if getattr(job, 'metadata_and_covers', None) == (False, True):
# Only covers, remove failed cover downloads from id_map
for book_id in failed_covers:
if hasattr(id_map, 'discard'):
id_map.discard(book_id)
payload = (id_map, tdir, log_file, lm_map,
failed_ids.union(failed_covers))
if self.do_review:
QApplication.setOverrideCursor(Qt.ArrowCursor)
try:
self.apply_downloaded_metadata(True, payload, self.restrict_to_failed)
finally:
QApplication.restoreOverrideCursor()
else:
self.apply_downloaded_metadata(False, payload, self.restrict_to_failed)
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)
opt_gb = QGroupBox(_('Options'))
opt_gb_l = QVBoxLayout()
opt_gb.setLayout(opt_gb_l)
l.addWidget(opt_gb)
self.metadata_opt = QRadioButton(_('Download Metadata'))
self.covers_opt = QRadioButton(_('Download Covers'))
self.both_opt = QRadioButton(_('Download Both'))
self.both_opt.setChecked(True)
opt_gb_l.addWidget(self.metadata_opt)
opt_gb_l.addWidget(self.covers_opt)
opt_gb_l.addWidget(self.both_opt)
self.review_chk = QCheckBox(_('Review downloaded metadata before applying them'))
self.wait_chk = QCheckBox(_('Wait for metadata download jobs to finish'))
self.wait_chk.setToolTip(_('Check this if this action in not the last action in the chain.'))
l.addWidget(self.review_chk)
l.addWidget(self.wait_chk)
l.addStretch(1)
self.setMinimumSize(500,300)
def load_settings(self, settings):
if settings:
self.metadata_opt.setChecked(settings.get('download_metadata', False))
self.covers_opt.setChecked(settings.get('download_covers', False))
self.both_opt.setChecked(settings.get('download_both', True))
self.review_chk.setChecked(settings.get('review', True))
self.wait_chk.setChecked(settings.get('wait_jobs', False))
def save_settings(self):
settings = {}
settings['download_metadata'] = self.metadata_opt.isChecked()
settings['download_covers'] = self.covers_opt.isChecked()
settings['download_both'] = self.both_opt.isChecked()
settings['review'] = self.review_chk.isChecked()
settings['wait_jobs'] = self.wait_chk.isChecked()
return settings
class DownloadMetadata(ChainAction):
name = 'Download Metadata'
support_scopes = True
def run(self, gui, settings, chain):
identify = settings.get('download_metadata') or settings.get('download_both', True)
covers = settings.get('download_covers') or settings.get('download_both', True)
wait_jobs = settings.get('wait_jobs', False)
ensure_fields = None
edit_metadata = ModifiedEditMetadataAction(gui, '')
edit_metadata.do_review = settings.get('review', True)
edit_metadata.restrict_to_failed = settings.get('restrict_to_failed', True)
callback = Dispatcher(edit_metadata.metadata_downloaded)
ids = chain.scope().get_book_ids()
if len(ids) == 0:
return error_dialog(gui, _('Cannot download metadata'),
_('No books selected'), show=True)
jobs_before_ids = unfinished_job_ids(gui)
tf = PersistentTemporaryFile('_metadata_bulk.log')
tf.close()
job = Job('metadata bulk download',
ngettext(
'Download metadata for one book',
'Download metadata for {} books', len(ids)).format(len(ids)),
download, (ids, tf.name, gui.current_db, identify, covers,
ensure_fields), {}, callback)
job.metadata_and_covers = (identify, covers)
job.download_debug_log = tf.name
gui.job_manager.run_threaded_job(job)
gui.status_bar.show_message(_('Metadata download started'), 3000)
if wait_jobs:
# wait for jobs spawned by action to kick in
responsive_wait(1)
# save ids of jobs started after running the action
ids_jobs_by_action = unfinished_job_ids(gui).difference(jobs_before_ids)
# wait for jobs to finish
responsive_wait_until(lambda: ids_jobs_by_action.intersection(unfinished_job_ids(gui)) == set())
def validate(self, settings):
#if not settings:
#return (_('Settings Error'), _('You must configure this action before running it'))
return True
def config_widget(self):
return ConfigWidget
Last edited by capink; 09-29-2022 at 04:52 PM. |
|
|
|
|
|
#29 |
|
Wizard
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,216
Karma: 1995558
Join Date: Aug 2015
Device: Kindle
|
This is an action to merge duplicates produced by the Find Duplicates plugin. You can merge the books using one of two options:
Note: Duplicates will be merged into the first book in each group. You can control the order of books in duplicate groups using the Find Duplicates advanced mode (group sorting option) Warning: This action can destroy your library as it merges book irreversibly. You use it at your risk, without any guarantee. You must always backup your library before using the action, and you must examine the results carefully until you are satisfied. If you brick your library without having backed it up, and without having examined the result meticulously, you bear full responsibility. Don't come here to complain or ask for support. You have been warned. Code:
import json
from functools import partial
from qt.core import (QApplication, Qt, QWidget, QVBoxLayout, QGroupBox,
QRadioButton, QCheckBox)
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import choose_files, error_dialog, question_dialog
from calibre_plugins.action_chains.common_utils import DoubleProgressDialog, truncate
from calibre_plugins.action_chains.actions.base import ChainAction
#from calibre.gui2.actions.edit_metadata import EditMetadataAction
class MergeConfigWidget(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)
groupbox = QGroupBox(_('Duplicate source'))
groupbox_layout = QVBoxLayout()
groupbox.setLayout(groupbox_layout)
l.addWidget(groupbox)
from_plugin_opt = self.from_plugin_opt = QRadioButton(_('From Find Duplicates Results'))
from_file_opt = self.from_file_opt = QRadioButton(_('From Find Duplicates Exported Json'))
groupbox_layout.addWidget(from_plugin_opt)
groupbox_layout.addWidget(from_file_opt)
from_plugin_opt.setChecked(True)
self.confirm_chk = QCheckBox(_('Ask for confirmation before merging'))
self.confirm_chk.setChecked(True)
l.addWidget(self.confirm_chk)
l.addStretch(1)
self.setMinimumSize(300,300)
def load_settings(self, settings):
if settings:
if settings['opt'] == 'from_plugin':
self.from_plugin_opt.setChecked(True)
elif settings['opt'] == 'from_file':
self.from_file_opt.setChecked(True)
self.confirm_chk.setChecked(settings.get('confirm', True))
def save_settings(self):
settings = {}
if self.from_plugin_opt.isChecked():
settings['opt'] = 'from_plugin'
elif self.from_file_opt:
settings['opt'] = 'from_file'
settings['confirm'] = self.confirm_chk.isChecked()
return settings
class MergeDuplicates(ChainAction):
name = 'Merge Duplicates'
def merge_books(self, dest_id, src_ids):
gui = self.plugin_action.gui
db = gui.current_db
em = gui.iactions['Edit Metadata']
em.add_formats(dest_id, em.formats_for_ids([dest_id]+src_ids))
em.merge_metadata(dest_id, src_ids)
#em.delete_books_after_merge(src_ids)
# delete books now and will notify db at the end
db.new_api.remove_books(src_ids)
self.deleted_ids.extend(src_ids)
def notify_db_deleted_books(self):
gui = self.plugin_action.gui
gui.library_view.model().ids_deleted(self.deleted_ids)
def pd_callback(self, db, duplicates, pbar):
entangled_groups = set()
for book_id, group_ids in duplicates['entangled_groups_for_book']:
for group_id in group_ids:
entangled_groups.add(group_id)
pbar.update_overall(len(duplicates['books_for_group']))
self.deleted_ids = []
for group_id, book_ids in duplicates['books_for_group'].items():
if group_id in entangled_groups:
msg = _('Group_id () in entangled groups. skipping'.format(group_id))
if DEBUG:
prints('Action Chains: '+msg)
else:
dest_id = book_ids[0]
src_ids = book_ids[1:]
title = db.new_api.field_for('title', dest_id)
title = truncate(title, 30)
msg = _('Group ({}): merging into: {}'.format(group_id, title))
self.merge_books(dest_id, src_ids)
pbar.update_progress(1, msg)
def run(self, gui, settings, chain):
if settings.get('confirm', True):
message = _('The following action will merge books in your library and data will be permenatly lost. '
'Are you sure you want to proceed?')
if not question_dialog(gui, _('Are you sure?'), message, show_copy_button=False):
return
if settings['opt'] == 'from_file':
filters=[(_('Settings'), ['json'])]
json_file = choose_files(gui, 'Choose duplicates json file',
_('Select duplicates json file'), filters=filters,
select_only_single_file=True)
if not json_file:
return
json_file = json_file[0]
with open(json_file) as f:
duplicates = json.load(f)
if duplicates['library_uuid'] != gui.current_db.library_id:
return error_dialog(gui,
'uuid error',
'Library uuid in duplicates json does not match current library uuid.\n'
'Quitting without merging',
show=True)
elif settings['opt'] == 'from_plugin':
duplicates = {}
find_duplicates_plugin = gui.iactions.get('Find Duplicates')
if not find_duplicates_plugin:
return
duplicate_finder = find_duplicates_plugin.duplicate_finder
# if not hasattr(duplicate_finder, '_groups_for_book_map'):
# return
if not duplicate_finder.has_results():
return
entangled_books = {}
for book_id, groups in duplicate_finder._groups_for_book_map.items():
if len(groups) > 1:
entangled_books[book_id] = list(groups)
duplicates['books_for_group'] = duplicate_finder._books_for_group_map
duplicates['entangled_groups_for_book'] = entangled_books
callback = partial(self.pd_callback, gui.current_db, duplicates)
pd = DoubleProgressDialog(1, callback, gui, window_title=_('Merging ...'))
gui.tags_view.blockSignals(True)
QApplication.setOverrideCursor(Qt.ArrowCursor)
try:
pd.exec_()
pd.thread = None
if pd.error is not None:
return error_dialog(gui, _('Failed'),
pd.error[0], det_msg=pd.error[1],
show=True)
finally:
self.notify_db_deleted_books()
QApplication.restoreOverrideCursor()
gui.tags_view.recount()
def config_widget(self):
return MergeConfigWidget
def default_settings(self):
return {'opt': 'from_plugin'}
Last edited by capink; 09-22-2022 at 10:17 PM. |
|
|
|
|
|
#30 | |
|
want to learn what I want
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() Posts: 1,679
Karma: 7908443
Join Date: Sep 2020
Device: none
|
Quote:
I'm thinking it could be useful to have different shortcut keys for quick one-click metadata downloads like, one for Amazon, another for Goodreads, another one for BN... and so on. Or perhaps this could be another action script. Last edited by Comfy.n; 09-25-2022 at 05:15 AM. |
|
|
|
|
![]() |
| Thread Tools | Search this Thread |
|
Similar Threads
|
||||
| Thread | Thread Starter | Forum | Replies | Last Post |
| [GUI Plugin] Action Chains | capink | Plugins | 1555 | Yesterday 06:48 AM |
| Book Scanning tool chains | tomsem | Workshop | 17 | 12-03-2023 09:19 AM |
| Mystery and Crime Thorne, Guy: Chance in Chains (1914); v1 | Pulpmeister | Kindle Books | 0 | 11-25-2018 09:09 PM |
| Mystery and Crime Thorne, Guy: Chance in Chains (1914); v1 | Pulpmeister | ePub Books | 0 | 11-25-2018 09:08 PM |
| Could this be the last year for the big chains? | Connallmac | News | 66 | 01-07-2011 04:11 PM |