diff --git a/manual/gui.rst b/manual/gui.rst index 5582405bcf..b77bde0cc3 100755 --- a/manual/gui.rst +++ b/manual/gui.rst @@ -53,6 +53,8 @@ Add books 3. **Add books from directories, including sub-directories (Multiple books per directory, assumes every ebook file is a different book)**: Allows you to choose a directory. The directory and all its sub-directories are scanned recursively and any ebooks found are added to the library. |app| assumes that each directory contains many books. All ebook files with the same name in a directory are assumed to be the same book in different formats. Ebooks with different names are added as different books. + 4. **Add multiple books from archive (ZIP/RAR)**: Allows you to add multiple ebooks that are stored inside a single ZIP or RAR file. It is a convenient shortcut that avoids having to first unzip the archive and then add the books via one of the above two options. + 4. **Add empty book. (Book Entry with no formats)**: Allows you to create a blank book record. This can be used to then manually fill out the information about a book that you may not have yet in your collection. 5. **Add from ISBN**: Allows you to add one or more books by entering their ISBNs. diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index cbe4e1d1f6..3e3c48b8b8 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -66,6 +66,12 @@ class AddAction(InterfaceAction): 'sub directories (Multiple books per directory, assumes every ' 'ebook file is a different book)')).triggered.connect( self.add_recursive_multiple) + arm = self.add_archive_menu = self.add_menu.addMenu(_('Add multiple books from archive (ZIP/RAR)')) + self.create_menu_action(arm, 'recursive-single-archive', _( + 'One book per directory in the archive')).triggered.connect(partial(self.add_archive, True)) + self.create_menu_action(arm, 'recursive-multiple-archive', _( + 'Multiple books per directory in the archive')).triggered.connect(partial(self.add_archive, False)) + self.add_menu.addSeparator() self.add_menu.addSeparator() ma('add-empty', _('Add Empty book. (Book entry with no formats)'), shortcut='Shift+Ctrl+E').triggered.connect(self.add_empty) @@ -135,11 +141,21 @@ class AddAction(InterfaceAction): if current_idx.isValid(): view.model().current_changed(current_idx, current_idx) + 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=True) + if paths: + self.do_add_recursive(paths[0], single) + def add_recursive(self, single): root = choose_dir(self.gui, 'recursive book import root dir dialog', - 'Select root folder') + _('Select root folder')) if not root: return + self.do_add_recursive(root, single) + + def do_add_recursive(self, root, single): from calibre.gui2.add import Adder self._adder = Adder(self.gui, self.gui.library_view.model().db, diff --git a/src/calibre/gui2/add.py b/src/calibre/gui2/add.py index d9e075b179..554b8d4a36 100644 --- a/src/calibre/gui2/add.py +++ b/src/calibre/gui2/add.py @@ -7,6 +7,7 @@ from functools import partial from PyQt4.Qt import QThread, QObject, Qt, QProgressDialog, pyqtSignal, QTimer +from calibre.ptempfile import PersistentTemporaryDirectory from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2 import (error_dialog, info_dialog, gprefs, warning_dialog, available_width) @@ -54,10 +55,11 @@ class RecursiveFind(QThread): # {{{ update = pyqtSignal(object) found = pyqtSignal(object) - def __init__(self, parent, db, root, single): + def __init__(self, parent, db, root, single, tdir=None): QThread.__init__(self, parent) self.db = db self.path = root + self.tdir = tdir self.single_book_per_directory = single self.canceled = False @@ -72,7 +74,34 @@ class RecursiveFind(QThread): # {{{ self.books += list(self.db.find_books_in_directory(dirpath[0], self.single_book_per_directory)) + def extract(self): + if self.path.lower().endswith('.zip'): + from calibre.utils.zipfile import ZipFile + try: + with ZipFile(self.path) as zf: + zf.extractall(self.tdir) + except Exception: + prints('Corrupt ZIP file, trying to use local headers') + from calibre.utils.localunzip import extractall + extractall(self.path, self.tdir) + elif self.path.lower().endswith('.rar'): + from calibre.utils.unrar import extract + extract(self.path, self.tdir) + else: + raise ValueError('Can only process ZIP or RAR archives') + def run(self): + if self.tdir is not None: + try: + self.extract() + except Exception as err: + import traceback + traceback.print_exc() + msg = as_unicode(err) + self.found.emit(msg) + return + self.path = self.tdir + root = os.path.abspath(self.path) try: self.walk(root) @@ -263,12 +292,16 @@ class Adder(QObject): # {{{ self.pd.canceled_signal.connect(self.canceled) def add_recursive(self, root, single=True): - self.path = root + if os.path.exists(root) and os.path.isfile(root) and root.lower().rpartition('.')[-1] in {'zip', 'rar'}: + self.path = tdir = PersistentTemporaryDirectory('_arcv_') + else: + self.path = root + tdir = None self.pd.set_msg(_('Searching in all sub-directories...')) self.pd.set_min(0) self.pd.set_max(0) self.pd.value = 0 - self.rfind = RecursiveFind(self, self.db, root, single) + self.rfind = RecursiveFind(self, self.db, root, single, tdir=tdir) self.rfind.update.connect(self.pd.set_msg, type=Qt.QueuedConnection) self.rfind.found.connect(self.add, type=Qt.QueuedConnection) self.rfind.start()