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/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() diff --git a/cps/editbooks.py b/cps/editbooks.py index f52f08aa..b8f6363f 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/kobo.py b/cps/kobo.py index 582cafc3..ee394509 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -166,12 +166,6 @@ def HandleSyncRequest(): only_kobo_shelves = current_user.kobo_only_shelves_sync if only_kobo_shelves: - #if sqlalchemy_version2: - # changed_entries = select(db.Books, - # ub.ArchivedBook.last_modified, - # ub.BookShelf.date_added, - # ub.ArchivedBook.is_archived) - #else: changed_entries = calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.BookShelf.date_added, @@ -192,9 +186,6 @@ def HandleSyncRequest(): .filter(ub.Shelf.kobo_sync) .distinct()) else: - #if sqlalchemy_version2: - # changed_entries = select(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) - #else: changed_entries = calibre_db.session.query(db.Books, ub.ArchivedBook.last_modified, ub.ArchivedBook.is_archived) @@ -209,9 +200,6 @@ def HandleSyncRequest(): .order_by(db.Books.id)) reading_states_in_new_entitlements = [] - #if sqlalchemy_version2: - # books = calibre_db.session.execute(changed_entries.limit(SYNC_ITEM_LIMIT)) - #else: books = changed_entries.limit(SYNC_ITEM_LIMIT) log.debug("Books to Sync: {}".format(len(books.all()))) for book in books: @@ -255,13 +243,6 @@ def HandleSyncRequest(): new_books_last_created = max(ts_created, new_books_last_created) kobo_sync_status.add_synced_books(book.Books.id) - '''if sqlalchemy_version2: - max_change = calibre_db.session.execute(changed_entries - .filter(ub.ArchivedBook.is_archived) - .filter(ub.ArchivedBook.user_id == current_user.id) - .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()))\ - .columns(db.Books).first() - else:''' max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\ .filter(ub.ArchivedBook.user_id == current_user.id) \ .order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first() @@ -271,10 +252,6 @@ def HandleSyncRequest(): new_archived_last_modified = max(new_archived_last_modified, max_change) # no. of books returned - '''if sqlalchemy_version2: - entries = calibre_db.session.execute(changed_entries).all() - book_count = len(entries) - else:''' book_count = changed_entries.count() # last entry: cont_sync = bool(book_count) @@ -523,7 +500,7 @@ def get_metadata(book): @requires_kobo_auth # Creates a Shelf with the given items, and returns the shelf's uuid. def HandleTagCreate(): - # catch delete requests, otherwise the are handled in the book delete handler + # catch delete requests, otherwise they are handled in the book delete handler if request.method == "DELETE": abort(405) name, items = None, None @@ -717,14 +694,6 @@ def sync_shelves(sync_token, sync_results, only_kobo_shelves=False): }) extra_filters.append(ub.Shelf.kobo_sync) - '''if sqlalchemy_version2: - shelflist = ub.session.execute(select(ub.Shelf).outerjoin(ub.BookShelf).filter( - or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, - func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified), - ub.Shelf.user_id == current_user.id, - *extra_filters - ).distinct().order_by(func.datetime(ub.Shelf.last_modified).asc())).columns(ub.Shelf) - else:''' shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter( or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, func.datetime(ub.BookShelf.date_added) > sync_token.tags_last_modified), diff --git a/cps/server.py b/cps/server.py index ed3b7716..ed5913bb 100644 --- a/cps/server.py +++ b/cps/server.py @@ -288,4 +288,7 @@ class WebServer(object): if _GEVENT: self.wsgiserver.close() else: - self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop) + if restart: + self.wsgiserver.call_later(1.0, self.wsgiserver.stop) + else: + self.wsgiserver.add_callback_from_signal(self.wsgiserver.stop) diff --git a/cps/services/SyncToken.py b/cps/services/SyncToken.py index c44841c1..bf31a7bc 100644 --- a/cps/services/SyncToken.py +++ b/cps/services/SyncToken.py @@ -19,10 +19,8 @@ import sys from base64 import b64decode, b64encode -from jsonschema import validate, exceptions, __version__ -from datetime import datetime, timezone - -from urllib.parse import unquote +from jsonschema import validate, exceptions +from datetime import datetime from flask import json from .. import logger diff --git a/cps/static/js/kthoom.js b/cps/static/js/kthoom.js index d955dfb1..33e4b2fc 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,8 +132,8 @@ var createURLFromArray = function(array, mimeType) { } if ((typeof URL !== "function" && typeof URL !== "object") || - typeof URL.createObjectURL !== "function") { - throw "Browser support for Object URLs is missing"; + typeof URL.createObjectURL !== "function") { + throw "Browser support for Object URLs is missing"; } return URL.createObjectURL(blob); @@ -177,12 +178,36 @@ kthoom.ImageFile = function(file) { } }; +function updateDirectionButtons(){ + $("#right").show(); + $("#left").show(); + if (currentImage == 0 ) { + if (settings.direction === 0) { + $("#right").show(); + $("#left").hide(); + } else { + $("#left").show(); + $("#right").hide(); + } + } + if ((currentImage + 1) >= Math.max(totalImages, imageFiles.length)) { + if (settings.direction === 0) { + $("#left").show(); + $("#right").hide(); + } else { + $("#right").show(); + $("#left").hide(); + } + } +} function initProgressClick() { $("#progress").click(function(e) { var offset = $(this).offset(); var x = e.pageX - offset.left; var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1; + updateDirectionButtons(); + setBookmark(); updatePage(); }); } @@ -222,6 +247,7 @@ function loadFromArrayBuffer(ab) { // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { + updateDirectionButtons(); updatePage(); } } else { @@ -241,7 +267,7 @@ function scrollTocToActive() { // 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) + "]") @@ -409,6 +435,7 @@ function showLeftPage() { } else { showNextPage(); } + setBookmark(); } function showRightPage() { @@ -417,6 +444,7 @@ function showRightPage() { } else { showPrevPage(); } + setBookmark(); } function showPrevPage() { @@ -427,6 +455,7 @@ function showPrevPage() { } else { updatePage(); } + updateDirectionButtons(); } function showNextPage() { @@ -437,6 +466,7 @@ function showNextPage() { } else { updatePage(); } + updateDirectionButtons(); } function scrollCurrentImageIntoView() { @@ -621,11 +651,21 @@ function drawCanvas() { $("#mainContent").append(canvasElement); } +function updateArrows() { + if ($('input[name="direction"]:checked').val() === "0") { + $("#prev_page_key").html("←"); + $("#next_page_key").html("→"); + } else { + $("#prev_page_key").html("→"); + $("#next_page_key").html("←"); + } +}; + function init(filename) { var request = new XMLHttpRequest(); request.open("GET", filename); request.responseType = "arraybuffer"; - request.addEventListener("load", function() { + request.addEventListener("load", function () { if (request.status >= 200 && request.status < 300) { loadFromArrayBuffer(request.response); } else { @@ -641,18 +681,18 @@ function init(filename) { $(document).keydown(keyHandler); - $(window).resize(function() { + $(window).resize(function () { updateScale(); }); // Open TOC menu - $("#slider").click(function() { + $("#slider").click(function () { $("#sidebar").toggleClass("open"); $("#main").toggleClass("closed"); $(this).toggleClass("icon-menu icon-right"); // We need this in a timeout because if we call it during the CSS transition, IE11 shakes the page ¯\_(ツ)_/¯ - setTimeout(function() { + setTimeout(function () { // Focus on the TOC or the main content area, depending on which is open $("#main:not(.closed) #mainContent, #sidebar.open #tocView").focus(); scrollTocToActive(); @@ -660,12 +700,12 @@ function init(filename) { }); // Open Settings modal - $("#setting").click(function() { + $("#setting").click(function () { $("#settings-modal").toggleClass("md-show"); }); // On Settings input change - $("#settings input").on("change", function() { + $("#settings input").on("change", function () { // Get either the checked boolean or the assigned value var value = this.type === "checkbox" ? this.checked : this.value; @@ -674,39 +714,40 @@ function init(filename) { settings[this.name] = value; - if(["hflip", "vflip", "rotateTimes"].includes(this.name)) { + if (["hflip", "vflip", "rotateTimes"].includes(this.name)) { reloadImages(); - } else if(this.name === "direction") { + } else if (this.name === "direction") { + updateDirectionButtons(); return updateProgress(); } - + updatePage(); updateScale(); }); // Close modal - $(".closer, .overlay").click(function() { + $(".closer, .overlay").click(function () { $(".md-show").removeClass("md-show"); - $("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it + $("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it }); // TOC thumbnail pagination - $("#thumbnails").on("click", "a", function() { + $("#thumbnails").on("click", "a", function () { currentImage = $(this).data("page") - 1; updatePage(); }); // Fullscreen mode if (typeof screenfull !== "undefined") { - $("#fullscreen").click(function() { + $("#fullscreen").click(function () { screenfull.toggle($("#container")[0]); - // Focus on main container so you can use up/down keys immediately after fullscreen - $("#mainContent").focus(); + // Focus on main container so you can use up/down keys immediately after fullscreen + $("#mainContent").focus(); }); if (screenfull.raw) { var $button = $("#fullscreen"); - document.addEventListener(screenfull.raw.fullscreenchange, function() { + document.addEventListener(screenfull.raw.fullscreenchange, function () { screenfull.isFullscreen ? $button.addClass("icon-resize-small").removeClass("icon-resize-full") : $button.addClass("icon-resize-full").removeClass("icon-resize-small"); @@ -717,16 +758,16 @@ function init(filename) { // Focus the scrollable area so that keyboard scrolling work as expected $("#mainContent").focus(); - $("#mainContent").swipe( { - swipeRight:function() { + $("#mainContent").swipe({ + swipeRight: function () { showLeftPage(); }, - swipeLeft:function() { + swipeLeft: function () { showRightPage(); }, }); - $(".mainImage").click(function(evt) { - // Firefox does not support offsetX/Y so we have to manually calculate + $(".mainImage").click(function (evt) { + // Firefox does not support offsetX/Y, so we have to manually calculate // where the user clicked in the image. var mainContentWidth = $("#mainContent").width(); var mainContentHeight = $("#mainContent").height(); @@ -762,30 +803,38 @@ function init(filename) { }); // Scrolling up/down will update current image if a new image is into view (for Long Strip Display) - $("#mainContent").scroll(function(){ + $("#mainContent").scroll(function (){ var scroll = $("#mainContent").scrollTop(); - if(settings.pageDisplay === 0) { + var viewLength = 0; + $(".mainImage").each(function(){ + viewLength += $(this).height(); + }); + if (settings.pageDisplay === 0) { // Don't trigger the scroll for Single Page - } else if(scroll > prevScrollPosition) { + } else if (scroll > prevScrollPosition) { //Scroll Down - if(currentImage + 1 < imageFiles.length) { - if(currentImageOffset(currentImage + 1) <= 1) { - currentImage++; + if (currentImage + 1 < imageFiles.length) { + if (currentImageOffset(currentImage + 1) <= 1) { + currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0); + if ( currentImage >= imageFiles.length) { + currentImage = imageFiles.length - 1; + } + console.log(currentImage); scrollTocToActive(); updateProgress(); } } } else { //Scroll Up - if(currentImage - 1 > -1 ) { - if(currentImageOffset(currentImage - 1) >= 0) { - currentImage--; + if (currentImage - 1 > -1) { + if (currentImageOffset(currentImage - 1) >= 0) { + currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0); + console.log(currentImage); scrollTocToActive(); updateProgress(); } } } - // Update scroll position prevScrollPosition = scroll; }); @@ -794,3 +843,31 @@ function init(filename) { function currentImageOffset(imageIndex) { return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top } + +function setBookmark() { + // get csrf_token + let csrf_token = $("input[name='csrf_token']").val(); + //This sends a bookmark update to calibreweb. + $.ajax(calibre.bookmarkUrl, { + method: "post", + data: { + csrf_token: csrf_token, + bookmark: currentImage + } + }).fail(function (xhr, status, error) { + console.error(error); + }); +} + +$(function() { + $('input[name="direction"]').change(function () { + updateArrows(); + }); + + $('#left').click(function () { + showLeftPage(); + }); + $('#right').click(function () { + showRightPage(); + }); +}); diff --git a/cps/static/js/main.js b/cps/static/js/main.js index 8d7354ef..34d3bc96 100644 --- a/cps/static/js/main.js +++ b/cps/static/js/main.js @@ -333,7 +333,6 @@ $(function() { } else { $("#parent").addClass('hidden') } - // console.log(data); data.files.forEach(function(entry) { if(entry.type === "dir") { var type = ""; diff --git a/cps/templates/readcbr.html b/cps/templates/readcbr.html index a4ac3722..39786ec2 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,83 @@ - -
{{_('Rotate')}}: -
- - - - -
-
{{_('Flip')}}: -
- - -
-
{{_('Direction')}}: -
+
+
{{_('Rotate')}}: +
+ + + + +
+
{{_('Flip')}}: +
+ + +
+
{{_('Direction')}}: +
{{_('Next Page')}}: +
+ + +
+
{{_('Scrollbar')}}:
-
-
+
+ + + + + +
-
- -
- +
+ + diff --git a/cps/tornado_wsgi.py b/cps/tornado_wsgi.py index af93219c..c1571ece 100644 --- a/cps/tornado_wsgi.py +++ b/cps/tornado_wsgi.py @@ -16,12 +16,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - from tornado.wsgi import WSGIContainer import tornado from tornado import escape from tornado import httputil +from tornado.ioloop import IOLoop from typing import List, Tuple, Optional, Callable, Any, Dict, Text from types import TracebackType @@ -34,61 +34,67 @@ if typing.TYPE_CHECKING: class MyWSGIContainer(WSGIContainer): def __call__(self, request: httputil.HTTPServerRequest) -> None: - data = {} # type: Dict[str, Any] - response = [] # type: List[bytes] + if tornado.version_info < (6, 3, 0, -99): + data = {} # type: Dict[str, Any] + response = [] # type: List[bytes] - def start_response( - status: str, - headers: List[Tuple[str, str]], - exc_info: Optional[ - Tuple[ - "Optional[Type[BaseException]]", - Optional[BaseException], - Optional[TracebackType], - ] - ] = None, - ) -> Callable[[bytes], Any]: - data["status"] = status - data["headers"] = headers - return response.append + def start_response( + status: str, + headers: List[Tuple[str, str]], + exc_info: Optional[ + Tuple[ + "Optional[Type[BaseException]]", + Optional[BaseException], + Optional[TracebackType], + ] + ] = None, + ) -> Callable[[bytes], Any]: + data["status"] = status + data["headers"] = headers + return response.append - app_response = self.wsgi_application( - MyWSGIContainer.environ(request), start_response - ) + app_response = self.wsgi_application( + MyWSGIContainer.environ(self, request), start_response + ) + try: + response.extend(app_response) + body = b"".join(response) + finally: + if hasattr(app_response, "close"): + app_response.close() # type: ignore + if not data: + raise Exception("WSGI app did not call start_response") + + status_code_str, reason = data["status"].split(" ", 1) + status_code = int(status_code_str) + headers = data["headers"] # type: List[Tuple[str, str]] + header_set = set(k.lower() for (k, v) in headers) + body = escape.utf8(body) + if status_code != 304: + if "content-length" not in header_set: + headers.append(("Content-Length", str(len(body)))) + if "content-type" not in header_set: + headers.append(("Content-Type", "text/html; charset=UTF-8")) + if "server" not in header_set: + headers.append(("Server", "TornadoServer/%s" % tornado.version)) + + start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason) + header_obj = httputil.HTTPHeaders() + for key, value in headers: + header_obj.add(key, value) + assert request.connection is not None + request.connection.write_headers(start_line, header_obj, chunk=body) + request.connection.finish() + self._log(status_code, request) + else: + IOLoop.current().spawn_callback(self.handle_request, request) + + + def environ(self, request: httputil.HTTPServerRequest) -> Dict[Text, Any]: try: - response.extend(app_response) - body = b"".join(response) - finally: - if hasattr(app_response, "close"): - app_response.close() # type: ignore - if not data: - raise Exception("WSGI app did not call start_response") - - status_code_str, reason = data["status"].split(" ", 1) - status_code = int(status_code_str) - headers = data["headers"] # type: List[Tuple[str, str]] - header_set = set(k.lower() for (k, v) in headers) - body = escape.utf8(body) - if status_code != 304: - if "content-length" not in header_set: - headers.append(("Content-Length", str(len(body)))) - if "content-type" not in header_set: - headers.append(("Content-Type", "text/html; charset=UTF-8")) - if "server" not in header_set: - headers.append(("Server", "TornadoServer/%s" % tornado.version)) - - start_line = httputil.ResponseStartLine("HTTP/1.1", status_code, reason) - header_obj = httputil.HTTPHeaders() - for key, value in headers: - header_obj.add(key, value) - assert request.connection is not None - request.connection.write_headers(start_line, header_obj, chunk=body) - request.connection.finish() - self._log(status_code, request) - - @staticmethod - def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]: - environ = WSGIContainer.environ(request) + environ = WSGIContainer.environ(self, request) + except TypeError as e: + environ = WSGIContainer.environ(request) environ['RAW_URI'] = request.path return environ diff --git a/cps/web.py b/cps/web.py index 9f94d1b4..4430dd90 100755 --- a/cps/web.py +++ b/cps/web.py @@ -1014,7 +1014,7 @@ def series_list(): func.max(db.Books.series_index), db.Books.id) .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters()) .group_by(text('books_series_link.series')) - .having(func.max(db.Books.series_index)) + .having(or_(func.max(db.Books.series_index), db.Books.series_index=="")) .order_by(order) .all()) return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list, @@ -1569,7 +1569,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") diff --git a/optional-requirements.txt b/optional-requirements.txt index d34d09aa..6e82fd60 100644 --- a/optional-requirements.txt +++ b/optional-requirements.txt @@ -1,31 +1,31 @@ # GDrive Integration -google-api-python-client>=1.7.11,<2.90.0 -gevent>20.6.0,<23.0.0 +google-api-python-client>=1.7.11,<2.98.0 +gevent>20.6.0,<24.0.0 greenlet>=0.4.17,<2.1.0 httplib2>=0.9.2,<0.23.0 oauth2client>=4.0.0,<4.1.4 uritemplate>=3.0.0,<4.2.0 pyasn1-modules>=0.0.8,<0.4.0 pyasn1>=0.1.9,<0.6.0 -PyDrive2>=1.3.1,<1.16.0 -PyYAML>=3.12 +PyDrive2>=1.3.1,<1.18.0 +PyYAML>=3.12,<6.1 rsa>=3.4.2,<4.10.0 # Gmail -google-auth-oauthlib>=0.4.3,<0.9.0 -google-api-python-client>=1.7.11,<2.90.0 +google-auth-oauthlib>=0.4.3,<1.1.0 +google-api-python-client>=1.7.11,<2.98.0 # goodreads goodreads>=0.3.2,<0.4.0 -python-Levenshtein>=0.12.0,<0.21.0 +python-Levenshtein>=0.12.0,<0.22.0 # ldap login python-ldap>=3.0.0,<3.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0 # oauth -Flask-Dance>=2.0.0,<6.3.0 -SQLAlchemy-Utils>=0.33.5,<0.40.0 +Flask-Dance>=2.0.0,<7.1.0 +SQLAlchemy-Utils>=0.33.5,<0.42.0 # metadata extraction rarfile>=3.2 @@ -33,8 +33,8 @@ scholarly>=1.2.0,<1.8 markdown2>=2.0.0,<2.5.0 html2text>=2020.1.16,<2022.1.1 python-dateutil>=2.1,<2.9.0 -beautifulsoup4>=4.0.1,<4.12.0 -faust-cchardet>=2.1.18 +beautifulsoup4>=4.0.1,<4.13.0 +faust-cchardet>=2.1.18,<2.1.20 py7zr>=0.15.0,<0.21.0 # Comics @@ -42,4 +42,4 @@ natsort>=2.2.0,<8.4.0 comicapi>=2.2.0,<3.3.0 # Kobo integration -jsonschema>=3.2.0,<4.18.0 +jsonschema>=3.2.0,<4.20.0 diff --git a/requirements.txt b/requirements.txt index 2566e348..e384ed87 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,20 +1,20 @@ Werkzeug<3.0.0 APScheduler>=3.6.3,<3.11.0 Babel>=1.3,<3.0 -Flask-Babel>=0.11.1,<3.1.0 +Flask-Babel>=0.11.1,<3.2.0 Flask-Login>=0.3.2,<0.6.3 Flask-Principal>=0.3.2,<0.5.1 Flask>=1.0.2,<2.4.0 iso-639>=0.4.5,<0.5.0 -PyPDF>=3.0.0,<3.8.0 +PyPDF>=3.0.0,<3.16.0 pytz>=2016.10 -requests>=2.11.1,<2.29.0 +requests>=2.28.0,<2.32.0 SQLAlchemy>=1.3.0,<2.0.0 -tornado>=4.1,<6.3 +tornado>=6.3,<6.4 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 lxml>=3.8.0,<5.0.0 flask-wtf>=0.14.2,<1.2.0 chardet>=3.0.0,<4.1.0 advocate>=1.0.0,<1.1.0 -Flask-Limiter>=2.3.0,<3.4.0 +Flask-Limiter>=2.3.0,<3.5.0 diff --git a/setup.cfg b/setup.cfg index 1f617648..11703785 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,64 +38,65 @@ console_scripts = [options] include_package_data = True install_requires = + Werkzeug<3.0.0 APScheduler>=3.6.3,<3.11.0 Babel>=1.3,<3.0 - Flask-Babel>=0.11.1,<3.1.0 + Flask-Babel>=0.11.1,<3.2.0 Flask-Login>=0.3.2,<0.6.3 Flask-Principal>=0.3.2,<0.5.1 Flask>=1.0.2,<2.4.0 iso-639>=0.4.5,<0.5.0 - PyPDF>=3.0.0,<3.8.0 + PyPDF>=3.0.0,<3.16.0 pytz>=2016.10 - requests>=2.11.1,<2.29.0 + requests>=2.28.0,<2.32.0 SQLAlchemy>=1.3.0,<2.0.0 - tornado>=4.1,<6.3 + tornado>=6.3,<6.4 Wand>=0.4.4,<0.7.0 unidecode>=0.04.19,<1.4.0 lxml>=3.8.0,<5.0.0 flask-wtf>=0.14.2,<1.2.0 chardet>=3.0.0,<4.1.0 advocate>=1.0.0,<1.1.0 - Flask-Limiter>=2.3.0,<3.4.0 + Flask-Limiter>=2.3.0,<3.5.0 [options.extras_require] gdrive = - google-api-python-client>=1.7.11,<2.90.0 - gevent>20.6.0,<23.0.0 + google-api-python-client>=1.7.11,<2.98.0 + gevent>20.6.0,<24.0.0 greenlet>=0.4.17,<2.1.0 httplib2>=0.9.2,<0.23.0 oauth2client>=4.0.0,<4.1.4 uritemplate>=3.0.0,<4.2.0 pyasn1-modules>=0.0.8,<0.4.0 pyasn1>=0.1.9,<0.6.0 - PyDrive2>=1.3.1,<1.16.0 - PyYAML>=3.12 + PyDrive2>=1.3.1,<1.18.0 + PyYAML>=3.12,<6.1 rsa>=3.4.2,<4.10.0 gmail = - google-auth-oauthlib>=0.4.3,<0.9.0 - google-api-python-client>=1.7.11,<2.90.0 + google-auth-oauthlib>=0.4.3,<1.1.0 + google-api-python-client>=1.7.11,<2.98.0 goodreads = goodreads>=0.3.2,<0.4.0 - python-Levenshtein>=0.12.0,<0.21.0 + python-Levenshtein>=0.12.0,<0.22.0 ldap = python-ldap>=3.0.0,<3.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0 oauth = - Flask-Dance>=2.0.0,<6.3.0 - SQLAlchemy-Utils>=0.33.5,<0.40.0 + Flask-Dance>=2.0.0,<7.1.0 + SQLAlchemy-Utils>=0.33.5,<0.42.0 metadata = rarfile>=3.2 scholarly>=1.2.0,<1.8 markdown2>=2.0.0,<2.5.0 html2text>=2020.1.16,<2022.1.1 python-dateutil>=2.1,<2.9.0 - beautifulsoup4>=4.0.1,<4.12.0 - faust-cchardet>=2.1.18 + beautifulsoup4>=4.0.1,<4.13.0 + faust-cchardet>=2.1.18,<2.1.20 py7zr>=0.15.0,<0.21.0 comics = natsort>=2.2.0,<8.4.0 comicapi>=2.2.0,<3.3.0 kobo = - jsonschema>=3.2.0,<4.18.0 + jsonschema>=3.2.0,<4.20.0 diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index 66d4df88..31b7c621 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2023-08-23 21:16:31

