%PDF- %PDF-
| Direktori : /usr/lib/calibre/calibre/gui2/actions/ |
| Current File : //usr/lib/calibre/calibre/gui2/actions/add.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from collections import defaultdict
from functools import partial
from qt.core import QApplication, QDialog, QPixmap, QTimer
from calibre import as_unicode, guess_type, prepare_string_for_xml
from calibre.constants import iswindows
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import MetaInformation, normalize_isbn
from calibre.gui2 import (
choose_dir, choose_files, choose_files_and_remember_all_files, error_dialog,
gprefs, info_dialog, question_dialog, warning_dialog
)
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.add_empty_book import AddEmptyBookDialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config_base import tweaks
from calibre.utils.filenames import ascii_filename, make_long_path_useable
from calibre.utils.icu import sort_key
from polyglot.builtins import iteritems, string_or_bytes
def get_filters():
return [
(_('Books'), BOOK_EXTENSIONS),
(_('EPUB books'), ['epub', 'kepub']),
(_('Kindle books'), ['mobi', 'prc', 'azw', 'azw3', 'kfx', 'tpz', 'azw1', 'azw4']),
(_('PDF books'), ['pdf', 'azw4']),
(_('HTML books'), ['htm', 'html', 'xhtm', 'xhtml']),
(_('LIT books'), ['lit']),
(_('Text books'), ['txt', 'text', 'rtf', 'md', 'markdown', 'textile', 'txtz']),
(_('Comics'), ['cbz', 'cbr', 'cbc']),
(_('Archives'), ['zip', 'rar']),
(_('Wordprocessor files'), ['odt', 'doc', 'docx']),
]
class AddAction(InterfaceAction):
name = 'Add Books'
action_spec = (_('Add books'), 'add_book.png',
_('Add books to the calibre library/device from files on your computer')
, _('A'))
action_type = 'current'
action_add_menu = True
action_menu_clone_qaction = _('Add books from a single folder')
def genesis(self):
self._add_filesystem_book = self.Dispatcher(self.__add_filesystem_book)
self.add_menu = self.qaction.menu()
ma = partial(self.create_menu_action, self.add_menu)
ma('recursive-add', _('Add from folders and sub-folders'), icon='mimetypes/dir.png').triggered.connect(self.add_recursive_question)
ma('archive-add-book', _('Add multiple books from archive (ZIP/RAR)'), icon='mimetypes/zip.png').triggered.connect(self.add_from_archive)
self.add_menu.addSeparator()
ma('add-empty', _('Add empty book (Book entry with no formats)'),
shortcut='Shift+Ctrl+E').triggered.connect(self.add_empty)
ma('add-isbn', _('Add from ISBN'), icon='identifiers.png').triggered.connect(self.add_from_isbn)
self.add_menu.addSeparator()
ma('add-formats', _('Add files to selected book records'),
triggered=self.add_formats, shortcut='Shift+A')
ma('add-formats-clipboard', _('Add files to selected book records from clipboard'),
triggered=self.add_formats_from_clipboard, shortcut='Shift+Alt+A', icon='edit-paste.png')
ma('add-empty-format-to-books', _(
'Add an empty file to selected book records')).triggered.connect(self.add_empty_format_choose)
self.add_menu.addSeparator()
ma('add-config', _('Control the adding of books'), icon='config.png',
triggered=self.add_config)
self.qaction.triggered.connect(self.add_books)
def location_selected(self, loc):
enabled = loc == 'library'
for action in list(self.add_menu.actions())[1:]:
action.setEnabled(enabled)
def add_config(self):
self.gui.iactions['Preferences'].do_config(
initial_plugin=('Import/Export', 'Adding'),
close_after_initial=True)
def _check_add_formats_ok(self):
if self.gui.current_view() is not self.gui.library_view:
return []
view = self.gui.library_view
rows = view.selectionModel().selectedRows()
if not rows:
error_dialog(self.gui, _('No books selected'),
_('Cannot add files as no books are selected'), show=True)
ids = [view.model().id(r) for r in rows]
return ids
def add_formats_from_clipboard(self):
ids = self._check_add_formats_ok()
if not ids:
return
md = QApplication.instance().clipboard().mimeData()
files_to_add = []
images = []
if md.hasUrls():
for url in md.urls():
if url.isLocalFile():
path = url.toLocalFile()
if os.access(path, os.R_OK):
mt = guess_type(path)[0]
if mt and mt.startswith('image/'):
images.append(path)
else:
files_to_add.append(path)
if not files_to_add and not images:
return error_dialog(self.gui, _('No files in clipboard'),
_('No files have been copied to the clipboard'), show=True)
if files_to_add:
self._add_formats(files_to_add, ids)
if images:
if len(ids) > 1 and not question_dialog(
self.gui,
_('Are you sure?'),
_('Are you sure you want to set the same'
' cover for all %d books?')%len(ids)):
return
with lopen(images[0], 'rb') as f:
cdata = f.read()
self.gui.current_db.new_api.set_cover({book_id: cdata for book_id in ids})
self.gui.refresh_cover_browser()
m = self.gui.library_view.model()
current = self.gui.library_view.currentIndex()
m.current_changed(current, current)
def add_formats(self, *args):
ids = self._check_add_formats_ok()
if not ids:
return
books = choose_files_and_remember_all_files(self.gui, 'add formats dialog dir',
_('Select book files'), filters=get_filters())
if books:
self._add_formats(books, ids)
def _add_formats(self, paths, ids):
if len(ids) > 1 and not question_dialog(
self.gui,
_('Are you sure?'),
_('Are you sure you want to add the same'
' files to all %d books? If the format'
' already exists for a book, it will be replaced.')%len(ids)):
return
paths = list(map(make_long_path_useable, paths))
db = self.gui.current_db
if len(ids) == 1:
formats = db.formats(ids[0], index_is_id=True)
if formats:
formats = {x.upper() for x in formats.split(',')}
nformats = {f.rpartition('.')[-1].upper() for f in paths}
override = formats.intersection(nformats)
if override:
title = db.title(ids[0], index_is_id=True)
msg = ngettext(
'The {0} format will be replaced in the book {1}. Are you sure?',
'The {0} formats will be replaced in the book {1}. Are you sure?',
len(override)).format(', '.join(override), title)
if not confirm(msg, 'confirm_format_override_on_add', title=_('Are you sure?'), parent=self.gui):
return
fmt_map = {os.path.splitext(fpath)[1][1:].upper():fpath for fpath in paths}
for id_ in ids:
for fmt, fpath in iteritems(fmt_map):
if fmt:
db.add_format_with_hooks(id_, fmt, fpath, index_is_id=True,
notify=True)
current_idx = self.gui.library_view.currentIndex()
if current_idx.isValid():
self.gui.library_view.model().current_changed(current_idx, current_idx)
def is_ok_to_add_empty_formats(self):
if self.gui.stack.currentIndex() != 0:
return
view = self.gui.library_view
rows = view.selectionModel().selectedRows()
if not rows:
return error_dialog(self.gui, _('No books selected'),
_('Cannot add files as no books are selected'), show=True)
ids = [view.model().id(r) for r in rows]
if len(ids) > 1 and not question_dialog(
self.gui,
_('Are you sure?'),
_('Are you sure you want to add the same'
' empty file to all %d books? If the format'
' already exists for a book, it will be replaced.')%len(ids)):
return
return True
def add_empty_format_choose(self):
if not self.is_ok_to_add_empty_formats():
return
from calibre.ebooks.oeb.polish.create import valid_empty_formats
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
d = ChooseFormatDialog(self.gui, _('Choose format of empty file'), sorted(valid_empty_formats))
if d.exec() != QDialog.DialogCode.Accepted or not d.format():
return
self._add_empty_format(d.format())
def add_empty_format(self, format_):
if not self.is_ok_to_add_empty_formats():
return
self._add_empty_format(format_)
def _add_empty_format(self, format_):
view = self.gui.library_view
rows = view.selectionModel().selectedRows()
ids = [view.model().id(r) for r in rows]
db = self.gui.library_view.model().db
if len(ids) == 1:
formats = db.formats(ids[0], index_is_id=True)
if formats:
formats = {x.lower() for x in formats.split(',')}
if format_ in formats:
title = db.title(ids[0], index_is_id=True)
msg = _('The {0} format will be replaced in the book: {1}. Are you sure?').format(
format_, title)
if not confirm(msg, 'confirm_format_override_on_add', title=_('Are you sure?'),
parent=self.gui):
return
for id_ in ids:
self.add_empty_format_to_book(id_, format_)
current_idx = self.gui.library_view.currentIndex()
if current_idx.isValid():
view.model().current_changed(current_idx, current_idx)
def add_empty_format_to_book(self, book_id, fmt):
from calibre.ebooks.oeb.polish.create import create_book
db = self.gui.current_db
pt = PersistentTemporaryFile(suffix='.' + fmt.lower())
pt.close()
try:
mi = db.new_api.get_metadata(book_id, get_cover=False,
get_user_categories=False, cover_as_data=False)
create_book(mi, pt.name, fmt=fmt.lower())
db.add_format_with_hooks(book_id, fmt, pt.name, index_is_id=True, notify=True)
finally:
os.remove(pt.name)
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=False)
if paths:
self.do_add_recursive(paths, single, list_of_archives=True)
def add_from_archive(self):
single = question_dialog(self.gui, _('Type of archive'), _(
'Will the archive have a single book per internal folder?'))
paths = choose_files(
self.gui, 'recursive-archive-add', _('Choose archive file'),
filters=[(_('Archives'), ('zip', 'rar'))], all_files=False, select_only_single_file=False)
if paths:
self.do_add_recursive(paths, single, list_of_archives=True)
def add_recursive(self, single):
root = choose_dir(self.gui, 'recursive book import root dir dialog',
_('Select root folder'))
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 do_add_recursive(self, root, single, list_of_archives=False):
from calibre.gui2.add import Adder
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())
def add_recursive_single(self, *args):
'''
Add books from the local filesystem to either the library or the device
recursively assuming one book per folder.
'''
self.add_recursive(True)
def add_recursive_multiple(self, *args):
'''
Add books from the local filesystem to either the library or the device
recursively assuming multiple books per folder.
'''
self.add_recursive(False)
def add_recursive_question(self):
single = question_dialog(self.gui, _('Multi-file books?'), _(
'Assume all e-book files in a single folder are multiple formats of the same book?'))
self.add_recursive(single)
def add_empty(self, *args):
'''
Add an empty book item to the library. This does not import any formats
from a book file.
'''
author = series = title = None
index = self.gui.library_view.currentIndex()
if index.isValid():
raw = index.model().db.authors(index.row())
if raw:
authors = [a.strip().replace('|', ',') for a in raw.split(',')]
if authors:
author = authors[0]
series = index.model().db.series(index.row())
title = index.model().db.title(index.row())
dlg = AddEmptyBookDialog(self.gui, self.gui.library_view.model().db,
author, series, dup_title=title)
if dlg.exec() == QDialog.DialogCode.Accepted:
temp_files = []
num = dlg.qty_to_add
series = dlg.selected_series
title = dlg.selected_title or _('Unknown')
db = self.gui.library_view.model().db
ids, orig_fmts = [], []
if dlg.duplicate_current_book:
origmi = db.get_metadata(index.row(), get_cover=True, cover_as_data=True)
if dlg.copy_formats.isChecked():
book_id = db.id(index.row())
orig_fmts = tuple(db.new_api.format(book_id, fmt, as_path=True) for fmt in db.new_api.formats(book_id))
for x in range(num):
if dlg.duplicate_current_book:
mi = origmi
else:
mi = MetaInformation(title, dlg.selected_authors)
if series:
mi.series = series
mi.series_index = db.get_next_series_num_for(series)
fmts = []
empty_format = gprefs.get('create_empty_format_file', '')
if dlg.duplicate_current_book and dlg.copy_formats.isChecked():
fmts = orig_fmts
elif empty_format:
from calibre.ebooks.oeb.polish.create import create_book
pt = PersistentTemporaryFile(suffix='.' + empty_format)
pt.close()
temp_files.append(pt.name)
create_book(mi, pt.name, fmt=empty_format)
fmts = [pt.name]
ids.append(db.import_book(mi, fmts))
for path in orig_fmts:
os.remove(path)
self.refresh_gui(num)
if ids:
ids.reverse()
self.gui.library_view.select_rows(ids)
for path in temp_files:
os.remove(path)
def check_for_existing_isbns(self, books):
db = self.gui.current_db.new_api
book_id_identifiers = db.all_field_for('identifiers', db.all_book_ids(tuple))
existing_isbns = {normalize_isbn(ids.get('isbn', '')): book_id for book_id, ids in book_id_identifiers.items()}
existing_isbns.pop('', None)
ok = []
duplicates = []
for book in books:
q = normalize_isbn(book['isbn'])
if q and q in existing_isbns:
duplicates.append((book, existing_isbns[q]))
else:
ok.append(book)
if duplicates:
det_msg = '\n'.join(f'{book["isbn"]}: {db.field_for("title", book_id)}' for book, book_id in duplicates)
if question_dialog(self.gui, _('Duplicates found'), _(
'Books with some of the specified ISBNs already exist in the calibre library.'
' Click "Show details" for the full list. Do you want to add them anyway?'), det_msg=det_msg
):
ok += [x[0] for x in duplicates]
return ok
def add_isbns(self, books, add_tags=[], check_for_existing=False):
books = list(books)
if check_for_existing:
books = self.check_for_existing_isbns(books)
if not books:
return
self.isbn_books = books
self.add_by_isbn_ids = set()
self.isbn_add_tags = add_tags
QTimer.singleShot(10, self.do_one_isbn_add)
self.isbn_add_dialog = ProgressDialog(_('Adding'),
_('Creating book records from ISBNs'), max=len(books),
cancelable=False, parent=self.gui)
self.isbn_add_dialog.exec()
def do_one_isbn_add(self):
try:
db = self.gui.library_view.model().db
try:
x = self.isbn_books.pop(0)
except IndexError:
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
self.isbn_add_dialog.accept()
self.gui.iactions['Edit Metadata'].download_metadata(
ids=self.add_by_isbn_ids, ensure_fields=frozenset(['title',
'authors']))
return
mi = MetaInformation(None)
mi.isbn = x['isbn']
if self.isbn_add_tags:
mi.tags = list(self.isbn_add_tags)
fmts = [] if x['path'] is None else [x['path']]
self.add_by_isbn_ids.add(db.import_book(mi, fmts))
self.isbn_add_dialog.value += 1
QTimer.singleShot(10, self.do_one_isbn_add)
except:
self.isbn_add_dialog.accept()
raise
def files_dropped(self, paths):
to_device = self.gui.stack.currentIndex() != 0
self._add_books(paths, to_device)
def remote_file_dropped_on_book(self, url, fname):
if self.gui.current_view() is not self.gui.library_view:
return
db = self.gui.library_view.model().db
current_idx = self.gui.library_view.currentIndex()
if not current_idx.isValid():
return
cid = db.id(current_idx.row())
from calibre.gui2.dnd import DownloadDialog
d = DownloadDialog(url, fname, self.gui)
d.start_download()
if d.err is None:
self.files_dropped_on_book(None, [d.fpath], cid=cid)
def files_dropped_on_book(self, event, paths, cid=None, do_confirm=True):
accept = False
if self.gui.current_view() is not self.gui.library_view:
return
db = self.gui.library_view.model().db
cover_changed = False
current_idx = self.gui.library_view.currentIndex()
if cid is None:
if not current_idx.isValid():
return
cid = db.id(current_idx.row()) if cid is None else cid
formats = []
from calibre.gui2.dnd import image_extensions
image_exts = set(image_extensions()) - set(tweaks['cover_drop_exclude'])
if iswindows:
from calibre.gui2.add import resolve_windows_links
paths = list(resolve_windows_links(paths, hwnd=int(self.gui.effectiveWinId())))
for path in paths:
ext = os.path.splitext(path)[1].lower()
if ext:
ext = ext[1:]
if ext in image_exts:
pmap = QPixmap()
pmap.load(path)
if not pmap.isNull():
accept = True
db.set_cover(cid, pmap)
cover_changed = True
else:
formats.append((ext, path))
accept = True
if accept and event is not None:
event.accept()
add_as_book = False
if do_confirm and formats:
ok, add_as_book = confirm(
_('You have dropped some files onto the book <b>%s</b>. This will'
' add or replace the files for this book. Do you want to proceed?') % db.title(cid, index_is_id=True),
'confirm_drop_on_book', parent=self.gui,
extra_button=ngettext('Add as new book', 'Add as new books', len(formats)))
if ok and add_as_book:
add_as_book = [path for ext, path in formats]
if not ok or add_as_book:
formats = []
for ext, path in formats:
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
if current_idx.isValid():
self.gui.library_view.model().current_changed(current_idx, current_idx)
if cover_changed:
self.gui.refresh_cover_browser()
if add_as_book:
self.files_dropped(add_as_book)
def __add_filesystem_book(self, paths, allow_device=True):
if isinstance(paths, string_or_bytes):
paths = [paths]
books = [path for path in map(os.path.abspath, paths) if os.access(path,
os.R_OK)]
if books:
to_device = allow_device and self.gui.stack.currentIndex() != 0
self._add_books(books, to_device)
if to_device:
self.gui.status_bar.show_message(
_('Uploading books to device.'), 2000)
def add_filesystem_book(self, paths, allow_device=True):
self._add_filesystem_book(paths, allow_device=allow_device)
def add_from_isbn(self, *args):
from calibre.gui2.dialogs.add_from_isbn import AddFromISBN
d = AddFromISBN(self.gui)
if d.exec() == QDialog.DialogCode.Accepted and d.books:
self.add_isbns(d.books, add_tags=d.set_tags, check_for_existing=d.check_for_existing)
def add_books(self, *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)]
books = choose_files_and_remember_all_files(self.gui, 'add books dialog dir',
_('Select books'), filters=filters)
if not books:
return
self._add_books(books, to_device)
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
from calibre.gui2.add import Adder
Adder(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())
def refresh_gui(self, num, set_current_row=-1, recount=True):
self.gui.library_view.model().books_added(num)
if set_current_row > -1:
self.gui.library_view.set_current_row(0)
self.gui.refresh_cover_browser()
if recount:
self.gui.tags_view.recount()
def _files_added(self, adder, on_card=None):
if adder.items:
paths, infos, names = [], [], []
for mi, cover_path, format_paths in adder.items:
mi.cover = cover_path
paths.append(format_paths[0]), infos.append(mi)
names.append(ascii_filename(os.path.basename(paths[-1])))
self.gui.upload_books(paths, names, infos, on_card=on_card)
self.gui.status_bar.show_message(
_('Uploading books to device.'), 2000)
return
if adder.number_of_books_added > 0:
self.refresh_gui(adder.number_of_books_added, set_current_row=0)
if adder.merged_books:
merged = defaultdict(list)
for title, author in adder.merged_books:
merged[author].append(title)
lines = []
for author in sorted(merged, key=sort_key):
lines.append(f'<b><i>{prepare_string_for_xml(author)}</i></b><ol style="margin-top: 0">')
for title in sorted(merged[author]):
lines.append(f'<li>{prepare_string_for_xml(title)}</li>')
lines.append('</ol>')
pm = ngettext('The following duplicate book was found.',
'The following {} duplicate books were found.',
len(adder.merged_books)).format(len(adder.merged_books))
info_dialog(self.gui, _('Merged some books'), pm + ' ' +
_('Incoming book formats were processed and merged into your '
'calibre database according to your auto-merge '
'settings. Click "Show details" to see the list of merged books.'),
det_msg='\n'.join(lines), show=True, only_copy_details=True)
if adder.number_of_books_added > 0 or adder.merged_books:
# The formats of the current book could have changed if
# automerge is enabled
current_idx = self.gui.library_view.currentIndex()
if current_idx.isValid():
self.gui.library_view.model().current_changed(current_idx,
current_idx)
def _add_from_device_adder(self, adder, on_card=None, model=None):
self._files_added(adder, on_card=on_card)
# set the in-library flags, and as a consequence send the library's
# metadata for this book to the device. This sets the uuid to the
# correct value. Note that set_books_in_library might sync_booklists
self.gui.set_books_in_library(booklists=[model.db], reset=True)
self.gui.refresh_ondevice()
def add_books_from_device(self, view, paths=None):
backloading_err = self.gui.device_manager.device.BACKLOADING_ERROR_MESSAGE
if backloading_err is not None:
return error_dialog(self.gui, _('Add to library'), backloading_err,
show=True)
if paths is None:
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
d.exec()
return
paths = [p for p in view.model().paths(rows) if p is not None]
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
def ext(x):
ans = os.path.splitext(x)[1]
ans = ans[1:] if len(ans) > 1 else ans
return ans.lower()
remove = {p for p in paths if ext(p) in ve}
if remove:
paths = [p for p in paths if p not in remove]
vmsg = getattr(self.gui.device_manager.device, 'VIRTUAL_BOOK_EXTENSION_MESSAGE', None) or _(
'The following books are virtual and cannot be added'
' to the calibre library:')
info_dialog(self.gui, _('Not Implemented'), vmsg, '\n'.join(remove), show=True)
if not paths:
return
if not paths or len(paths) == 0:
d = error_dialog(self.gui, _('Add to library'), _('No book files found'))
d.exec()
return
self.gui.device_manager.prepare_addable_books(self.Dispatcher(partial(
self.books_prepared, view)), paths)
self.bpd = ProgressDialog(_('Downloading books'),
msg=_('Downloading books from device'), parent=self.gui,
cancelable=False)
QTimer.singleShot(1000, self.show_bpd)
def show_bpd(self):
if self.bpd is not None:
self.bpd.show()
def books_prepared(self, view, job):
self.bpd.hide()
self.bpd = None
if job.exception is not None:
self.gui.device_job_exception(job)
return
paths = job.result
ok_paths = [x for x in paths if isinstance(x, string_or_bytes)]
failed_paths = [x for x in paths if isinstance(x, tuple)]
if failed_paths:
if not ok_paths:
msg = _('Could not download files from the device')
typ = error_dialog
else:
msg = _('Could not download some files from the device')
typ = warning_dialog
det_msg = [x[0]+ '\n ' + as_unicode(x[1]) for x in failed_paths]
det_msg = '\n\n'.join(det_msg)
typ(self.gui, _('Could not download files'), msg, det_msg=det_msg,
show=True)
if ok_paths:
from calibre.gui2.add import Adder
callback = partial(self._add_from_device_adder, on_card=None, model=view.model())
Adder(ok_paths, db=self.gui.current_db, parent=self.gui, callback=callback, pool=self.gui.spare_pool())