From 6742fdeadd7bf2c4c9e17a7494570a12b585aa6f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 30 May 2012 14:39:33 +0530 Subject: [PATCH] calibredb: Allow setting metadata for individual fields with the set_metadata command --- src/calibre/ebooks/metadata/book/base.py | 35 ++++++++- src/calibre/library/cli.py | 92 +++++++++++++++++++++--- 2 files changed, 116 insertions(+), 11 deletions(-) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 32aad28022..b4c202f5a6 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -13,7 +13,7 @@ from calibre.ebooks.metadata.book import (SC_COPYABLE_FIELDS, SC_FIELDS_COPY_NOT_NULL, STANDARD_METADATA_FIELDS, TOP_LEVEL_IDENTIFIERS, ALL_METADATA_FIELDS) from calibre.library.field_metadata import FieldMetadata -from calibre.utils.date import isoformat, format_date +from calibre.utils.date import isoformat, format_date, parse_only_date from calibre.utils.icu import sort_key from calibre.utils.formatter import TemplateFormatter @@ -799,3 +799,36 @@ class Metadata(object): # }}} +def field_from_string(field, raw, field_metadata): + ''' Parse the string raw to return an object that is suitable for calling + set() on a Metadata object. ''' + dt = field_metadata['datatype'] + val = object + if dt in {'int', 'float'}: + val = int(raw) if dt == 'int' else float(raw) + elif dt == 'rating': + val = float(raw) * 2 + elif dt == 'datetime': + val = parse_only_date(raw) + elif dt == 'bool': + if raw.lower() in {'true', 'yes', 'y'}: + val = True + elif raw.lower() in {'false', 'no', 'n'}: + val = False + else: + raise ValueError('Unknown value for %s: %s'%(field, raw)) + elif dt == 'text': + ism = field_metadata['is_multiple'] + if ism: + val = [x.strip() for x in raw.split(ism['ui_to_list'])] + if field == 'identifiers': + val = {x.partition(':')[0]:x.partition(':')[-1] for x in val} + elif field == 'languages': + from calibre.utils.localization import canonicalize_lang + val = [canonicalize_lang(x) for x in val] + val = [x for x in val if x] + if val is object: + val = raw + return val + + diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 17c980bd0f..eb53cadb34 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -13,6 +13,7 @@ from textwrap import TextWrapper from calibre import preferred_encoding, prints, isbytestring from calibre.utils.config import OptionParser, prefs, tweaks from calibre.ebooks.metadata.meta import get_metadata +from calibre.ebooks.metadata.book.base import field_from_string from calibre.library.database2 import LibraryDatabase2 from calibre.ebooks.metadata.opf2 import OPFCreator, OPF from calibre.utils.date import isoformat @@ -498,10 +499,6 @@ def command_show_metadata(args, dbpath): def do_set_metadata(db, id, stream): mi = OPF(stream).to_book_metadata() db.set_metadata(id, mi) - db.clean() - do_show_metadata(db, id, False) - write_dirtied(db) - send_message() def set_metadata_option_parser(): return get_parser(_( @@ -511,19 +508,94 @@ def set_metadata_option_parser(): 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 list command. You can get a quick feel for the OPF format by using the --as-opf switch to the -show_metadata command. +show_metadata command. You can also set the metadata of individual fields with +the --field option. ''')) def command_set_metadata(args, dbpath): parser = set_metadata_option_parser() - opts, args = parser.parse_args(sys.argv[1:]+args) - if len(args) < 3: + 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')) + opts, args = parser.parse_args(sys.argv[0:1]+args) + db = get_db(dbpath, opts) + + def fields(): + for key in sorted(db.field_metadata.all_field_keys()): + m = db.field_metadata[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 = db.field_metadata['cover'].copy() + c['datatype'] = 'text' + yield 'cover', c + + if opts.list_fields: + prints('%-40s'%_('Title'), _('Field name'), '\n') + for key, m in fields(): + prints('%-40s'%m['name'], key) + + return 0 + + def verify_int(x): + try: + int(x) + return True + except: + return False + + if len(args) < 2 or not verify_int(args[1]): parser.print_help() print - print >>sys.stderr, _('You must specify an id and a metadata file') + print >>sys.stderr, _('You must specify a record id as the ' + 'first argument') return 1 - id, opf = int(args[1]), open(args[2], 'rb') - do_set_metadata(get_db(dbpath, opts), id, opf) + if len(args) < 3 and not opts.field: + parser.print_help() + print + print >>sys.stderr, _('You must specify either a field or an opf file') + return 1 + book_id = int(args[1]) + + if len(args) > 2: + opf = args[2] + do_set_metadata(db, book_id, opf) + + if opts.field: + fields = {k:v for k, v in fields()} + vals = {} + for x in opts.field: + field, val = x.partition(':')[::2] + if field not in fields: + print >>sys.stderr, _('%s is not a known field'%field) + return 1 + val = field_from_string(field, val, fields[field]) + vals[field] = val + mi = db.get_metadata(book_id, index_is_id=True, get_cover=False) + for field, val in sorted(vals.iteritems(), key=lambda k: 1 if + k[0].endswith('_index') else 0): + mi.set(field, val) + db.set_metadata(book_id, mi, force_changes=True) + db.clean() + do_show_metadata(db, book_id, False) + write_dirtied(db) + send_message() + return 0 def do_export(db, ids, dir, opts):