+

Start Time: 2023-10-11 19:32:23

-

Stop Time: 2023-08-24 03:51:45

+

Stop Time: 2023-10-12 01:29:49

-

Duration: 5h 34 min

+

Duration: 4h 56 min

@@ -234,11 +234,11 @@ - + TestBackupMetadata 22 - 22 - 0 + 21 + 1 0 0 @@ -248,11 +248,31 @@ - +
TestBackupMetadata - test_backup_all
- PASS + +
+ FAIL +
+ + + + @@ -322,7 +342,7 @@ -
TestBackupMetadata - test_backup_change_book_seriesindex
+
TestBackupMetadata - test_backup_change_book_series_index
PASS @@ -1014,12 +1034,12 @@ - + TestEditAdditionalBooks 20 - 17 + 18 + 0 0 - 1 2 Detail @@ -1136,31 +1156,11 @@ - +
TestEditAdditionalBooks - test_upload_metadata_cb7
- -
- ERROR -
- - - - + PASS @@ -1246,12 +1246,12 @@ AttributeError: 'bool' object has no attribute 'click' - + TestEditBooks 38 - 34 + 36 + 0 0 - 2 2 Detail @@ -1537,31 +1537,11 @@ AttributeError: 'bool' object has no attribute 'click' - +
TestEditBooks - test_upload_book_cb7
- -
- ERROR -
- - - - + PASS @@ -1647,31 +1627,11 @@ AttributeError: 'bool' object has no attribute 'click' - +
TestEditBooks - test_upload_cover_hdd
- -
- ERROR -
- - - - + PASS @@ -1992,12 +1952,12 @@ NameError: name 'details' is not defined - + TestLoadMetadata 1 0 - 1 0 + 1 0 Detail @@ -2006,32 +1966,26 @@ NameError: name 'details' is not defined - +
TestLoadMetadata - test_load_metadata
- FAIL + ERROR
-