diff --git a/src/calibre/srv/standalone.py b/src/calibre/srv/standalone.py
index 2316699918..03ab6db388 100644
--- a/src/calibre/srv/standalone.py
+++ b/src/calibre/srv/standalone.py
@@ -11,6 +11,7 @@ import sys
from calibre import as_unicode
from calibre.constants import is_running_from_develop, isosx, iswindows, plugins
from calibre.db.legacy import LibraryDatabase
+from calibre.db.delete_service import shutdown as shutdown_delete_service
from calibre.srv.bonjour import BonJour
from calibre.srv.handler import Handler
from calibre.srv.http_response import create_http_handler
@@ -220,4 +221,7 @@ def main(args=sys.argv):
# Needed for dynamic cover generation, which uses Qt for drawing
from calibre.gui2 import ensure_app, load_builtin_fonts
ensure_app(), load_builtin_fonts()
- server.serve_forever()
+ try:
+ server.serve_forever()
+ finally:
+ shutdown_delete_service()
diff --git a/src/pyj/book_list/add.pyj b/src/pyj/book_list/add.pyj
index 0fab202de6..b9cad2c31d 100644
--- a/src/pyj/book_list/add.pyj
+++ b/src/pyj/book_list/add.pyj
@@ -82,6 +82,17 @@ def list_duplicate_book(container, container_id, job_id, data, file):
container.appendChild(b)
+def write_access_error(msg, xhr):
+ html = msg + '
'
+ if xhr.status is 403:
+ un = get_interface_data().username
+ if un:
+ html += _('You are not allowed to make changes to the library') + '
'
+ else:
+ html += _('You must be logged in to make changes to the library') + '
'
+ return html + '
' + xhr.error_html
+
+
def on_complete(container_id, job_id, end_type, xhr, ev):
idx = state.transfers.indexOf(xhr)
if idx > -1:
@@ -100,14 +111,8 @@ def on_complete(container_id, job_id, end_type, xhr, ev):
elif end_type is 'abort':
return
else:
- html = _('Failed to upload the file: {}').format(this.name) + '
'
- if xhr.status is 403:
- un = get_interface_data().username
- if un:
- html += _('You are not allowed to make changes to the library') + '
'
- else:
- html += _('You must be logged in to make changes to the library') + '
'
- safe_set_inner_html(container, html + '
' + xhr.error_html)
+ html = write_access_error(_('Failed to upload the file: {}').format(this.name), xhr)
+ safe_set_inner_html(container, html)
def fake_send(container_id, job_id):
@@ -123,7 +128,7 @@ def send_file(file, container_id, job_id, add_duplicates):
lid = loaded_books_query().library_id
ad = 'y' if add_duplicates else 'n'
xhr = ajax_send_file(
- f'/cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
+ f'cdb/add-book/{job_id}/{ad}/{encodeURIComponent(file.name)}/{lid}',
file, on_complete.bind(file, container_id, job_id), on_progress.bind(None, container_id, job_id))
state.transfers.push(xhr)
return xhr
diff --git a/src/pyj/book_list/book_details.pyj b/src/pyj/book_list/book_details.pyj
index 31c96adf28..91c3d54a82 100644
--- a/src/pyj/book_list/book_details.pyj
+++ b/src/pyj/book_list/book_details.pyj
@@ -7,6 +7,7 @@ from elementmaker import E
from gettext import gettext as _
from ajax import ajax, encode_query_component
+from book_list.delete_book import start_delete_book
from book_list.globals import get_session_data
from book_list.item_list import create_item, create_item_list
from book_list.library_data import (
@@ -15,7 +16,7 @@ from book_list.library_data import (
)
from book_list.router import back, home, open_book
from book_list.theme import get_color, get_font_size
-from book_list.top_bar import add_button, create_top_bar, set_title, clear_buttons
+from book_list.top_bar import add_button, clear_buttons, create_top_bar, set_title
from book_list.ui import query_as_href, set_panel_handler, show_panel
from book_list.views import search_query_for
from date import format_date
@@ -415,6 +416,7 @@ def render_book(container_id, book_id):
if not c:
return
metadata = book_metadata(book_id)
+ render_book.title = metadata.title
set_title(c, metadata.title)
authors = metadata.authors.join(' & ') if metadata.authors else _('Unknown')
alt = _('{} by {}').format(metadata.title, authors)
@@ -453,6 +455,7 @@ def add_top_bar_buttons(container_id):
container = document.getElementById(container_id)
if container:
clear_buttons(container)
+ add_button(container, 'trash', action=delete_book, tooltip=_('Delete this book'))
book_id = parse_url_params().book_id
if book_id is '0':
add_button(container, 'random', def(): fetch_metadata(container_id, 0);)
@@ -630,6 +633,10 @@ def search_internet(container_id):
))
+
+def delete_book():
+ start_delete_book(current_library_id(), render_book.book_id, render_book.title or _('Unknown'))
+
set_panel_handler('book_details', init)
set_panel_handler('book_details^more_actions', create_more_actions_panel)
set_panel_handler('book_details^search_internet', search_internet)
diff --git a/src/pyj/book_list/delete_book.pyj b/src/pyj/book_list/delete_book.pyj
new file mode 100644
index 0000000000..0a23342202
--- /dev/null
+++ b/src/pyj/book_list/delete_book.pyj
@@ -0,0 +1,110 @@
+# vim:fileencoding=utf-8
+# License: GPL v3 Copyright: 2018, Kovid Goyal
+from __python__ import bound_methods, hash_literals
+
+from elementmaker import E
+from gettext import gettext as _
+
+from ajax import ajax
+from book_list.add import write_access_error
+from book_list.library_data import book_after, force_refresh_on_next_load
+from book_list.router import back
+from book_list.ui import show_panel
+from dom import clear
+from modals import create_custom_dialog
+from read_book.db import get_db
+from utils import safe_set_inner_html
+from widgets import create_button
+
+
+def delete_from_cache(library_id, book_id, title):
+ db = get_db()
+ db.delete_books_matching(library_id, book_id)
+
+
+def do_delete_from_library(parent, close_modal, library_id, book_id, title):
+ parent.appendChild(E.div(_('Deleting {0} from library, please wait...').format(title)))
+
+ def oncomplete(end_type, xhr, ev):
+ if end_type is 'load' or end_type is 'abort':
+ close_modal()
+ if end_type is 'load':
+ force_refresh_on_next_load()
+ next_book_id = book_after(book_id)
+ if next_book_id:
+ show_panel('book_details', {'book_id': str(next_book_id), 'library_id': library_id}, True)
+ else:
+ back()
+ return
+ clear(parent)
+ msg = E.div()
+ safe_set_inner_html(msg, write_access_error(_('Failed to delete {0}, with error:').format(title), xhr))
+ parent.appendChild(E.div(
+ msg,
+ E.div(class_='button-box',
+ create_button(_('Close'), None, close_modal, True)
+ )
+ ))
+
+ ajax(f'cdb/delete-books/{book_id}/{library_id}', oncomplete, method='POST').send()
+
+
+def confirm_delete_from_library(library_id, book_id, title):
+ create_custom_dialog(_('Are you sure?'), def(parent, close_modal):
+
+ def action(doit):
+ if doit:
+ clear(parent)
+ do_delete_from_library(parent, close_modal, library_id, book_id, title)
+ else:
+ close_modal()
+
+ msg = _('This will permanently delete {0} from your calibre library. Are you sure?').format(title)
+ m = E.div()
+ safe_set_inner_html(m, msg)
+ parent.appendChild(E.div(
+ m,
+ E.div(class_='button-box',
+ create_button(_('OK'), None, action.bind(None, True)),
+ '\xa0',
+ create_button(_('Cancel'), None, action.bind(None, False), highlight=True),
+ )
+ ))
+ )
+
+
+def choose_which_delete(library_id, book_id, title):
+
+ create_custom_dialog(_('Delete from?'), def(parent, close_modal):
+
+ def action(from_cache, from_library):
+ close_modal()
+ if from_cache:
+ delete_from_cache(library_id, book_id, title)
+ if from_library:
+ confirm_delete_from_library(library_id, book_id, title)
+
+
+ parent.appendChild(E.div(
+ E.div(_('{0} is available both in the browser cache for offline reading and in'
+ ' your calibre library. Where do you want to delete it from?').format(title)),
+ E.div(class_='button-box',
+ create_button(_('Cache'), None, action.bind(None, True, False)),
+ '\xa0',
+ create_button(_('Library and cache'), None, action.bind(None, True, True)),
+ )
+ ))
+ )
+
+
+def delete_book_stage2(library_id, book_id, title, has_offline_copies):
+ if has_offline_copies:
+ choose_which_delete(library_id, book_id, title)
+ else:
+ confirm_delete_from_library(library_id, book_id, title)
+
+
+def start_delete_book(library_id, book_id, title):
+ book_id = int(book_id)
+ db = get_db()
+ db.has_book_matching(library_id, book_id, delete_book_stage2.bind(None, library_id, book_id, title))
diff --git a/src/pyj/book_list/library_data.pyj b/src/pyj/book_list/library_data.pyj
index e1eb06052b..f25e956be1 100644
--- a/src/pyj/book_list/library_data.pyj
+++ b/src/pyj/book_list/library_data.pyj
@@ -97,6 +97,16 @@ def current_book_ids():
return library_data.previous_book_ids.concat(library_data.search_result.book_ids)
+def book_after(book_id):
+ ids = current_book_ids()
+ idx = ids.indexOf(int(book_id))
+ if idx > -1:
+ if idx < ids.length - 1:
+ return ids[idx + 1]
+ if idx > 0: # wrap around
+ return ids[0]
+
+
def on_data_loaded(end_type, xhr, ev):
load_status.current_fetch = None
def bad_load(msg):
diff --git a/src/pyj/read_book/db.pyj b/src/pyj/read_book/db.pyj
index 631f2274b8..3d50b6dafd 100644
--- a/src/pyj/read_book/db.pyj
+++ b/src/pyj/read_book/db.pyj
@@ -348,6 +348,25 @@ class DB:
if cursor:
cursor.continue()
+ def has_book_matching(self, library_id, book_id, proceed):
+ # Should really be using a multiEntry index to avoid iterating over all
+ # books in JS, but since the number of stored books is not that large,
+ # I can't be bothered.
+ c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
+ c.onerror = def(event):
+ proceed(False)
+ book_id = int(book_id)
+ c.onsuccess = def (ev):
+ cursor = ev.target.result
+ if cursor:
+ book = cursor.value
+ if book.key[0] is library_id and book.key[1] is book_id:
+ proceed(True)
+ return
+ cursor.continue()
+ else:
+ proceed(False)
+
def delete_book(self, book, proceed):
c = self.idb.transaction(['books', 'files'], 'readwrite')
files = c.objectStore('files')
@@ -366,6 +385,31 @@ class DB:
books.delete(book.key)
next_step()
+ def delete_books_matching(self, library_id, book_id, proceed):
+ c = self.idb.transaction(['books'], 'readonly').objectStore('books').index('recent_index').openCursor(None, 'prev')
+ c.onerror = def(event):
+ pass
+ book_id = int(book_id)
+ matches = v'[]'
+
+ def delete_all():
+ if matches.length:
+ book = matches.pop()
+ self.delete_book(book, delete_all)
+ else:
+ if proceed:
+ proceed()
+
+ c.onsuccess = def (ev):
+ cursor = ev.target.result
+ if cursor:
+ book = cursor.value
+ if book.key[0] is library_id and book.key[1] is book_id:
+ matches.push(book)
+ cursor.continue()
+ else:
+ delete_all()
+
def get_db(callback, show_read_book_error):
if not get_db.ans: