1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-29 11:30:31 +00:00

Merge branch 'Develop':

- Fix for new tornado version
- bookmark for comic viewer
- Bugfix for showing series containing only one book in list view containing having this book no series_index value set
- updated requirements
This commit is contained in:
Ozzie Isaacs 2023-10-14 15:27:46 +02:00
commit 2c339ed10c
15 changed files with 420 additions and 377 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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),

View File

@ -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)

View File

@ -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

View File

@ -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();
});
});

View File

@ -333,7 +333,6 @@ $(function() {
} else {
$("#parent").addClass('hidden')
}
// console.log(data);
data.files.forEach(function(entry) {
if(entry.type === "dir") {
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";

View File

@ -1,5 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
@ -20,23 +21,6 @@
<script src="{{ url_for('static', filename='js/libs/screenfull.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/compress/uncompress.js') }}"></script>
<script src="{{ url_for('static', filename='js/kthoom.js') }}"></script>
<script>
var updateArrows = function() {
if ($('input[name="direction"]:checked').val() === "0") {
$("#prev_page_key").html("&larr;");
$("#next_page_key").html("&rarr;");
} else {
$("#prev_page_key").html("&rarr;");
$("#next_page_key").html("&larr;");
}
};
document.onreadystatechange = function () {
if (document.readyState == "complete") {
init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
updateArrows();
}
}
</script>
</head>
<body>
<div id="sidebar">
@ -77,8 +61,8 @@
<div id="mainContent" tabindex="-1">
<div id="mainText" style="display:none"></div>
</div>
<div id="left" class="arrow" onclick="showLeftPage()"></div>
<div id="right" class="arrow" onclick="showRightPage()"></div>
<div id="left" class="arrow" style="display:none"></div>
<div id="right" class="arrow" style="display:none"></div>
</div>
<div class="modal md-effect-1" id="settings-modal">
@ -89,8 +73,8 @@
<table>
<thead>
<tr><th colspan="2">{{_('Keyboard Shortcuts')}}</th></tr>
</thead>
<tbody>
</thead>
<tbody>
<tr><td id="prev_page_key">&larr;</td> <td>{{_('Previous Page')}}</td></tr>
<tr><td id="next_page_key">&rarr;</td> <td>{{_('Next Page')}}</td></tr>
<tr><td>S</td> <td>{{_('Single Page Display')}}</td></tr>
@ -102,21 +86,21 @@
<tr><td>R</td> <td>{{_('Rotate Right')}}</td></tr>
<tr><td>L</td> <td>{{_('Rotate Left')}}</td></tr>
<tr><td>F</td> <td>{{_('Flip Image')}}</td></tr>
</tbody>
</table>
</div>
<div class="settings-column">
<table id="settings">
<thead>
<tr>
<th>{{_('Settings')}}</th>
</tr>
</thead>
<tbody>
<tr>
<th>{{_('Theme')}}:</th>
<td>
<div class="inputs">
</tbody>
</table>
</div>
<div class="settings-column">
<table id="settings">
<thead>
<tr>
<th>{{_('Settings')}}</th>
</tr>
</thead>
<tbody>
<tr>
<th>{{_('Theme')}}:</th>
<td>
<div class="inputs">
<label for="lightTheme"><input type="radio" id="lightTheme" name="theme" value="light" /> {{_('Light')}}</label>
<label for="darkTheme"><input type="radio" id="darkTheme" name="theme" value="dark" /> {{_('Dark')}}</label>
</div>
@ -139,59 +123,83 @@
<label for="fitWidth"><input type="radio" id="fitWidth" name="fitMode" value="87" /> {{_('Width')}}</label>
<label for="fitHeight"><input type="radio" id="fitHeight" name="fitMode" value="72" /> {{_('Height')}}</label>
<label for="fitNative"><input type="radio" id="fitNative" name="fitMode" value="78" /> {{_('Native')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Rotate')}}:</th>
<td>
<div class="inputs">
<label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0&deg;</label>
<label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90&deg;</label>
<label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180&deg;</label>
<label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270&deg;</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Flip')}}:</th>
<td>
<div class="inputs">
<label for="vflip"><input type="checkbox" id="vflip" name="vflip" /> {{_('Horizontal')}}</label>
<label for="hflip"><input type="checkbox" id="hflip" name="hflip" /> {{_('Vertical')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Direction')}}:</th>
<td>
<div class="inputs">
</div>
</td>
</tr>
<tr>
<th>{{_('Rotate')}}:</th>
<td>
<div class="inputs">
<label for="r0"><input type="radio" id="r0" name="rotateTimes" value="0" /> 0&deg;</label>
<label for="r90"><input type="radio" id="r90" name="rotateTimes" value="1" /> 90&deg;</label>
<label for="r180"><input type="radio" id="r180" name="rotateTimes" value="2" /> 180&deg;</label>
<label for="r270"><input type="radio" id="r270" name="rotateTimes" value="3" /> 270&deg;</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Flip')}}:</th>
<td>
<div class="inputs">
<label for="vflip"><input type="checkbox" id="vflip" name="vflip" /> {{_('Horizontal')}}</label>
<label for="hflip"><input type="checkbox" id="hflip" name="hflip" /> {{_('Vertical')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Direction')}}:</th>
<td>
<div class="inputs">
<label for="leftToRight"><input type="radio" id="leftToRight" name="direction" value="0" /> {{_('Left to Right')}}</label>
<label for="rightToLeft"><input type="radio" id="rightToLeft" name="direction" value="1" /> {{_('Right to Left')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Next Page')}}:</th>
<td>
<div class="inputs">
<label for="resetToTop"><input type="radio" id="resetToTop" name="nextPage" value="0" /> {{_('Reset to Top')}}</label>
<label for="rememberPosition"><input type="radio" id="rememberPosition" name="nextPage" value="1" /> {{_('Remember Position')}}</label>
</div>
</td>
</tr>
<tr>
<th>{{_('Scrollbar')}}:</th>
<td>
<div class="inputs">
<label for="showScrollbar"><input type="radio" id="showScrollbar" name="scrollbar" value="1" /> {{_('Show')}}</label>
<label for="hideScrollbar"><input type="radio" id="hideScrollbar" name="scrollbar" value="0" /> {{_('Hide')}}</label>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="closer icon-cancel-circled"></div>
</div>
<div class="closer icon-cancel-circled"></div>
</div>
</div>
<div class="overlay"></div>
<script>
$('input[name="direction"]').change(function() {
updateArrows();
});
</script>
<div class="overlay"></div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<script>
window.calibre = {
bookmarkUrl: "{{ url_for('web.set_bookmark', book_id=comicfile, book_format=extension.upper()) }}",
bookmark: "{{ bookmark.bookmark_key if bookmark != None }}",
useBookmarks: "{{ current_user.is_authenticated | tojson }}"
};
document.onreadystatechange = function () {
if (document.readyState == "complete") {
if (calibre.useBookmarks) {
currentImage = eval(calibre.bookmark);
if (typeof currentImage !== 'number') {
currentImage = 0;
}
}
init("{{ url_for('web.serve_book', book_id=comicfile, book_format=extension) }}");
}
}
</script>
</body>
</html>

View File

@ -16,12 +16,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -37,20 +37,20 @@
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;">
<p class='text-justify attribute'><strong>Start Time: </strong>2023-08-23 21:16:31</p>
<p class='text-justify attribute'><strong>Start Time: </strong>2023-10-11 19:32:23</p>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Stop Time: </strong>2023-08-24 03:51:45</p>
<p class='text-justify attribute'><strong>Stop Time: </strong>2023-10-12 01:29:49</p>
</div>
</div>
<div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3">
<p class='text-justify attribute'><strong>Duration: </strong>5h 34 min</p>
<p class='text-justify attribute'><strong>Duration: </strong>4h 56 min</p>
</div>
</div>
</div>
@ -234,11 +234,11 @@
<tr id="su" class="passClass">
<tr id="su" class="failClass">
<td>TestBackupMetadata</td>
<td class="text-center">22</td>
<td class="text-center">22</td>
<td class="text-center">0</td>
<td class="text-center">21</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">
@ -248,11 +248,31 @@
<tr id='pt2.1' class='hiddenRow bg-success'>
<tr id="ft2.1" class="none bg-danger">
<td>
<div class='testcase'>TestBackupMetadata - test_backup_all</div>
</td>
<td colspan='6' align='center'>PASS</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft2.1')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft2.1" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft2.1').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_backup_metadata.py&#34;, line 49, in test_backup_all
self.assertEqual(1, len(res))
AssertionError: 1 != 0</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -322,7 +342,7 @@
<tr id='pt2.9' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_seriesindex</div>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_series_index</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -1014,12 +1034,12 @@
<tr id="su" class="errorClass">
<tr id="su" class="skipClass">
<td>TestEditAdditionalBooks</td>
<td class="text-center">20</td>
<td class="text-center">17</td>
<td class="text-center">18</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">2</td>
<td class="text-center">
<a onclick="showClassDetail('c12', 20)">Detail</a>
@ -1136,31 +1156,11 @@
<tr id="et12.13" class="none bg-info">
<tr id='pt12.13' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cb7</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et12.13')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_et12.13" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_et12.13').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_additional_books.py&#34;, line 225, in test_upload_metadata_cb7
self.check_element_on_page((By.ID, &#39;edit_cancel&#39;)).click()
AttributeError: &#39;bool&#39; object has no attribute &#39;click&#39;</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -1246,12 +1246,12 @@ AttributeError: &#39;bool&#39; object has no attribute &#39;click&#39;</pre>
<tr id="su" class="errorClass">
<tr id="su" class="skipClass">
<td>TestEditBooks</td>
<td class="text-center">38</td>
<td class="text-center">34</td>
<td class="text-center">36</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">2</td>
<td class="text-center">2</td>
<td class="text-center">
<a onclick="showClassDetail('c13', 38)">Detail</a>
@ -1537,31 +1537,11 @@ AttributeError: &#39;bool&#39; object has no attribute &#39;click&#39;</pre>
<tr id="et13.28" class="none bg-info">
<tr id='pt13.28' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestEditBooks - test_upload_book_cb7</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et13.28')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_et13.28" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_et13.28').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books.py&#34;, line 1159, in test_upload_book_cb7
self.check_element_on_page((By.ID, &#39;edit_cancel&#39;)).click()
AttributeError: &#39;bool&#39; object has no attribute &#39;click&#39;</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -1647,31 +1627,11 @@ AttributeError: &#39;bool&#39; object has no attribute &#39;click&#39;</pre>
<tr id="et13.38" class="none bg-info">
<tr id='pt13.38' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestEditBooks - test_upload_cover_hdd</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et13.38')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_et13.38" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_et13.38').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books.py&#34;, line 866, in test_upload_cover_hdd
self.delete_book(details[&#39;id&#39;])
NameError: name &#39;details&#39; is not defined</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -1992,12 +1952,12 @@ NameError: name &#39;details&#39; is not defined</pre>
<tr id="su" class="failClass">
<tr id="su" class="errorClass">
<td>TestLoadMetadata</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">
<a onclick="showClassDetail('c17', 1)">Detail</a>
@ -2006,32 +1966,26 @@ NameError: name &#39;details&#39; is not defined</pre>
<tr id="ft17.1" class="none bg-danger">
<tr id="et17.1" class="none bg-info">
<td>
<div class='testcase'>TestLoadMetadata - test_load_metadata</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft17.1')">FAIL</a>
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_et17.1')">ERROR</a>
</div>
<!--css div popup start-->
<div id="div_ft17.1" class="popup_window test_output" style="display:block;">
<div id="div_et17.1" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft17.1').style.display='none'"><span
onclick="document.getElementById('div_et17.1').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py&#34;, line 209, in test_load_metadata
self.assertEqual(old_results, results)
AssertionError: Lists differ: [] != [{&#39;cover_element&#39;: &lt;selenium.webdriver.rem[10121 chars]4/&#39;}]
Second list contains 20 additional elements.
First extra element 0:
{&#39;cover_element&#39;: &lt;selenium.webdriver.remote.webelement.WebElement (session=&#34;34034d2d-f804-47c1-b9ad-fcf09f75f812&#34;, element=&#34;6dfe81e2-4752-4f1f-bd33-9388d0d529c1&#34;)&gt;, &#39;cover&#39;: &#39;https://books.google.com/books/content?id=Ub8TAQAAIAAJ&amp;printsec=frontcover&amp;img=1&amp;zoom=1&amp;source=gbs_api&amp;fife=w800-h900&#39;, &#39;source&#39;: &#39;https://books.google.com/&#39;, &#39;author&#39;: &#39;Martin Vogt&#39;, &#39;publisher&#39;: &#39;&#39;, &#39;title&#39;: &#39;Der Buchtitel in der römischen Poesie&#39;, &#39;title_link&#39;: &#39;https://books.google.com/books?id=Ub8TAQAAIAAJ&#39;}
Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py&#34;, line 84, in test_load_metadata
elif &#39;https://amazon.com/&#39; == results[20][&#39;source&#39;]:
IndexError: list index out of range</pre>
</div>
<div class="clearfix"></div>
</div>
@ -3374,13 +3328,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id="su" class="passClass">
<td>TestOPDSFeed</td>
<td class="text-center">23</td>
<td class="text-center">23</td>
<td class="text-center">24</td>
<td class="text-center">24</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">
<a onclick="showClassDetail('c36', 23)">Detail</a>
<a onclick="showClassDetail('c36', 24)">Detail</a>
</td>
</tr>
@ -3559,7 +3513,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='pt36.20' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_tags</div>
<div class='testcase'>TestOPDSFeed - test_opds_stats</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3568,7 +3522,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='pt36.21' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
<div class='testcase'>TestOPDSFeed - test_opds_tags</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3577,7 +3531,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='pt36.22' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div>
<div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3585,6 +3539,15 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='pt36.23' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr id='pt36.24' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_recently_added</div>
</td>
@ -4082,11 +4045,11 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id="su" class="skipClass">
<tr id="su" class="failClass">
<td>TestThumbnails</td>
<td class="text-center">8</td>
<td class="text-center">7</td>
<td class="text-center">0</td>
<td class="text-center">6</td>
<td class="text-center">1</td>
<td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">
@ -4159,11 +4122,31 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='pt45.8' class='hiddenRow bg-success'>
<tr id="ft45.8" class="none bg-danger">
<td>
<div class='testcase'>TestThumbnails - test_sideloaded_book</div>
</td>
<td colspan='6' align='center'>PASS</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft45.8')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft45.8" class="popup_window test_output" style="display:block;">
<div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();"
onclick="document.getElementById('div_ft45.8').style.display='none'"><span
aria-hidden="true">&times;</span></button>
</div>
<div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last):
File &#34;/home/ozzie/Development/calibre-web-test/test/test_thumbnails.py&#34;, line 311, in test_sideloaded_book
self.assertAlmostEqual(diff(BytesIO(list_cover), BytesIO(old_list_cover), delete_diff_file=True), 0.0,
AssertionError: 0.004399004046062869 != 0.0 within 0.0001 delta (0.004399004046062869 difference)</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -5237,10 +5220,10 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr id='total_row' class="text-center bg-grey">
<td>Total</td>
<td>461</td>
<td>448</td>
<td>462</td>
<td>450</td>
<td>2</td>
<td>1</td>
<td>3</td>
<td>9</td>
<td>&nbsp;</td>
</tr>
@ -5269,7 +5252,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>Platform</th>
<td>Linux 6.2.0-26-generic #26~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Jul 13 16:27:29 UTC 2 x86_64 x86_64</td>
<td>Linux 6.2.0-34-generic #34~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Sep 7 13:12:03 UTC 2 x86_64 x86_64</td>
<td>Basic</td>
</tr>
@ -5293,7 +5276,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>Babel</th>
<td>2.12.1</td>
<td>2.13.0</td>
<td>Basic</td>
</tr>
@ -5311,13 +5294,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>flask-babel</th>
<td>3.0.1</td>
<td>3.1.0</td>
<td>Basic</td>
</tr>
<tr>
<th>Flask-Limiter</th>
<td>3.3.1</td>
<td>3.4.1</td>
<td>Basic</td>
</tr>
@ -5335,13 +5318,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>Flask-WTF</th>
<td>1.1.1</td>
<td>1.1.2</td>
<td>Basic</td>
</tr>
<tr>
<th>greenlet</th>
<td>2.0.2</td>
<td>3.0.0</td>
<td>Basic</td>
</tr>
@ -5371,19 +5354,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>pypdf</th>
<td>3.7.1</td>
<td>3.15.5</td>
<td>Basic</td>
</tr>
<tr>
<th>pytz</th>
<td>2022.7.1</td>
<td>2023.3.post1</td>
<td>Basic</td>
</tr>
<tr>
<th>requests</th>
<td>2.28.2</td>
<td>2.31.0</td>
<td>Basic</td>
</tr>
@ -5395,13 +5378,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>tornado</th>
<td>6.2</td>
<td>6.3.3</td>
<td>Basic</td>
</tr>
<tr>
<th>Unidecode</th>
<td>1.3.6</td>
<td>1.3.7</td>
<td>Basic</td>
</tr>
@ -5419,7 +5402,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestBackupMetadataGdrive</td>
</tr>
@ -5449,7 +5432,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestCliGdrivedb</td>
</tr>
@ -5479,7 +5462,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestEbookConvertCalibreGDrive</td>
</tr>
@ -5509,7 +5492,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestEbookConvertGDriveKepubify</td>
</tr>
@ -5551,7 +5534,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>rarfile</th>
<td>4.0</td>
<td>4.1</td>
<td>TestEditAdditionalBooks</td>
</tr>
@ -5563,7 +5546,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestEditAuthorsGdrive</td>
</tr>
@ -5599,7 +5582,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestEditBooksOnGdrive</td>
</tr>
@ -5641,7 +5624,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>google-api-python-client</th>
<td>2.97.0</td>
<td>2.103.0</td>
<td>TestSetupGdrive</td>
</tr>
@ -5677,19 +5660,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>python-Levenshtein</th>
<td>0.21.1</td>
<td>0.23.0</td>
<td>TestGoodreads</td>
</tr>
<tr>
<th>jsonschema</th>
<td>4.19.0</td>
<td>4.19.1</td>
<td>TestKoboSync</td>
</tr>
<tr>
<th>jsonschema</th>
<td>4.19.0</td>
<td>4.19.1</td>
<td>TestKoboSyncBig</td>
</tr>
@ -5701,7 +5684,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr>
<th>jsonschema</th>
<td>4.19.0</td>
<td>4.19.1</td>
<td>TestLdapLogin</td>
</tr>
@ -5731,7 +5714,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
</div>
<script>
drawCircle(448, 1, 3, 9);
drawCircle(450, 2, 1, 9);
showCase(5);
</script>