Here is a new custom "Add Books" action. It is based on calibre's add books action and can be configured to run in one of the following modes:
- Add books from single directory.
- Add from folders and sub-folders (single book per directory).
- Add from folders and sub-folders (multiple books per directory).
When running the the first option, you can make it add books from a predefined directory, which makes it suitable to
create your auto-add chain (if used with the timer event). Note however, that unlike calibre's auto-add, this will launch a progress dialog that will block the gui until all books are added.
Also the action has an option to select the newly added books, so that other actions in the chain can work on them.
Code:
import os
from functools import partial
import copy
import shutil
from PyQt5.Qt import (Qt, QApplication, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QRadioButton, QCheckBox, QIcon, QGroupBox, QLabel, QComboBox, QToolButton)
from calibre import prints
from calibre.constants import DEBUG
from calibre.gui2 import choose_dir, choose_files, gprefs
from calibre.gui2.add import Adder
from calibre.gui2.auto_add import allowed_formats
from calibre.gui2.actions.add import AddAction, get_filters
from calibre.utils.date import now
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.database import get_books_modified_since
class ModifiedAdder(Adder):
def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None, pool=None, list_of_archives=False, duplicate_opt='runtime'):
self.duplicate_opt = duplicate_opt
Adder.__init__(self, source, single_book_per_directory, db, parent, callback, pool, list_of_archives)
self.block_thread()
def block_thread(self):
while not self.abort_scan:
QApplication.processEvents()
return
def process_duplicates(self):
if self.duplicate_opt == 'runtime':
return Adder.process_duplicates(self)
elif self.duplicate_opt == 'add':
duplicates = copy.copy(self.duplicates)
if duplicates:
self.do_one = self.process_duplicate
self.duplicates_to_process = iter(duplicates)
self.pd.title = _('Adding duplicates')
self.pd.msg = ''
self.pd.max, self.pd.value = len(duplicates), 0
self.do_one_signal.emit()
return
elif self.duplicate_opt == 'remove':
duplicates = ()
self.finish()
class ModifiedAddAction(AddAction):
def __init__(self, gui):
self.gui = gui
self.allowed = allowed_formats()
self.duplicate_opt = 'runtime'
def do_add_recursive(self, root, single, list_of_archives=False):
from calibre.gui2.add import Adder
adder = ModifiedAdder(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(), duplicate_opt=self.duplicate_opt)
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
ModifiedAdder(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(), duplicate_opt=self.duplicate_opt)
def add_books(self, books, *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)]
if not books:
return
self._add_books(books, to_device)
def add_recursive(self, single, root):
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 is_filename_allowed(self, filename):
ext = os.path.splitext(filename)[1][1:].lower()
allowed = ext in self.allowed
return allowed
def books_from_path(self, path):
files = [os.path.join(path, x) for x in os.listdir(path) if
# Firefox creates 0 byte placeholder files when downloading
os.stat(os.path.join(path, x)).st_size > 0 and
# Must be a file
os.path.isfile(os.path.join(path, x)) and
# Must have read and write permissions
os.access(os.path.join(path, x), os.R_OK|os.W_OK) and
# Must be a known ebook file type
self.is_filename_allowed(x)
]
return files
def clean_path(self, path):
files = [os.path.join(path, x) for x in os.listdir(path) if
os.path.isfile(os.path.join(path, x)) and
os.access(os.path.join(path, x), os.R_OK|os.W_OK)]
dirs = [os.path.join(path, x) for x in os.listdir(path) if
os.path.isdir(os.path.join(path, x)) and
os.access(os.path.join(path, x), os.R_OK|os.W_OK)]
for f in files:
os.remove(f)
for d in dirs:
shutil.rmtree(d)
class ConfigWidget(QWidget):
def __init__(self, plugin_action):
QWidget.__init__(self)
self.gui = plugin_action.gui
self.db = self.gui.current_db
self._init_controls()
def _init_controls(self):
l = QVBoxLayout()
self.setLayout(l)
actions_group_box = QGroupBox('Actions')
actions_group_box_l = QVBoxLayout()
actions_group_box.setLayout(actions_group_box_l)
l.addWidget(actions_group_box)
add_books_opt = self.add_books_opt = QRadioButton('Add books from a single directory')
self.add_books_opt.setChecked(True)
actions_group_box_l.addWidget(self.add_books_opt)
recursive_single_opt = self.recursive_single_opt = QRadioButton('Add from folders and sub-folders (Single book per directory)')
actions_group_box_l.addWidget(self.recursive_single_opt)
recursive_multiple_opt = self.recursive_multiple_opt = QRadioButton('Add from folders and sub-folders (Multiple books per directory)')
actions_group_box_l.addWidget(self.recursive_multiple_opt)
for opt in [add_books_opt, recursive_single_opt, recursive_multiple_opt]:
opt.toggled.connect(self._action_opt_toggled)
location_group_box = QGroupBox(_('Books location'))
location_group_box_l = QVBoxLayout()
location_group_box.setLayout(location_group_box_l)
l.addWidget(location_group_box)
self.runtime_opt = QRadioButton('Ask at runtime')
self.runtime_opt.setChecked(True)
location_group_box_l.addWidget(self.runtime_opt)
self.predefined_opt = QRadioButton('Add from location defined below')
location_group_box_l.addWidget(self.predefined_opt)
hl1 = QHBoxLayout()
location_group_box_l.addLayout(hl1)
self.file_combo = DragDropComboBox(self, drop_mode='file')
hl1.addWidget(self.file_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)
duplicate_group_box = QGroupBox(_('What to do with duplicate books'))
duplicate_group_box_l = QVBoxLayout()
duplicate_group_box.setLayout(duplicate_group_box_l)
l.addWidget(duplicate_group_box)
self.duplicate_options = {
'runtime': _('Ask at runtime'),
'add': _('Add duplicates'),
'remove': _('Remove duplicates')
}
self.duplicates_combo = QComboBox()
self.duplicates_combo.addItems(list(self.duplicate_options.values()))
duplicate_group_box_l.addWidget(self.duplicates_combo)
options_group_box = QGroupBox(_('Options'))
options_group_box_l = QVBoxLayout()
options_group_box.setLayout(options_group_box_l)
l.addWidget(options_group_box)
self.delete_books_chk = QCheckBox(_('Delete books after adding'))
options_group_box_l.addWidget(self.delete_books_chk)
# self.warning_1 = QLabel(_('<b>Warning: </b> This will delete all files and directories in directory'))
# self.warning_1.setWordWrap(True)
# options_group_box_l.addWidget(self.warning_1)
self.select_added_chk = QCheckBox(_('Select added books'))
options_group_box_l.addWidget(self.select_added_chk)
# self.hide_progress_chk = QCheckBox('When adding from predefined path, hide progress bar')
# options_group_box_l.addWidget(self.hide_progress_chk)
l.addStretch(1)
self.setMinimumSize(400,500)
self._action_opt_toggled()
def _action_opt_toggled(self):
self.delete_books_chk.setEnabled(self.add_books_opt.isChecked())
if not self.delete_books_chk.isEnabled():
self.delete_books_chk.setChecked(False)
self.predefined_opt.setEnabled(self.add_books_opt.isChecked())
if not self.predefined_opt.isEnabled():
self.runtime_opt.setChecked(True)
def _choose_path(self):
root = choose_dir(self.gui, 'recursive book import root dir dialog',
_('Select root folder'))
if not root:
return
self.block_events = True
existing_index = self.file_combo.findText(root, Qt.MatchExactly)
if existing_index >= 0:
self.file_combo.setCurrentIndex(existing_index)
else:
self.file_combo.insertItem(0, root)
self.file_combo.setCurrentIndex(0)
self.block_events = False
def load_settings(self, settings):
if settings:
if settings['action'] == 'add_books':
self.add_books_opt.setChecked(True)
else:
if settings['is_single']:
self.recursive_single_opt.setChecked(True)
else:
self.recursive_multiple_opt.setChecked(True)
if settings['path_opt'] == 'runtime':
self.runtime_opt.setChecked(True)
else:
self.predefined_opt.setChecked(True)
self.file_combo.populate_items(settings.get('file_list', []), settings['path_to_books'])
self.duplicates_combo.setCurrentText(self.duplicate_options[settings['duplicate_opt']])
self.delete_books_chk.setChecked(settings['delete_books'])
self.select_added_chk.setChecked(settings['select_added_books'])
# self.hide_progress_chk.setChecked(settings['hide_progress'])
self._action_opt_toggled()
def save_settings(self):
settings = {}
if self.add_books_opt.isChecked():
settings['action'] = 'add_books'
elif self.recursive_single_opt.isChecked():
settings['action'] = 'recursive_add'
settings['is_single'] = True
elif self.recursive_multiple_opt.isChecked():
settings['action'] = 'recursive_add'
settings['is_single'] = False
if self.runtime_opt.isChecked():
settings['path_opt'] = 'runtime'
else:
settings['path_opt'] = 'predefined'
settings['path_to_books'] = self.file_combo.currentText()
settings['file_list'] = self.file_combo.get_items_list()
settings['duplicate_opt'] = {v: k for k, v in self.duplicate_options.items()}[self.duplicates_combo.currentText()]
settings['delete_books'] = self.delete_books_chk.isChecked()
settings['select_added_books'] = self.select_added_chk.isChecked()
# settings['hide_progress'] = self.hide_progress_chk.isChecked()
return settings
class ChainsAddAction(ChainAction):
name = 'Add Books'
def __init__(self, plugin_action):
ChainAction.__init__(self, plugin_action)
#self.add_action = ModifiedAddAction(plugin_action.gui)
def run(self, gui, settings, chain):
add_action = ModifiedAddAction(self.plugin_action.gui)
start_time = now()
action = settings['action']
path_opt = settings['path_opt']
path_to_books = settings.get('path_to_books', '')
duplicate_opt = settings['duplicate_opt']
add_action.duplicate_opt = duplicate_opt
# if path_opt == 'predefined' and settings['hide_progress']:
# add_action.hide_progress_dialog = True
if action == 'add_books':
if path_opt == 'runtime':
filters = get_filters()
books = choose_files(gui, 'add books dialog dir',
_('Select books'), filters=filters)
else:
books = add_action.books_from_path(path_to_books)
add_action.add_books(books)
if settings['delete_books']:
for book in books:
os.remove(book)
elif action == 'recursive_add':
is_single = settings['is_single']
if path_opt == 'runtime':
root = choose_dir(gui, 'recursive book import root dir dialog',
_('Select root folder'))
else:
root = path_to_books
add_action.add_recursive(is_single, root)
# if settings['delete_books']:
# add_action.clean_path(path_to_books)
if settings['select_added_books']:
added_ids = get_books_modified_since(gui.current_db, start_time)
gui.library_view.select_rows(added_ids)
def validate(self, settings):
if not settings:
return (_('Settings Error'), _('You must configure this action before running it'))
if settings['path_opt'] == 'predefined':
path_to_books = settings['path_to_books']
if not path_to_books:
return (_('Path Error'), _('You must choose a path to add books from'))
if not os.path.isdir(path_to_books):
return (_('Path Error'), _('Path entered ({}) is not a valid directory'.format(path_to_books)))
if not os.access(path_to_books, os.R_OK|os.W_OK):
return (_('Path Error'), _('Path entered ({}) does not have read/write access'.format(path_to_books)))
if os.path.normpath(path_to_books) == os.path.normpath(gprefs['auto_add_path']):
return (_('Path Error'), _('Predefined path ({}) cannot be the same as calibre auto add path'.format(path_to_books)))
return True
def config_widget(self):
return ConfigWidget