diff --git a/src/calibre/srv/cdb.py b/src/calibre/srv/cdb.py
index 52c8fd65fc..797488967a 100644
--- a/src/calibre/srv/cdb.py
+++ b/src/calibre/srv/cdb.py
@@ -5,6 +5,7 @@
from __future__ import absolute_import, division, print_function, unicode_literals
import os
+from base64 import standard_b64decode
from functools import partial
from io import BytesIO
@@ -13,10 +14,11 @@ from calibre.db.cli import module_for_cmd
from calibre.ebooks.metadata.meta import get_metadata
from calibre.srv.changes import books_added, books_deleted, metadata
from calibre.srv.errors import HTTPBadRequest, HTTPForbidden, HTTPNotFound
+from calibre.srv.metadata import book_as_json
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 calibre.srv.metadata import book_as_json
receive_data_methods = {'GET', 'POST'}
@@ -110,6 +112,17 @@ def cdb_delete_book(ctx, rd, book_ids, library_id):
return {}
+@endpoint('/cdb/set-cover/{book_id}/{library_id=None}', types={'book_id': int},
+ needs_db_write=True, postprocess=json, methods=receive_data_methods, cache_control='no-cache')
+def cdb_set_cover(ctx, rd, book_id, library_id):
+ db = get_db(ctx, rd, library_id)
+ if ctx.restriction_for(rd, db):
+ raise HTTPForbidden('Cannot use the add book interface with a user who has per library restrictions')
+ rd.request_body_file.seek(0)
+ dirtied = db.set_cover({book_id: rd.request_body_file})
+ return tuple(dirtied)
+
+
@endpoint('/cdb/set-fields/{book_id}/{library_id=None}', types={'book_id': int},
needs_db_write=True, postprocess=msgpack_or_json, methods=receive_data_methods, cache_control='no-cache')
def cdb_set_fields(ctx, rd, book_id, library_id):
@@ -130,6 +143,21 @@ def cdb_set_fields(ctx, rd, book_id, library_id):
except Exception:
raise HTTPBadRequest('Invalid encoded data')
dirtied = set()
+ cdata = changes.pop('cover', False)
+ if cdata is not False:
+ if cdata is not None:
+ try:
+ cdata = standard_b64decode(cdata.split(',', 1)[-1].encode('ascii'))
+ except Exception:
+ raise HTTPBadRequest('Cover data is not valid base64 encoded data')
+ try:
+ fmt = what(None, cdata)
+ except Exception:
+ fmt = None
+ if fmt not in ('jpeg', 'png'):
+ raise HTTPBadRequest('Cover data must be either JPEG or PNG')
+ dirtied |= db.set_cover({book_id: cdata})
+
for field, value in changes.iteritems():
dirtied |= db.set_field(field, {book_id: value})
metadata(dirtied)
diff --git a/src/pyj/book_list/edit_metadata.pyj b/src/pyj/book_list/edit_metadata.pyj
index b0656cf912..da815f9664 100644
--- a/src/pyj/book_list/edit_metadata.pyj
+++ b/src/pyj/book_list/edit_metadata.pyj
@@ -12,8 +12,8 @@ from book_list.book_details import (
report_load_failure
)
from book_list.library_data import (
- book_metadata, current_library_id, field_names_for, library_data, load_status,
- loaded_book_ids, set_book_metadata
+ book_metadata, cover_url, current_library_id, field_names_for, library_data,
+ load_status, loaded_book_ids, set_book_metadata
)
from book_list.router import back
from book_list.theme import get_color
@@ -21,6 +21,9 @@ from book_list.top_bar import create_top_bar, set_title
from book_list.ui import set_panel_handler, show_panel
from date import UNDEFINED_DATE_ISO, format_date
from dom import add_extra_css, build_rule, clear, svgicon
+from file_uploads import (
+ update_status_widget, upload_files_widget, upload_status_widget
+)
from modals import error_dialog
from session import get_interface_data
from utils import (
@@ -474,6 +477,29 @@ def bool_edit(container_id, book_id, field, fm, div, mi):
return val_map[x]
# }}}
+# Cover edit {{{
+
+def cover_chosen(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
+ file = files[0]
+ changes.cover = file
+ has_changes = True
+ on_close(top_container_id)
+cover_chosen.counter = 0
+
+
+def cover_edit(container_id, book_id, field, fm, div, mi):
+ upload_files_widget(div, cover_chosen.bind(None, container_id, book_id), _(
+ 'Change the cover by selecting the cover image or drag and drop of the cover image here.'),
+ single_file=True, accept_extensions='png jpeg jpg')
+
+# }}}
+
def edit_field(container_id, book_id, field):
nonlocal value_to_json
fm = library_data.field_metadata[field]
@@ -491,6 +517,8 @@ def edit_field(container_id, book_id, field):
update_completions.prefix = ''
if field is 'authors':
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 fm.datatype is 'series':
series_edit(container_id, book_id, field, fm, d, mi)
elif fm.datatype is 'datetime':
@@ -710,6 +738,21 @@ def render_metadata(mi, table, container_id, book_id): # {{{
print('Failed to render metadata field: ' + field)
traceback.print_exc()
+ current_edit_action = edit_field.bind(None, container_id, book_id, 'cover')
+ table.appendChild(E.tr(onclick=current_edit_action, E.td(_('Cover') + ':'), E.td()))
+ img = E.img(
+ style='max-width: 300px; max-height: 400px',
+ )
+ if changes.cover:
+ r = FileReader()
+ r.onload = def(evt):
+ img.src = evt.target.result
+ changes.cover = evt.target.result
+ r.readAsDataURL(changes.cover)
+ v'delete changes.cover'
+ else:
+ img.src = cover_url(book_id)
+ table.lastChild.lastChild.appendChild(img)
# }}}
@@ -735,14 +778,22 @@ def changes_submitted(container_id, book_id, end_type, xhr, ev):
on_close(container_id)
+def on_progress(container_id, book_id, loaded, total, xhr):
+ container = document.getElementById(container_id)
+ if container and total:
+ update_status_widget(container, loaded, total)
+
+
def submit_changes(container_id, book_id):
c = document.getElementById(container_id)
d = c.querySelector('div[data-ctype="show"]')
clear(d)
d.appendChild(E.div(style='margin: 1ex 1rem', _('Uploading changes to server, please wait...')))
data = {'changes': changes, 'loaded_book_ids': loaded_book_ids()}
+ w = upload_status_widget()
+ d.appendChild(w)
ajax_send(
- f'cdb/set-fields/{book_id}/{current_library_id()}', data, changes_submitted.bind(None, container_id, book_id))
+ f'cdb/set-fields/{book_id}/{current_library_id()}', data, changes_submitted.bind(None, container_id, book_id), on_progress.bind(None, container_id, book_id))
def show_book(container_id, book_id):
diff --git a/src/pyj/file_uploads.pyj b/src/pyj/file_uploads.pyj
index 445d5e1d54..492d9686db 100644
--- a/src/pyj/file_uploads.pyj
+++ b/src/pyj/file_uploads.pyj
@@ -5,11 +5,12 @@ from __python__ import bound_methods, hash_literals
from elementmaker import E
from gettext import gettext as _
-from utils import safe_set_inner_html, human_readable
+from dom import ensure_id
+from utils import human_readable, safe_set_inner_html
-def upload_files_widget(container, proceed, msg):
- container_id = container.id
+def upload_files_widget(container, proceed, msg, single_file=False, accept_extensions=None):
+ container_id = ensure_id(container, 'upload-files')
def files_selected():
files = this.files
@@ -19,7 +20,11 @@ def upload_files_widget(container, proceed, msg):
proceed(container_id, files)
msg = msg or _('Upload books by selecting the book files or drag and drop of the files here')
- c = E.div(E.span(), E.input(type='file', multiple=True, style='display:none', onchange=files_selected))
+ c = E.div(E.span(), E.input(type='file', style='display:none', onchange=files_selected))
+ if not single_file:
+ c.lastChild.setAttribute('multiple', 'multiple')
+ if accept_extensions:
+ c.lastChild.setAttribute('accept', ', '.join(['.' + x for x in accept_extensions.split(' ')]))
c.style.minHeight = '80vh'
c.style.padding = '1rem'
c.style.borderBottom = 'solid 1px currentColor'
@@ -58,7 +63,8 @@ def update_status_widget(w, sent, total):
def upload_status_widget(name, job_id):
ans = E.div(style='padding: 1rem 1ex;', data_job='' + job_id)
- ans.appendChild(E.h3(E.b(name)))
+ if name:
+ ans.appendChild(E.h3(E.b(name)))
ans.appendChild(E.progress())
ans.appendChild(E.span())
return ans