From a80735d7d389d622639e1026db8991476c8058f2 Mon Sep 17 00:00:00 2001 From: Olivier Date: Tue, 11 Oct 2022 23:37:12 +0900 Subject: [PATCH 01/16] Save the position of the last read page for comics --- cps/static/js/kthoom.js | 35 +++-- cps/templates/readcbr.html | 305 ++++++++++++++++++++----------------- cps/web.py | 2 +- 3 files changed, 186 insertions(+), 156 deletions(-) diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index 51dbadba..c587206f 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -70,8 +70,8 @@ var settings = { fitMode: kthoom.Key.B, theme: "light", direction: 0, // 0 = Left to Right, 1 = Right to Left - nextPage: 0, // 0 = Reset to Top, 1 = Remember Position - scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar + nextPage: 0, // 0 = Reset to Top, 1 = Remember Position + scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar }; kthoom.saveSettings = function() { @@ -130,7 +130,7 @@ var createURLFromArray = function(array, mimeType) { } if ((typeof URL !== "function" && typeof URL !== "object") || - typeof URL.createObjectURL !== "function") { + typeof URL.createObjectURL !== "function") { throw "Browser support for Object URLs is missing"; } @@ -186,8 +186,7 @@ function initProgressClick() { }); } -function loadFromArrayBuffer(ab) { - var lastCompletion = 0; +function loadFromArrayBuffer(ab, lastCompletion = 0) { const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); loadArchiveFormats(['rar', 'zip', 'tar'], function() { // Open the file as an archive @@ -244,7 +243,7 @@ function updatePage() { // Mark the current page in the TOC $("#tocView a[data-page]") - // Remove the currently active thumbnail + // Remove the currently active thumbnail .removeClass("active") // Find the new one .filter("[data-page=" + (currentImage + 1) + "]") @@ -261,7 +260,7 @@ function updatePage() { } $("body").toggleClass("dark-theme", settings.theme === "dark"); - $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); + $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); kthoom.setSettings(); kthoom.saveSettings(); @@ -335,7 +334,7 @@ function setImage(url) { x.font = "50px sans-serif"; x.strokeStyle = "black"; x.fillText("Page #" + (currentImage + 1) + " (" + - imageFiles[currentImage].filename + ")", innerWidth / 2, 100); + imageFiles[currentImage].filename + ")", innerWidth / 2, 100); x.fillStyle = "black"; x.fillText("Is corrupt or not an image", innerWidth / 2, 200); @@ -418,9 +417,9 @@ function showPrevPage() { currentImage++; } else { updatePage(); - if (settings.nextPage === 0) { - $("#mainContent").scrollTop(0); - } + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -431,9 +430,9 @@ function showNextPage() { currentImage--; } else { updatePage(); - if (settings.nextPage === 0) { - $("#mainContent").scrollTop(0); - } + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -551,7 +550,7 @@ function init(filename) { request.responseType = "arraybuffer"; request.addEventListener("load", function() { if (request.status >= 200 && request.status < 300) { - loadFromArrayBuffer(request.response); + loadFromArrayBuffer(request.response, currentImage); } else { console.warn(request.statusText, request.responseText); } @@ -609,9 +608,9 @@ function init(filename) { $("#thumbnails").on("click", "a", function() { currentImage = $(this).data("page") - 1; updatePage(); - if (settings.nextPage === 0) { - $("#mainContent").scrollTop(0); - } + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } }); // Fullscreen mode diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index 411e3fdd..6ab14a94 100644 --- a/cps/templates/readcbr.html +++ b/cps/templates/readcbr.html @@ -1,5 +1,6 @@ + @@ -17,78 +18,61 @@ - - - -
-
-
- Menu -
-
- {{ title | shortentitle }} -   –   - + {% endif %} - {% endif %} - {% if current_user.kindle_mail and entry.email_share_list %} - - {% if entry.email_share_list.__len__() == 1 %} -
- -
- {% else %} -
- - -
+ {% if current_user.kindle_mail and entry.email_share_list %} + + {% if entry.email_share_list.__len__() == 1 %} +
+ +
+ {% else %} +
+ + +
+ {% endif %} {% endif %} {% endif %} {% if entry.reader_list and current_user.role_viewer() %} From 2f12b2e315d3a7cc811a6e8dfbbcbc0893018036 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 29 Jul 2023 15:32:35 +0200 Subject: [PATCH 09/16] Handle invalid or missing container.xml during kobo sync (Fix for #2840) --- cps/epub.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/cps/epub.py b/cps/epub.py index c22bad7b..22652c63 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -21,10 +21,11 @@ import zipfile from lxml import etree from . import isoLanguages, cover -from . import config +from . import config, logger from .helper import split_authors from .constants import BookMeta +log = logger.create() def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name): if cover_file is None: @@ -49,15 +50,20 @@ def get_epub_layout(book, book_data): } file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower())) - epubZip = zipfile.ZipFile(file_path) - txt = epubZip.read('META-INF/container.xml') - tree = etree.fromstring(txt) - cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] - cf = epubZip.read(cfname) - tree = etree.fromstring(cf) - p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] + try: + epubZip = zipfile.ZipFile(file_path) + txt = epubZip.read('META-INF/container.xml') + tree = etree.fromstring(txt) + cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0] + cf = epubZip.read(cfname) + + tree = etree.fromstring(cf) + p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] - layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) + layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) + except (etree.XMLSyntaxError, KeyError) as e: + log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e)) + layout = [] if len(layout) == 0: return None From 3efcbcc6797e4e4faf4b611985087b247552ccc1 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 30 Jul 2023 07:44:16 +0200 Subject: [PATCH 10/16] Fix deprecated text parameter --- cps/metadata_provider/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/metadata_provider/amazon.py b/cps/metadata_provider/amazon.py index a83747e6..30291a3f 100644 --- a/cps/metadata_provider/amazon.py +++ b/cps/metadata_provider/amazon.py @@ -98,7 +98,7 @@ class Amazon(Metadata): try: match.authors = [next( filter(lambda i: i != " " and i != "\n" and not i.startswith("{"), - x.findAll(text=True))).strip() + x.findAll(string=True))).strip() for x in soup2.findAll("span", attrs={"class": "author"})] except (AttributeError, TypeError, StopIteration): match.authors = "" From 34c6010ad0613e374f4835417192dda799f706d5 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sun, 30 Jul 2023 19:57:42 +0200 Subject: [PATCH 11/16] Catch additional error during kobo detect layout --- cps/epub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/epub.py b/cps/epub.py index 22652c63..50adba59 100644 --- a/cps/epub.py +++ b/cps/epub.py @@ -61,7 +61,7 @@ def get_epub_layout(book, book_data): p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0] layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns) - except (etree.XMLSyntaxError, KeyError) as e: + except (etree.XMLSyntaxError, KeyError, IndexError) as e: log.error("Could not parse epub metadata of book {} during kobo sync: {}".format(book.id, e)) layout = [] From 7818c4a7b0f6ca133c04185bd3c520e6bca7d6ba Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Tue, 8 Aug 2023 19:21:47 +0200 Subject: [PATCH 12/16] Adapt cover size to kobo sync requested sze --- cps/kobo.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cps/kobo.py b/cps/kobo.py index dfda6483..c8fecd1c 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -930,7 +930,8 @@ def get_current_bookmark_response(current_bookmark): @kobo.route("//////image.jpg") @requires_kobo_auth def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale): - book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=COVER_THUMBNAIL_SMALL) + resolution = None if height > 1000 else COVER_THUMBNAIL_SMALL + book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=resolution) if book_cover: log.debug("Serving local cover image of book %s" % book_uuid) return book_cover From d253804a50fd6bb8d8348d61f65a2cd9c50747a7 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Wed, 16 Aug 2023 18:04:45 +0200 Subject: [PATCH 13/16] fix for #2865 (Kobo sync fails during cover request) --- cps/kobo.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cps/kobo.py b/cps/kobo.py index c8fecd1c..1655fb5e 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -930,7 +930,11 @@ def get_current_bookmark_response(current_bookmark): @kobo.route("//////image.jpg") @requires_kobo_auth def HandleCoverImageRequest(book_uuid, width, height, Quality, isGreyscale): - resolution = None if height > 1000 else COVER_THUMBNAIL_SMALL + try: + resolution = None if int(height) > 1000 else COVER_THUMBNAIL_SMALL + except ValueError: + log.error("Requested height %s of book %s is invalid" % (book_uuid, height)) + resolution = COVER_THUMBNAIL_SMALL book_cover = helper.get_book_cover_with_uuid(book_uuid, resolution=resolution) if book_cover: log.debug("Serving local cover image of book %s" % book_uuid) From 9b99427c84da9ba1a4f931b014a98caf90dbbf8e Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 26 Aug 2023 20:12:34 +0200 Subject: [PATCH 14/16] Merged comic save position --- cps/static/js/kthoom.js | 55 +++++++---- cps/templates/readcbr.html | 195 ++++++++++++++++++++++--------------- cps/web.py | 2 +- 3 files changed, 158 insertions(+), 94 deletions(-) diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index d955dfb1..84992450 100644 --- a/cps/static/js/kthoom.js +++ b/cps/static/js/kthoom.js @@ -71,7 +71,8 @@ var settings = { fitMode: kthoom.Key.B, theme: "light", direction: 0, // 0 = Left to Right, 1 = Right to Left - scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar + nextPage: 0, // 0 = Reset to Top, 1 = Remember Position + scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar pageDisplay: 0 // 0 = Single Page, 1 = Long Strip }; @@ -131,7 +132,7 @@ var createURLFromArray = function(array, mimeType) { } if ((typeof URL !== "function" && typeof URL !== "object") || - typeof URL.createObjectURL !== "function") { + typeof URL.createObjectURL !== "function") { throw "Browser support for Object URLs is missing"; } @@ -187,7 +188,7 @@ function initProgressClick() { }); } -function loadFromArrayBuffer(ab) { +function loadFromArrayBuffer(ab, lastCompletion = 0) { const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); loadArchiveFormats(['rar', 'zip', 'tar'], function() { // Open the file as an archive @@ -222,7 +223,7 @@ function loadFromArrayBuffer(ab) { // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { - updatePage(); + updatePage(lastCompletion); } } else { totalImages--; @@ -237,30 +238,38 @@ function loadFromArrayBuffer(ab) { } function scrollTocToActive() { + // Scroll to the thumbnail in the TOC on page change + $("#tocView").stop().animate({ + scrollTop: $("#tocView a.active").position().top + }, 200); +} + +function updatePage() { $(".page").text((currentImage + 1 ) + "/" + totalImages); // Mark the current page in the TOC $("#tocView a[data-page]") - // Remove the currently active thumbnail + // Remove the currently active thumbnail .removeClass("active") // Find the new one .filter("[data-page=" + (currentImage + 1) + "]") // Set it to active .addClass("active"); - // Scroll to the thumbnail in the TOC on page change - $("#tocView").stop().animate({ - scrollTop: $("#tocView a.active").position().top - }, 200); -} - -function updatePage() { scrollTocToActive(); - scrollCurrentImageIntoView(); updateProgress(); pageDisplayUpdate(); setTheme(); + if (imageFiles[currentImage]) { + setImage(imageFiles[currentImage].dataURI); + } else { + setImage("loading"); + } + + $("body").toggleClass("dark-theme", settings.theme === "dark"); + $("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0); + kthoom.setSettings(); kthoom.saveSettings(); } @@ -335,6 +344,7 @@ function setImage(url, _canvas) { img.onerror = function() { canvas.width = innerWidth - 100; canvas.height = 300; + updateScale(true); x.fillStyle = "black"; x.font = "50px sans-serif"; x.strokeStyle = "black"; @@ -388,6 +398,8 @@ function setImage(url, _canvas) { scrollTo(0, 0); x.drawImage(img, 0, 0); + updateScale(false); + canvas.style.display = ""; $("body").css("overflowY", ""); x.restore(); @@ -426,6 +438,9 @@ function showPrevPage() { currentImage++; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -436,6 +451,9 @@ function showNextPage() { currentImage--; } else { updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } } } @@ -451,7 +469,7 @@ function scrollCurrentImageIntoView() { } } -function updateScale() { +function updateScale(clear) { var canvasArray = $("#mainContent > canvas"); var maxheight = innerHeight - 50; @@ -460,7 +478,7 @@ function updateScale() { canvasArray.css("maxWidth", ""); canvasArray.css("maxHeight", ""); - if(settings.pageDisplay === 0) { + if(!clear) { canvasArray.addClass("hide"); pageDisplayUpdate(); } @@ -627,7 +645,7 @@ function init(filename) { request.responseType = "arraybuffer"; request.addEventListener("load", function() { if (request.status >= 200 && request.status < 300) { - loadFromArrayBuffer(request.response); + loadFromArrayBuffer(request.response, currentImage); } else { console.warn(request.statusText, request.responseText); } @@ -681,7 +699,7 @@ function init(filename) { } updatePage(); - updateScale(); + updateScale(false); }); // Close modal @@ -694,6 +712,9 @@ function init(filename) { $("#thumbnails").on("click", "a", function() { currentImage = $(this).data("page") - 1; updatePage(); + if (settings.nextPage === 0) { + $("#mainContent").scrollTop(0); + } }); // Fullscreen mode diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index a4ac3722..c4f49aec 100644 --- a/cps/templates/readcbr.html +++ b/cps/templates/readcbr.html @@ -1,5 +1,6 @@ + @@ -20,23 +21,6 @@ - -
- - - - - - - - - - +
{{_('Settings')}}
{{_('Theme')}}: -
+
+
+
+ + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - -
{{_('Settings')}}
{{_('Theme')}}: +
@@ -139,59 +123,118 @@ - -
{{_('Rotate')}}: -
- - - - -
-
{{_('Flip')}}: -
- - -
-
{{_('Direction')}}: -
+
+
{{_('Rotate')}}: +
+ + + + +
+
{{_('Flip')}}: +
+ + +
+
{{_('Direction')}}: +
{{_('Next Page')}}: +
+ + +
+
{{_('Scrollbar')}}:
-
-
+
+ + + + +
+
-
- -
- +
+ + + + diff --git a/cps/web.py b/cps/web.py index 51ff32b3..9793f01a 100755 --- a/cps/web.py +++ b/cps/web.py @@ -1561,7 +1561,7 @@ def read_book(book_id, book_format): title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.') log.debug("Start comic reader for %d", book_id) return render_title_template('readcbr.html', comicfile=all_name, title=title, - extension=fileExt) + extension=fileExt, bookmark=bookmark) log.debug("Selected book is unavailable. File does not exist or is not accessible") flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"), category="error") From 52172044e6bdb963c847e212f2d427d30ffd87c2 Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 26 Aug 2023 20:12:52 +0200 Subject: [PATCH 15/16] Fix deprecation warnings --- cps/admin.py | 2 +- cps/editbooks.py | 5 ++--- cps/services/SyncToken.py | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/cps/admin.py b/cps/admin.py index 93c1a3a9..045a9523 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -33,7 +33,7 @@ from functools import wraps from urllib.parse import urlparse from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response -from flask import Markup +from markupsafe import Markup from flask_login import login_required, current_user, logout_user from flask_babel import gettext as _ from flask_babel import get_locale, format_time, format_datetime, format_timedelta diff --git a/cps/editbooks.py b/cps/editbooks.py index 5a15740c..40f62713 100755 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -25,16 +25,15 @@ from datetime import datetime import json from shutil import copyfile from uuid import uuid4 -from markupsafe import escape # dependency of flask +from markupsafe import escape, Markup # dependency of flask from functools import wraps -import re try: from lxml.html.clean import clean_html, Cleaner except ImportError: clean_html = None -from flask import Blueprint, request, flash, redirect, url_for, abort, Markup, Response +from flask import Blueprint, request, flash, redirect, url_for, abort, Response from flask_babel import gettext as _ from flask_babel import lazy_gettext as N_ from flask_babel import get_locale diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index c44841c1..3af0e276 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -19,10 +19,9 @@ import sys from base64 import b64decode, b64encode -from jsonschema import validate, exceptions, __version__ -from datetime import datetime, timezone +from jsonschema import validate, exceptions +from datetime import datetime -from urllib.parse import unquote from flask import json from .. import logger From b2a26a421c2c2e7163e5a71b33e2dd50f753f09b Mon Sep 17 00:00:00 2001 From: Ozzie Isaacs Date: Sat, 26 Aug 2023 20:26:01 +0200 Subject: [PATCH 16/16] Removed deprecation warning for sqlalchemy2.0 --- cps/db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cps/db.py b/cps/db.py index f0295fe5..ceb692ec 100644 --- a/cps/db.py +++ b/cps/db.py @@ -663,7 +663,7 @@ class CalibreDB: cls.session_factory = scoped_session(sessionmaker(autocommit=False, autoflush=True, - bind=cls.engine)) + bind=cls.engine, future=True)) for inst in cls.instances: inst.init_session()