mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Port calibredb add
This commit is contained in:
parent
9d5b32f3dd
commit
ae38edc6ad
375
src/calibre/db/cli/cmd_add.py
Normal file
375
src/calibre/db/cli/cmd_add.py
Normal file
@ -0,0 +1,375 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
import os
|
||||
import sys
|
||||
from io import BytesIO
|
||||
from optparse import OptionGroup, OptionValueError
|
||||
|
||||
from calibre import prints
|
||||
from calibre.db.adding import compile_rule, recursive_import, import_book_directory, import_book_directory_multiple
|
||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||
from calibre.ebooks.metadata.book.serialize import read_cover
|
||||
from calibre.ebooks.metadata.meta import get_metadata
|
||||
from calibre.srv.changes import books_added
|
||||
from calibre.utils.localization import canonicalize_lang
|
||||
|
||||
readonly = False
|
||||
|
||||
|
||||
def to_stream(data):
|
||||
ans = BytesIO(data[1])
|
||||
ans.name = data[0]
|
||||
return ans
|
||||
|
||||
|
||||
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 book(db, notify_changes, is_remote, args):
|
||||
data, fmt, mi, add_duplicates = args
|
||||
if is_remote:
|
||||
data = to_stream(data)
|
||||
ids, duplicates = db.add_books(
|
||||
[(mi, {fmt: data})], add_duplicates=add_duplicates)
|
||||
if is_remote:
|
||||
notify_changes(books_added(ids))
|
||||
db.dump_metadata()
|
||||
return ids, bool(duplicates)
|
||||
|
||||
|
||||
def add_books(db, notify_changes, is_remote, args):
|
||||
books, kwargs = args
|
||||
if is_remote:
|
||||
books = [(mi, {k:to_stream(v) for k, v in fmt_map.iteritems()}) for mi, fmt_map in books]
|
||||
ids, duplicates = db.add_books(books, **kwargs)
|
||||
if is_remote:
|
||||
notify_changes(books_added(ids))
|
||||
db.dump_metadata()
|
||||
return ids, [(mi.title, [getattr(x, 'name', '<stream>') for x in format_map.itervalues()]) for mi, format_map in 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)
|
||||
|
||||
|
||||
class DBProxy(object):
|
||||
# Allows dbctx to be used with the directory adding API that expects a
|
||||
# normal db object. Fortunately that API only calls one method,
|
||||
# add_books()
|
||||
|
||||
def __init__(self, dbctx):
|
||||
self.new_api = self
|
||||
self.dbctx = dbctx
|
||||
|
||||
def add_books(self, books, **kwargs):
|
||||
books = [(read_cover(mi), {k:self.dbctx.path(v) for k, v in fmt_map.iteritems()}) for mi, fmt_map in books]
|
||||
return self.dbctx.run('add', 'add_books', books, kwargs)
|
||||
|
||||
|
||||
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)))
|
||||
|
||||
|
||||
def do_add(
|
||||
dbctx, paths, one_book_per_directory, recurse, add_duplicates, otitle, oauthors,
|
||||
oisbn, otags, oseries, oseries_index, ocover, oidentifiers, olanguages,
|
||||
compiled_rules
|
||||
):
|
||||
orig = sys.stdout
|
||||
try:
|
||||
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 = [], set()
|
||||
for book in files:
|
||||
fmt = os.path.splitext(book)[1]
|
||||
fmt = fmt[1:] if fmt else None
|
||||
if not fmt:
|
||||
continue
|
||||
with lopen(book, '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(book))[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 = ocover
|
||||
mi.cover_data = (None, None)
|
||||
|
||||
ids, dups = dbctx.run(
|
||||
'add', 'book', dbctx.path(book), fmt, read_cover(mi), add_duplicates
|
||||
)
|
||||
added_ids |= set(ids)
|
||||
if dups:
|
||||
file_duplicates.append((mi.title, book))
|
||||
|
||||
dir_dups = []
|
||||
dbproxy = DBProxy(dbctx)
|
||||
|
||||
for dpath in dirs:
|
||||
if recurse:
|
||||
dups = recursive_import(
|
||||
dbproxy,
|
||||
dpath,
|
||||
single_book_per_directory=one_book_per_directory,
|
||||
added_ids=added_ids,
|
||||
compiled_rules=compiled_rules,
|
||||
add_duplicates=add_duplicates
|
||||
) or []
|
||||
else:
|
||||
func = import_book_directory if one_book_per_directory else import_book_directory_multiple
|
||||
dups = func(
|
||||
dbproxy,
|
||||
dpath,
|
||||
added_ids=added_ids,
|
||||
compiled_rules=compiled_rules,
|
||||
add_duplicates=add_duplicates
|
||||
) or []
|
||||
dir_dups.extend(dups)
|
||||
|
||||
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):'
|
||||
),
|
||||
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(type(u''), added_ids))))
|
||||
finally:
|
||||
sys.stdout = orig
|
||||
|
||||
|
||||
def option_parser(get_parser):
|
||||
parser = get_parser(
|
||||
_(
|
||||
'''\
|
||||
%prog add [options] file1 file2 file3 ...
|
||||
|
||||
Add the specified files as books to the database. You can also specify directories, see
|
||||
the directory 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.'
|
||||
)
|
||||
)
|
||||
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, for 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 DIRECTORIES'),
|
||||
_(
|
||||
'Options to control the adding of books from directories. By default only files that have extensions of known e-book file types are added.'
|
||||
)
|
||||
)
|
||||
|
||||
def filter_pat(option, opt, value, parser, action):
|
||||
try:
|
||||
getattr(parser.values, option.dest).append(
|
||||
compile_rule({
|
||||
'match_type': 'glob',
|
||||
'query': value,
|
||||
'action': action
|
||||
})
|
||||
)
|
||||
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 directory 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 directories 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 directories for files.'
|
||||
' Can be specified multiple times for multiple patterns. For e.g.: *.pdf will ignore all pdf files'
|
||||
)
|
||||
)
|
||||
fadd(
|
||||
'--add', 'add',
|
||||
_(
|
||||
'A filename (glob) pattern, files matching this pattern will be added when scanning directories 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 = dict((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
|
||||
)
|
||||
return 0
|
13
src/calibre/srv/changes.py
Normal file
13
src/calibre/srv/changes.py
Normal file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python2
|
||||
# vim:fileencoding=utf-8
|
||||
# License: GPLv3 Copyright: 2017, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
|
||||
|
||||
def books_added(book_ids):
|
||||
pass
|
||||
|
||||
|
||||
def formats_added(formats_map):
|
||||
pass
|
Loading…
x
Reference in New Issue
Block a user