From b0d18ffb411641ac1434720e8093b85f463229e6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 10 Jul 2019 10:41:22 +0530 Subject: [PATCH] Content server: Allow adding or removing formats to a book via the edit metadata page. Fixes #1831304 [Request: Delete Formats on Content Server](https://bugs.launchpad.net/calibre/+bug/1831304) --- src/calibre/srv/cdb.py | 22 +++++++- src/pyj/book_list/edit_metadata.pyj | 78 ++++++++++++++++++++++++++++- 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/src/calibre/srv/cdb.py b/src/calibre/srv/cdb.py index 12aa557021..f407399452 100644 --- a/src/calibre/srv/cdb.py +++ b/src/calibre/srv/cdb.py @@ -18,8 +18,9 @@ from calibre.srv.routes import endpoint, json, msgpack_or_json from calibre.srv.utils import get_db, get_library_data from calibre.utils.imghdr import what from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads -from polyglot.builtins import iteritems +from calibre.utils.speedups import ReadOnlyFileBuffer from polyglot.binary import from_base64_bytes +from polyglot.builtins import iteritems receive_data_methods = {'GET', 'POST'} @@ -171,6 +172,25 @@ def cdb_set_fields(ctx, rd, book_id, library_id): raise HTTPBadRequest('Cover data must be either JPEG or PNG') dirtied |= db.set_cover({book_id: cdata}) + added_formats = changes.pop('added_formats', False) + if added_formats: + for data in added_formats: + try: + fmt = data['ext'].upper() + except Exception: + raise HTTPBadRequest('Format has no extension') + if fmt: + try: + fmt_data = from_base64_bytes(data['data_url'].split(',', 1)[-1]) + except Exception: + raise HTTPBadRequest('Format data is not valid base64 encoded data') + if db.add_format(book_id, fmt, ReadOnlyFileBuffer(fmt_data)): + dirtied.add(book_id) + removed_formats = changes.pop('removed_formats', False) + if removed_formats: + db.remove_formats({book_id: list(removed_formats)}) + dirtied.add(book_id) + for field, value in iteritems(changes): dirtied |= db.set_field(field, {book_id: value}) ctx.notify_changes(db.backend.library_path, metadata(dirtied)) diff --git a/src/pyj/book_list/edit_metadata.pyj b/src/pyj/book_list/edit_metadata.pyj index ef13d39cb6..5737924686 100644 --- a/src/pyj/book_list/edit_metadata.pyj +++ b/src/pyj/book_list/edit_metadata.pyj @@ -36,7 +36,7 @@ from utils import ( from widgets import create_button CLASS_NAME = 'edit-metadata-panel' -IGNORED_FIELDS = {'formats', 'sort', 'uuid', 'id', 'urls_from_identifiers', 'lang_names', 'last_modified', 'path', 'marked', 'size', 'ondevice', 'cover', 'au_map', 'isbn'} +IGNORED_FIELDS = {'sort', 'uuid', 'id', 'urls_from_identifiers', 'lang_names', 'last_modified', 'path', 'marked', 'size', 'ondevice', 'cover', 'au_map', 'isbn'} def identity(x): return x value_to_json = identity @@ -69,6 +69,22 @@ def resolved_metadata(mi, field): return mi[field] +def resolved_formats(val): + val = list(val or v'[]') + if changes.added_formats: + for data in changes.added_formats: + ext = data.ext.toUpperCase() + if ext and ext not in val: + val.push(ext) + if changes.removed_formats: + for fmt in changes.removed_formats: + fmt = fmt.toUpperCase() + if fmt in val: + val.remove(fmt) + val.sort() + return val + + def truncated_html(val): ans = val.replace(/<[^>]+>/g, '') if ans.length > 40: @@ -574,6 +590,54 @@ def cover_edit(container_id, book_id, field, fm, div, mi): # }}} +# Formats edit {{{ + +def format_added(top_container_id, book_id, container_id, files): + nonlocal has_changes + container = document.getElementById(container_id) + if not container: + return + if not files[0]: + return + added = changes.added_formats or v'[]' + for file in files: + ext = file.name.rpartition('.')[-1] + data = {'name': file.name, 'size': file.size, 'type': file.type, 'data_url': None, 'ext': ext} + added.push(data) + r = FileReader() + r.onload = def(evt): + data.data_url = evt.target.result + r.readAsDataURL(file) + changes.added_formats = added + has_changes = True + on_close(top_container_id) + + +def remove_format(top_container_id, book_id, fmt): + nonlocal has_changes + has_changes = True + removed_formats = changes.removed_formats or v'[]' + removed_formats.push(fmt.toUpperCase()) + changes.removed_formats = removed_formats + on_close(top_container_id) + + +def formats_edit(container_id, book_id, field, fm, div, mi): + upload_files_widget(div, format_added.bind(None, container_id, book_id), _( + 'Add a format by selecting the book file or drag and drop of the book file here.'), + single_file=True) + remove_buttons = E.div(style='padding: 1rem; display: flex; flex-wrap: wrap; align-content: flex-start') + formats = resolved_formats(mi.formats) + for i, fmt in enumerate(formats): + remove_buttons.appendChild(create_button( + _('Remove {}').format(fmt.upper()), action=remove_format.bind(None, container_id, book_id, fmt.upper()))) + remove_buttons.lastChild.style.marginBottom = '1ex' + remove_buttons.lastChild.style.marginRight = '1rem' + + div.appendChild(remove_buttons) + +# }}} + def edit_field(container_id, book_id, field): nonlocal value_to_json fm = library_data.field_metadata[field] @@ -593,6 +657,8 @@ def edit_field(container_id, book_id, field): multiple_line_edit(' & ', '&', container_id, book_id, field, fm, d, mi) elif field is 'cover': cover_edit(container_id, book_id, field, fm, d, mi) + elif field is 'formats': + formats_edit(container_id, book_id, field, fm, d, mi) elif fm.datatype is 'series': series_edit(container_id, book_id, field, fm, d, mi) elif fm.datatype is 'datetime': @@ -765,6 +831,14 @@ def render_metadata(mi, table, container_id, book_id): # {{{ else: add_row(name, None) + def process_formats(field, fm, name, val): + val = resolved_formats(val) + if val.length: + join = fm.is_multiple.list_to_ui if fm.is_multiple else None + add_row(name, val, join=join) + else: + add_row(name, None) + def process_field(field, fm): name = fm.name or field datatype = fm.datatype @@ -785,6 +859,8 @@ def render_metadata(mi, table, container_id, book_id): # {{{ func = process_publisher elif field is 'languages': func = process_languages + elif field is 'formats': + func = process_formats elif datatype is 'datetime': func = process_datetime elif datatype is 'series':