Add Books: Allow adding books from multiple ZIP/RAR archives, each containing many books, by right clicking on the Add Books and choosing "Add from Archive"

This commit is contained in:
Kovid Goyal 2014-11-12 14:08:49 +05:30
parent 99d36ca88a
commit 7226d2c639
3 changed files with 54 additions and 49 deletions

View File

@ -51,7 +51,7 @@ 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. calibre 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. 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. calibre 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 multiple books from archive (ZIP/RAR)**: Allows you to add multiple ebooks that are stored inside the selected ZIP or RAR files. 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.
5. **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 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.

View File

@ -142,9 +142,9 @@ class AddAction(InterfaceAction):
def add_archive(self, single): def add_archive(self, single):
paths = choose_files( paths = choose_files(
self.gui, 'recursive-archive-add', _('Choose archive file'), self.gui, 'recursive-archive-add', _('Choose archive file'),
filters=[(_('Archives'), ('zip', 'rar'))], all_files=False, select_only_single_file=True) filters=[(_('Archives'), ('zip', 'rar'))], all_files=False, select_only_single_file=False)
if paths: if paths:
self.do_add_recursive(paths[0], single) self.do_add_recursive(paths, single, list_of_archives=True)
def add_recursive(self, single): def add_recursive(self, single):
root = choose_dir(self.gui, 'recursive book import root dir dialog', root = choose_dir(self.gui, 'recursive book import root dir dialog',
@ -157,9 +157,9 @@ class AddAction(InterfaceAction):
'Cannot add books from the folder: %s as it contains the currently opened calibre library') % root, show=True) 'Cannot add books from the folder: %s as it contains the currently opened calibre library') % root, show=True)
self.do_add_recursive(root, single) self.do_add_recursive(root, single)
def do_add_recursive(self, root, single): def do_add_recursive(self, root, single, list_of_archives=False):
from calibre.gui2.add2 import Adder from calibre.gui2.add2 import Adder
Adder(root, single_book_per_directory=single, db=self.gui.current_db, 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()) callback=self._files_added, parent=self.gui, pool=self.gui.spare_pool())
def add_recursive_single(self, *args): def add_recursive_single(self, *args):

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2014, Kovid Goyal <kovid at kovidgoyal.net>'
import shutil, os, weakref, traceback import shutil, os, weakref, traceback, tempfile
from threading import Thread from threading import Thread
from collections import OrderedDict from collections import OrderedDict
from Queue import Empty from Queue import Empty
@ -33,21 +33,21 @@ def validate_source(source, parent=None): # {{{
error_dialog(parent, _('Cannot add books'), _( error_dialog(parent, _('Cannot add books'), _(
'The path %s does not exist') % source, show=True) 'The path %s does not exist') % source, show=True)
return False return False
if os.path.isdir(source): if not os.access(source, os.X_OK|os.R_OK):
if not os.access(source, os.X_OK|os.R_OK): error_dialog(parent, _('Cannot add books'), _(
error_dialog(parent, _('Cannot add books'), _( 'You do not have permission to read %s') % source, show=True)
'You do not have permission to read %s') % source, show=True) return False
return False else:
else: ok = False
if not os.access(source, os.R_OK): for path in source:
error_dialog(parent, _('Cannot add books'), _( if os.access(path, os.R_OK):
'You do not have permission to read %s') % source, show=True) ok = True
return False break
if not source.lower().rpartition(os.extsep)[-1] in {'zip', 'rar'}: if not ok:
error_dialog(parent, _('Cannot add books'), _( error_dialog(parent, _('Cannot add books'), _(
'The file %s is not a recognized archive format') % source, show=True) 'You do not have permission to read any of the selected files'),
return False det_msg='\n'.join(source), show=True)
return False
return True return True
# }}} # }}}
@ -55,11 +55,12 @@ class Adder(QObject):
do_one_signal = pyqtSignal() do_one_signal = pyqtSignal()
def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None, pool=None): def __init__(self, source, single_book_per_directory=True, db=None, parent=None, callback=None, pool=None, list_of_archives=False):
if not validate_source(source, parent): if not validate_source(source, parent):
return return
QObject.__init__(self, parent) QObject.__init__(self, parent)
self.single_book_per_directory = single_book_per_directory self.single_book_per_directory = single_book_per_directory
self.list_of_archives = list_of_archives
self.callback = callback self.callback = callback
self.add_formats_to_existing = prefs['add_formats_to_existing'] self.add_formats_to_existing = prefs['add_formats_to_existing']
self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection) self.do_one_signal.connect(self.tick, type=Qt.QueuedConnection)
@ -111,26 +112,46 @@ class Adder(QObject):
self.do_one() self.do_one()
# Filesystem scan {{{ # Filesystem scan {{{
def scan(self): def scan(self):
def find_files(root):
for dirpath, dirnames, filenames in os.walk(root):
for files in find_books_in_directory(dirpath, self.single_book_per_directory):
if self.abort_scan:
return
if files:
self.file_groups[len(self.file_groups)] = files
def extract(source):
tdir = tempfile.mkdtemp(suffix='_archive', dir=self.tdir)
if source.lower().endswith('.zip'):
from calibre.utils.zipfile import ZipFile
try:
with ZipFile(source) as zf:
zf.extractall(tdir)
except Exception:
prints('Corrupt ZIP file, trying to use local headers')
from calibre.utils.localunzip import extractall
extractall(source, tdir)
elif source.lower().endswith('.rar'):
from calibre.utils.unrar import extract
extract(source, tdir)
return tdir
try: try:
if isinstance(self.source, basestring): if isinstance(self.source, basestring):
if os.path.isdir(self.source): find_files(self.source)
root = self.source
else:
root = self.extract()
for dirpath, dirnames, filenames in os.walk(root):
for files in find_books_in_directory(dirpath, self.single_book_per_directory):
if self.abort_scan:
return
if files:
self.file_groups[len(self.file_groups)] = files
else: else:
unreadable_files = [] unreadable_files = []
for path in self.source: for path in self.source:
if self.abort_scan: if self.abort_scan:
return return
if os.access(path, os.R_OK): if os.access(path, os.R_OK):
self.file_groups[len(self.file_groups)] = [path] if self.list_of_archives:
find_files(extract(path))
else:
self.file_groups[len(self.file_groups)] = [path]
else: else:
unreadable_files.append(path) unreadable_files.append(path)
if unreadable_files: if unreadable_files:
@ -145,22 +166,6 @@ class Adder(QObject):
except Exception: except Exception:
self.scan_error = traceback.format_exc() self.scan_error = traceback.format_exc()
def extract(self):
tdir = os.path.join(self.tdir, 'archive')
if self.source.lower().endswith('.zip'):
from calibre.utils.zipfile import ZipFile
try:
with ZipFile(self.source) as zf:
zf.extractall(tdir)
except Exception:
prints('Corrupt ZIP file, trying to use local headers')
from calibre.utils.localunzip import extractall
extractall(self.source, tdir)
elif self.path.lower().endswith('.rar'):
from calibre.utils.unrar import extract
extract(self.source, tdir)
return tdir
def monitor_scan(self): def monitor_scan(self):
self.scan_thread.join(0.05) self.scan_thread.join(0.05)
if self.scan_thread.is_alive(): if self.scan_thread.is_alive():