mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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)
This commit is contained in:
parent
46afc39739
commit
b0d18ffb41
@ -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.srv.utils import get_db, get_library_data
|
||||||
from calibre.utils.imghdr import what
|
from calibre.utils.imghdr import what
|
||||||
from calibre.utils.serialize import MSGPACK_MIME, json_loads, msgpack_loads
|
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.binary import from_base64_bytes
|
||||||
|
from polyglot.builtins import iteritems
|
||||||
|
|
||||||
receive_data_methods = {'GET', 'POST'}
|
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')
|
raise HTTPBadRequest('Cover data must be either JPEG or PNG')
|
||||||
dirtied |= db.set_cover({book_id: cdata})
|
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):
|
for field, value in iteritems(changes):
|
||||||
dirtied |= db.set_field(field, {book_id: value})
|
dirtied |= db.set_field(field, {book_id: value})
|
||||||
ctx.notify_changes(db.backend.library_path, metadata(dirtied))
|
ctx.notify_changes(db.backend.library_path, metadata(dirtied))
|
||||||
|
@ -36,7 +36,7 @@ from utils import (
|
|||||||
from widgets import create_button
|
from widgets import create_button
|
||||||
|
|
||||||
CLASS_NAME = 'edit-metadata-panel'
|
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):
|
def identity(x):
|
||||||
return x
|
return x
|
||||||
value_to_json = identity
|
value_to_json = identity
|
||||||
@ -69,6 +69,22 @@ def resolved_metadata(mi, field):
|
|||||||
return 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):
|
def truncated_html(val):
|
||||||
ans = val.replace(/<[^>]+>/g, '')
|
ans = val.replace(/<[^>]+>/g, '')
|
||||||
if ans.length > 40:
|
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 <a>selecting the book file</a> 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):
|
def edit_field(container_id, book_id, field):
|
||||||
nonlocal value_to_json
|
nonlocal value_to_json
|
||||||
fm = library_data.field_metadata[field]
|
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)
|
multiple_line_edit(' & ', '&', container_id, book_id, field, fm, d, mi)
|
||||||
elif field is 'cover':
|
elif field is 'cover':
|
||||||
cover_edit(container_id, book_id, field, fm, d, mi)
|
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':
|
elif fm.datatype is 'series':
|
||||||
series_edit(container_id, book_id, field, fm, d, mi)
|
series_edit(container_id, book_id, field, fm, d, mi)
|
||||||
elif fm.datatype is 'datetime':
|
elif fm.datatype is 'datetime':
|
||||||
@ -765,6 +831,14 @@ def render_metadata(mi, table, container_id, book_id): # {{{
|
|||||||
else:
|
else:
|
||||||
add_row(name, None)
|
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):
|
def process_field(field, fm):
|
||||||
name = fm.name or field
|
name = fm.name or field
|
||||||
datatype = fm.datatype
|
datatype = fm.datatype
|
||||||
@ -785,6 +859,8 @@ def render_metadata(mi, table, container_id, book_id): # {{{
|
|||||||
func = process_publisher
|
func = process_publisher
|
||||||
elif field is 'languages':
|
elif field is 'languages':
|
||||||
func = process_languages
|
func = process_languages
|
||||||
|
elif field is 'formats':
|
||||||
|
func = process_formats
|
||||||
elif datatype is 'datetime':
|
elif datatype is 'datetime':
|
||||||
func = process_datetime
|
func = process_datetime
|
||||||
elif datatype is 'series':
|
elif datatype is 'series':
|
||||||
|
Loading…
x
Reference in New Issue
Block a user