diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index ddbc6e53d5..39f2acf341 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1244,6 +1244,7 @@ class Cache(object): provided, but are never deleted. Also note that force_changes has no effect on setting title or authors. ''' + dirtied = set() try: # Handle code passing in an OPF object instead of a Metadata object @@ -1252,7 +1253,7 @@ class Cache(object): pass def set_field(name, val): - self._set_field(name, {book_id:val}, do_path_update=False, allow_case_change=allow_case_change) + dirtied.update(self._set_field(name, {book_id:val}, do_path_update=False, allow_case_change=allow_case_change)) path_changed = False if set_title and mi.title: @@ -1342,6 +1343,7 @@ class Cache(object): # the db and Cache are in sync self._reload_from_db() raise + return dirtied def _do_add_format(self, book_id, fmt, stream, name=None, mtime=None): path = self._field_for('path', book_id) diff --git a/src/calibre/db/cli/cmd_set_metadata.py b/src/calibre/db/cli/cmd_set_metadata.py index 81b38b65b3..9e8096c4d7 100644 --- a/src/calibre/db/cli/cmd_set_metadata.py +++ b/src/calibre/db/cli/cmd_set_metadata.py @@ -4,19 +4,182 @@ from __future__ import absolute_import, division, print_function, unicode_literals +import os + +from calibre import prints +from calibre.ebooks.metadata.book.base import field_from_string +from calibre.ebooks.metadata.book.serialize import read_cover +from calibre.ebooks.metadata.opf import get_metadata +from calibre.srv.changes import metadata + readonly = False version = 0 # change this if you change signature of implementation() -def implementation(db, notify_changes, *args): +def implementation(db, notify_changes, action, *args): is_remote = notify_changes is not None - is_remote + if action == 'field_metadata': + return db.field_metadata + if action == 'opf': + book_id, mi = args + with db.write_lock: + if not db.has_id(book_id): + return + changed_ids = db.set_metadata(book_id, mi, force_changes=True, allow_case_change=False) + if is_remote: + notify_changes(metadata(changed_ids)) + return db.get_metadata(book_id) + if action == 'fields': + book_id, fvals = args + with db.write_lock: + if not db.has_id(book_id): + return + mi = db.get_metadata(book_id) + for field, val in fvals: + if field.endswith('_index'): + sname = mi.get(field[:-6]) + if sname: + mi.set(field[:-6], sname, extra=val) + if field == 'series_index': + mi.series_index = val # extra has no effect for the builtin series field + elif field == 'cover': + if is_remote: + mi.cover_data = None, val[1] + else: + mi.cover = val + read_cover(mi) + else: + mi.set(field, val) + changed_ids = db.set_metadata(book_id, mi, force_changes=True, allow_case_change=True) + if is_remote: + notify_changes(metadata(changed_ids)) + return db.get_metadata(book_id) def option_parser(get_parser): - pass + parser = get_parser( + _( + ''' +%prog set_metadata [options] id [/path/to/metadata.opf] + +Set the metadata stored in the calibre database for the book identified by id +from the OPF file metadata.opf. id is an id number from the search command. You +can get a quick feel for the OPF format by using the --as-opf switch to the +show_metadata command. You can also set the metadata of individual fields with +the --field option. If you use the --field option, there is no need to specify +an OPF file. +''' + ) + ) + parser.add_option( + '-f', + '--field', + action='append', + default=[], + help=_( + 'The field to set. Format is field_name:value, for example: ' + '{0} tags:tag1,tag2. Use {1} to get a list of all field names. You ' + 'can specify this option multiple times to set multiple fields. ' + 'Note: For languages you must use the ISO639 language codes (e.g. ' + 'en for English, fr for French and so on). For identifiers, the ' + 'syntax is {0} {2}. For boolean (yes/no) fields use true and false ' + 'or yes and no.' + ).format('--field', '--list-fields', 'identifiers:isbn:XXXX,doi:YYYYY') + ) + parser.add_option( + '-l', + '--list-fields', + action='store_true', + default=False, + help=_( + 'List the metadata field names that can be used' + ' with the --field option' + ) + ) + return parser + + +def get_fields(dbctx): + fm = dbctx.run('set_metadata', 'field_metadata') + for key in sorted(fm.all_field_keys()): + m = fm[key] + if (key not in {'formats', 'series_sort', 'ondevice', 'path', + 'last_modified'} and m['is_editable'] and m['name']): + yield key, m + if m['datatype'] == 'series': + si = m.copy() + si['name'] = m['name'] + ' Index' + si['datatype'] = 'float' + yield key + '_index', si + c = fm['cover'].copy() + c['datatype'] = 'text' + yield 'cover', c def main(opts, args, dbctx): - raise NotImplementedError('TODO: implement this') + if opts.list_fields: + ans = get_fields(dbctx) + prints('%-40s' % _('Title'), _('Field name'), '\n') + for key, m in ans: + prints('%-40s' % m['name'], key) + return 0 + + def verify_int(x): + try: + int(x) + return True + except: + return False + + if len(args) < 1 or not verify_int(args[0]): + raise SystemExit(_( + 'You must specify a record id as the ' + 'first argument' + )) + if len(args) < 2 and not opts.field: + raise SystemExit(_('You must specify either a field or an opf file')) + book_id = int(args[0]) + + if len(args) > 1: + opf = os.path.abspath(args[1]) + if not os.path.exists(opf): + raise SystemExit(_('The OPF file %s does not exist') % opf) + with lopen(opf, 'rb') as stream: + mi = get_metadata(stream)[0] + if mi.cover: + mi.cover = os.path.join(os.path.dirname(opf), os.path.relpath(mi.cover, os.getcwdu())) + final_mi = dbctx.run('set_metadata', 'opf', book_id, read_cover(mi)) + if not final_mi: + raise SystemExit(_('No book with id: %s in the database') % book_id) + + if opts.field: + fields = {k: v for k, v in get_fields(dbctx)} + fields['title_sort'] = fields['sort'] + vals = {} + for x in opts.field: + field, val = x.partition(':')[::2] + if field == 'sort': + field = 'title_sort' + if field not in fields: + raise SystemExit(_('%s is not a known field' % field)) + if field == 'cover': + val = dbctx.path(os.path.abspath(os.path.expanduser(val))) + else: + val = field_from_string(field, val, fields[field]) + vals[field] = val + fvals = [] + for field, val in sorted( # ensure series_index fields are set last + vals.iteritems(), key=lambda k: 1 if k[0].endswith('_index') else 0): + if field.endswith('_index'): + try: + val = float(val) + except Exception: + raise SystemExit('The value %r is not a valid series index' % val) + fvals.append((field, val)) + + final_mi = dbctx.run('set_metadata', 'fields', book_id, fvals) + if not final_mi: + raise SystemExit(_('No book with id: %s in the database') % book_id) + + prints(unicode(final_mi)) return 0 diff --git a/src/calibre/srv/changes.py b/src/calibre/srv/changes.py index b2c3d8980f..a643e478ea 100644 --- a/src/calibre/srv/changes.py +++ b/src/calibre/srv/changes.py @@ -21,3 +21,7 @@ def formats_removed(formats_map): def books_deleted(book_ids): pass + + +def metadata(book_ids): + pass