%PDF- %PDF-
| Direktori : /lib/calibre/calibre/db/cli/ |
| Current File : //lib/calibre/calibre/db/cli/cmd_add.py |
#!/usr/bin/env python3
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
import os
import sys
from contextlib import contextmanager
from optparse import OptionGroup, OptionValueError
from calibre import prints
from calibre.db.adding import (
cdb_find_in_dir, cdb_recursive_find, compile_rule, create_format_map,
run_import_plugins, run_import_plugins_before_metadata
)
from calibre.db.utils import find_identical_books
from calibre.ebooks.metadata import MetaInformation, string_to_authors
from calibre.ebooks.metadata.book.serialize import read_cover, serialize_cover
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
from calibre.ptempfile import TemporaryDirectory
from calibre.srv.changes import books_added, formats_added
from calibre.utils.localization import canonicalize_lang
from calibre.utils.short_uuid import uuid4
readonly = False
version = 0 # change this if you change signature of implementation()
def empty(db, notify_changes, is_remote, args):
mi = args[0]
ids, duplicates = db.add_books([(mi, {})])
if is_remote:
notify_changes(books_added(ids))
db.dump_metadata()
return ids, bool(duplicates)
def cached_identical_book_data(db, request_id):
key = db.library_id, request_id
if getattr(cached_identical_book_data, 'key', None) != key:
cached_identical_book_data.key = key
cached_identical_book_data.ans = db.data_for_find_identical_books()
return cached_identical_book_data.ans
def do_adding(db, request_id, notify_changes, is_remote, mi, format_map, add_duplicates, oautomerge):
identical_book_list, added_ids, updated_ids = set(), set(), set()
duplicates = []
identical_books_data = None
def add_format(book_id, fmt):
db.add_format(book_id, fmt, format_map[fmt], replace=True, run_hooks=False)
updated_ids.add(book_id)
def add_book():
nonlocal added_ids
added_ids_, duplicates_ = db.add_books(
[(mi, format_map)], add_duplicates=True, run_hooks=False)
added_ids |= set(added_ids_)
duplicates.extend(duplicates_)
if oautomerge != 'disabled' or not add_duplicates:
identical_books_data = cached_identical_book_data(db, request_id)
identical_book_list = find_identical_books(mi, identical_books_data)
if oautomerge != 'disabled':
if identical_book_list:
needs_add = False
duplicated_formats = set()
for book_id in identical_book_list:
book_formats = {q.upper() for q in db.formats(book_id)}
input_formats = {q.upper():q for q in format_map}
common_formats = book_formats & set(input_formats)
if not common_formats:
for x in input_formats:
add_format(book_id, input_formats[x])
else:
new_formats = set(input_formats) - book_formats
if new_formats:
for x in new_formats:
add_format(book_id, input_formats[x])
if oautomerge == 'overwrite':
for x in common_formats:
add_format(book_id, input_formats[x])
elif oautomerge == 'ignore':
for x in common_formats:
duplicated_formats.add(input_formats[x])
elif oautomerge == 'new_record':
needs_add = True
if needs_add:
add_book()
if duplicated_formats:
duplicates.append((mi, {x: format_map[x] for x in duplicated_formats}))
else:
add_book()
else:
if identical_book_list:
duplicates.append((mi, format_map))
else:
add_book()
if added_ids and identical_books_data is not None:
for book_id in added_ids:
db.update_data_for_find_identical_books(book_id, identical_books_data)
if is_remote:
notify_changes(books_added(added_ids))
if updated_ids:
notify_changes(formats_added({book_id: tuple(format_map) for book_id in updated_ids}))
db.dump_metadata()
return added_ids, updated_ids, duplicates
def book(db, notify_changes, is_remote, args):
data, fname, fmt, add_duplicates, otitle, oauthors, oisbn, otags, oseries, oseries_index, ocover, oidentifiers, olanguages, oautomerge, request_id = args
with add_ctx(), TemporaryDirectory('add-single') as tdir, run_import_plugins_before_metadata(tdir):
if is_remote:
with lopen(os.path.join(tdir, fname), 'wb') as f:
f.write(data[1])
path = f.name
else:
path = data
path = run_import_plugins([path])[0]
fmt = os.path.splitext(path)[1]
fmt = (fmt[1:] if fmt else None) or 'unknown'
with lopen(path, 'rb') as stream:
mi = get_metadata(stream, stream_type=fmt, use_libprs_metadata=True)
if not mi.title:
mi.title = os.path.splitext(os.path.basename(path))[0]
if not mi.authors:
mi.authors = [_('Unknown')]
if oidentifiers:
ids = mi.get_identifiers()
ids.update(oidentifiers)
mi.set_identifiers(ids)
for x in ('title', 'authors', 'isbn', 'tags', 'series', 'languages'):
val = locals()['o' + x]
if val:
setattr(mi, x, val)
if oseries:
mi.series_index = oseries_index
if ocover:
mi.cover = None
mi.cover_data = ocover
identical_book_list, added_ids, updated_ids = set(), set(), set()
duplicates = []
identical_books_data = None
added_ids, updated_ids, duplicates = do_adding(
db, request_id, notify_changes, is_remote, mi, {fmt: path}, add_duplicates, oautomerge)
return added_ids, updated_ids, bool(duplicates), mi.title
def format_group(db, notify_changes, is_remote, args):
formats, add_duplicates, oautomerge, request_id, cover_data = args
with add_ctx(), TemporaryDirectory('add-multiple') as tdir, run_import_plugins_before_metadata(tdir):
updated_ids = {}
if is_remote:
paths = []
for name, data in formats:
with lopen(os.path.join(tdir, os.path.basename(name)), 'wb') as f:
f.write(data)
paths.append(f.name)
else:
paths = list(formats)
paths = run_import_plugins(paths)
mi = metadata_from_formats(paths)
if mi.title is None:
return None, set(), set(), False
if cover_data and not mi.cover_data or not mi.cover_data[1]:
mi.cover_data = 'jpeg', cover_data
format_map = create_format_map(paths)
added_ids, updated_ids, duplicates = do_adding(
db, request_id, notify_changes, is_remote, mi, format_map, add_duplicates, oautomerge)
return mi.title, set(added_ids), set(updated_ids), bool(duplicates)
def implementation(db, notify_changes, action, *args):
is_remote = notify_changes is not None
func = globals()[action]
return func(db, notify_changes, is_remote, args)
def do_add_empty(
dbctx, title, authors, isbn, tags, series, series_index, cover, identifiers,
languages
):
mi = MetaInformation(None)
if title is not None:
mi.title = title
if authors:
mi.authors = authors
if identifiers:
mi.set_identifiers(identifiers)
if isbn:
mi.isbn = isbn
if tags:
mi.tags = tags
if series:
mi.series, mi.series_index = series, series_index
if cover:
mi.cover = cover
if languages:
mi.languages = languages
ids, duplicates = dbctx.run('add', 'empty', read_cover(mi))
prints(_('Added book ids: %s') % ','.join(map(str, ids)))
@contextmanager
def add_ctx():
orig = sys.stdout
yield
sys.stdout = orig
def do_add(
dbctx, paths, one_book_per_directory, recurse, add_duplicates, otitle, oauthors,
oisbn, otags, oseries, oseries_index, ocover, oidentifiers, olanguages,
compiled_rules, oautomerge
):
request_id = uuid4()
with add_ctx():
files, dirs = [], []
for path in paths:
path = os.path.abspath(path)
if os.path.isdir(path):
dirs.append(path)
else:
if os.path.exists(path):
files.append(path)
else:
prints(path, 'not found')
file_duplicates, added_ids, merged_ids = [], set(), set()
for book in files:
fmt = os.path.splitext(book)[1]
fmt = fmt[1:] if fmt else None
if not fmt:
continue
aids, mids, dups, book_title = dbctx.run(
'add', 'book', dbctx.path(book), os.path.basename(book), fmt, add_duplicates,
otitle, oauthors, oisbn, otags, oseries, oseries_index, serialize_cover(ocover) if ocover else None,
oidentifiers, olanguages, oautomerge, request_id
)
added_ids |= set(aids)
merged_ids |= set(mids)
if dups:
file_duplicates.append((book_title, book))
dir_dups = []
scanner = cdb_recursive_find if recurse else cdb_find_in_dir
for dpath in dirs:
for formats in scanner(dpath, one_book_per_directory, compiled_rules):
cover_data = None
for fmt in formats:
if fmt.lower().endswith('.opf'):
with lopen(fmt, 'rb') as f:
mi = get_metadata(f, stream_type='opf')
if mi.cover_data and mi.cover_data[1]:
cover_data = mi.cover_data[1]
elif mi.cover:
try:
with lopen(mi.cover, 'rb') as f:
cover_data = f.read()
except OSError:
pass
book_title, ids, mids, dups = dbctx.run(
'add', 'format_group', tuple(map(dbctx.path, formats)), add_duplicates, oautomerge, request_id, cover_data)
if book_title is not None:
added_ids |= set(ids)
merged_ids |= set(mids)
if dups:
dir_dups.append((book_title, formats))
sys.stdout = sys.__stdout__
if dir_dups or file_duplicates:
prints(
_(
'The following books were not added as '
'they already exist in the database '
'(see --duplicates option or --automerge option):'
),
file=sys.stderr
)
for title, formats in dir_dups:
prints(' ', title, file=sys.stderr)
for path in formats:
prints(' ', path)
if file_duplicates:
for title, path in file_duplicates:
prints(' ', title, file=sys.stderr)
prints(' ', path)
if added_ids:
prints(_('Added book ids: %s') % (', '.join(map(str, added_ids))))
if merged_ids:
prints(_('Merged book ids: %s') % (', '.join(map(str, merged_ids))))
def option_parser(get_parser, args):
parser = get_parser(
_(
'''\
%prog add [options] file1 file2 file3 ...
Add the specified files as books to the database. You can also specify folders, see
the folder related options below.
'''
)
)
parser.add_option(
'-d',
'--duplicates',
action='store_true',
default=False,
help=_(
'Add books to database even if they already exist. Comparison is done based on book titles and authors.'
' Note that the {} option takes precedence.'
).format('--automerge')
)
parser.add_option(
'-m',
'--automerge',
type='choice',
choices=('disabled', 'ignore', 'overwrite', 'new_record'),
default='disabled',
help=_(
'If books with similar titles and authors are found, merge the incoming formats (files) automatically into'
' existing book records. A value of "ignore" means duplicate formats are discarded. A value of'
' "overwrite" means duplicate formats in the library are overwritten with the newly added files.'
' A value of "new_record" means duplicate formats are placed into a new book record.'
)
)
parser.add_option(
'-e',
'--empty',
action='store_true',
default=False,
help=_('Add an empty book (a book with no formats)')
)
parser.add_option(
'-t', '--title', default=None, help=_('Set the title of the added book(s)')
)
parser.add_option(
'-a',
'--authors',
default=None,
help=_('Set the authors of the added book(s)')
)
parser.add_option(
'-i', '--isbn', default=None, help=_('Set the ISBN of the added book(s)')
)
parser.add_option(
'-I',
'--identifier',
default=[],
action='append',
help=_('Set the identifiers for this book, e.g. -I asin:XXX -I isbn:YYY')
)
parser.add_option(
'-T', '--tags', default=None, help=_('Set the tags of the added book(s)')
)
parser.add_option(
'-s',
'--series',
default=None,
help=_('Set the series of the added book(s)')
)
parser.add_option(
'-S',
'--series-index',
default=1.0,
type=float,
help=_('Set the series number of the added book(s)')
)
parser.add_option(
'-c',
'--cover',
default=None,
help=_('Path to the cover to use for the added book')
)
parser.add_option(
'-l',
'--languages',
default=None,
help=_(
'A comma separated list of languages (best to use ISO639 language codes, though some language names may also be recognized)'
)
)
g = OptionGroup(
parser,
_('ADDING FROM FOLDERS'),
_(
'Options to control the adding of books from folders. By default only files that have extensions of known e-book file types are added.'
)
)
def filter_pat(option, opt, value, parser, action):
rule = {'match_type': 'glob', 'query': value, 'action': action}
try:
getattr(parser.values, option.dest).append(compile_rule(rule))
except Exception:
raise OptionValueError('%r is not a valid filename pattern' % value)
g.add_option(
'-1',
'--one-book-per-directory',
action='store_true',
default=False,
help=_(
'Assume that each folder has only a single logical book and that all files in it are different e-book formats of that book'
)
)
g.add_option(
'-r',
'--recurse',
action='store_true',
default=False,
help=_('Process folders recursively')
)
def fadd(opt, action, help):
g.add_option(
opt,
action='callback',
type='string',
nargs=1,
default=[],
callback=filter_pat,
dest='filters',
callback_args=(action, ),
metavar=_('GLOB PATTERN'),
help=help
)
fadd(
'--ignore', 'ignore',
_(
'A filename (glob) pattern, files matching this pattern will be ignored when scanning folders for files.'
' Can be specified multiple times for multiple patterns. For example: *.pdf will ignore all PDF files'
)
)
fadd(
'--add', 'add',
_(
'A filename (glob) pattern, files matching this pattern will be added when scanning folders for files,'
' even if they are not of a known e-book file type. Can be specified multiple times for multiple patterns.'
)
)
parser.add_option_group(g)
return parser
def main(opts, args, dbctx):
aut = string_to_authors(opts.authors) if opts.authors else []
tags = [x.strip() for x in opts.tags.split(',')] if opts.tags else []
lcodes = [canonicalize_lang(x) for x in (opts.languages or '').split(',')]
lcodes = [x for x in lcodes if x]
identifiers = (x.partition(':')[::2] for x in opts.identifier)
identifiers = {k.strip(): v.strip() for k, v in identifiers
if k.strip() and v.strip()}
if opts.empty:
do_add_empty(
dbctx, opts.title, aut, opts.isbn, tags, opts.series, opts.series_index,
opts.cover, identifiers, lcodes
)
return 0
if len(args) < 1:
raise SystemExit(_('You must specify at least one file to add'))
do_add(
dbctx, args, opts.one_book_per_directory, opts.recurse, opts.duplicates,
opts.title, aut, opts.isbn, tags, opts.series, opts.series_index, opts.cover,
identifiers, lcodes, opts.filters, opts.automerge
)
return 0