1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-12-01 05:49:58 +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 urllib.parse import urlparse
from flask import Blueprint, flash, redirect, url_for, abort, request, make_response, send_from_directory, g, Response 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_login import login_required, current_user, logout_user
from flask_babel import gettext as _ from flask_babel import gettext as _
from flask_babel import get_locale, format_time, format_datetime, format_timedelta 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, cls.session_factory = scoped_session(sessionmaker(autocommit=False,
autoflush=True, autoflush=True,
bind=cls.engine)) bind=cls.engine, future=True))
for inst in cls.instances: for inst in cls.instances:
inst.init_session() inst.init_session()

View File

@ -25,16 +25,15 @@ from datetime import datetime
import json import json
from shutil import copyfile from shutil import copyfile
from uuid import uuid4 from uuid import uuid4
from markupsafe import escape # dependency of flask from markupsafe import escape, Markup # dependency of flask
from functools import wraps from functools import wraps
import re
try: try:
from lxml.html.clean import clean_html, Cleaner from lxml.html.clean import clean_html, Cleaner
except ImportError: except ImportError:
clean_html = None 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 gettext as _
from flask_babel import lazy_gettext as N_ from flask_babel import lazy_gettext as N_
from flask_babel import get_locale from flask_babel import get_locale

View File

@ -166,12 +166,6 @@ def HandleSyncRequest():
only_kobo_shelves = current_user.kobo_only_shelves_sync only_kobo_shelves = current_user.kobo_only_shelves_sync
if only_kobo_shelves: 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, changed_entries = calibre_db.session.query(db.Books,
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.BookShelf.date_added, ub.BookShelf.date_added,
@ -192,9 +186,6 @@ def HandleSyncRequest():
.filter(ub.Shelf.kobo_sync) .filter(ub.Shelf.kobo_sync)
.distinct()) .distinct())
else: 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, changed_entries = calibre_db.session.query(db.Books,
ub.ArchivedBook.last_modified, ub.ArchivedBook.last_modified,
ub.ArchivedBook.is_archived) ub.ArchivedBook.is_archived)
@ -209,9 +200,6 @@ def HandleSyncRequest():
.order_by(db.Books.id)) .order_by(db.Books.id))
reading_states_in_new_entitlements = [] 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) books = changed_entries.limit(SYNC_ITEM_LIMIT)
log.debug("Books to Sync: {}".format(len(books.all()))) log.debug("Books to Sync: {}".format(len(books.all())))
for book in books: for book in books:
@ -255,13 +243,6 @@ def HandleSyncRequest():
new_books_last_created = max(ts_created, new_books_last_created) new_books_last_created = max(ts_created, new_books_last_created)
kobo_sync_status.add_synced_books(book.Books.id) 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)\ max_change = changed_entries.filter(ub.ArchivedBook.is_archived)\
.filter(ub.ArchivedBook.user_id == current_user.id) \ .filter(ub.ArchivedBook.user_id == current_user.id) \
.order_by(func.datetime(ub.ArchivedBook.last_modified).desc()).first() .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) new_archived_last_modified = max(new_archived_last_modified, max_change)
# no. of books returned # 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() book_count = changed_entries.count()
# last entry: # last entry:
cont_sync = bool(book_count) cont_sync = bool(book_count)
@ -523,7 +500,7 @@ def get_metadata(book):
@requires_kobo_auth @requires_kobo_auth
# Creates a Shelf with the given items, and returns the shelf's uuid. # Creates a Shelf with the given items, and returns the shelf's uuid.
def HandleTagCreate(): 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": if request.method == "DELETE":
abort(405) abort(405)
name, items = None, None 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) 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( shelflist = ub.session.query(ub.Shelf).outerjoin(ub.BookShelf).filter(
or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, or_(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified,
func.datetime(ub.BookShelf.date_added) > 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: if _GEVENT:
self.wsgiserver.close() self.wsgiserver.close()
else: 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 import sys
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from jsonschema import validate, exceptions, __version__ from jsonschema import validate, exceptions
from datetime import datetime, timezone from datetime import datetime
from urllib.parse import unquote
from flask import json from flask import json
from .. import logger from .. import logger

View File

@ -71,7 +71,8 @@ var settings = {
fitMode: kthoom.Key.B, fitMode: kthoom.Key.B,
theme: "light", theme: "light",
direction: 0, // 0 = Left to Right, 1 = Right to Left 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 pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
}; };
@ -131,8 +132,8 @@ var createURLFromArray = function(array, mimeType) {
} }
if ((typeof URL !== "function" && typeof URL !== "object") || if ((typeof URL !== "function" && typeof URL !== "object") ||
typeof URL.createObjectURL !== "function") { typeof URL.createObjectURL !== "function") {
throw "Browser support for Object URLs is missing"; throw "Browser support for Object URLs is missing";
} }
return URL.createObjectURL(blob); 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() { function initProgressClick() {
$("#progress").click(function(e) { $("#progress").click(function(e) {
var offset = $(this).offset(); var offset = $(this).offset();
var x = e.pageX - offset.left; var x = e.pageX - offset.left;
var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width(); var rate = settings.direction === 0 ? x / $(this).width() : 1 - x / $(this).width();
currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1; currentImage = Math.max(1, Math.ceil(rate * totalImages)) - 1;
updateDirectionButtons();
setBookmark();
updatePage(); updatePage();
}); });
} }
@ -222,6 +247,7 @@ function loadFromArrayBuffer(ab) {
// display first page if we haven't yet // display first page if we haven't yet
if (imageFiles.length === currentImage + 1) { if (imageFiles.length === currentImage + 1) {
updateDirectionButtons();
updatePage(); updatePage();
} }
} else { } else {
@ -241,7 +267,7 @@ function scrollTocToActive() {
// Mark the current page in the TOC // Mark the current page in the TOC
$("#tocView a[data-page]") $("#tocView a[data-page]")
// Remove the currently active thumbnail // Remove the currently active thumbnail
.removeClass("active") .removeClass("active")
// Find the new one // Find the new one
.filter("[data-page=" + (currentImage + 1) + "]") .filter("[data-page=" + (currentImage + 1) + "]")
@ -409,6 +435,7 @@ function showLeftPage() {
} else { } else {
showNextPage(); showNextPage();
} }
setBookmark();
} }
function showRightPage() { function showRightPage() {
@ -417,6 +444,7 @@ function showRightPage() {
} else { } else {
showPrevPage(); showPrevPage();
} }
setBookmark();
} }
function showPrevPage() { function showPrevPage() {
@ -427,6 +455,7 @@ function showPrevPage() {
} else { } else {
updatePage(); updatePage();
} }
updateDirectionButtons();
} }
function showNextPage() { function showNextPage() {
@ -437,6 +466,7 @@ function showNextPage() {
} else { } else {
updatePage(); updatePage();
} }
updateDirectionButtons();
} }
function scrollCurrentImageIntoView() { function scrollCurrentImageIntoView() {
@ -621,11 +651,21 @@ function drawCanvas() {
$("#mainContent").append(canvasElement); $("#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) { function init(filename) {
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open("GET", filename); request.open("GET", filename);
request.responseType = "arraybuffer"; request.responseType = "arraybuffer";
request.addEventListener("load", function() { request.addEventListener("load", function () {
if (request.status >= 200 && request.status < 300) { if (request.status >= 200 && request.status < 300) {
loadFromArrayBuffer(request.response); loadFromArrayBuffer(request.response);
} else { } else {
@ -641,18 +681,18 @@ function init(filename) {
$(document).keydown(keyHandler); $(document).keydown(keyHandler);
$(window).resize(function() { $(window).resize(function () {
updateScale(); updateScale();
}); });
// Open TOC menu // Open TOC menu
$("#slider").click(function() { $("#slider").click(function () {
$("#sidebar").toggleClass("open"); $("#sidebar").toggleClass("open");
$("#main").toggleClass("closed"); $("#main").toggleClass("closed");
$(this).toggleClass("icon-menu icon-right"); $(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 ¯\_(ツ)_/¯ // 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 // Focus on the TOC or the main content area, depending on which is open
$("#main:not(.closed) #mainContent, #sidebar.open #tocView").focus(); $("#main:not(.closed) #mainContent, #sidebar.open #tocView").focus();
scrollTocToActive(); scrollTocToActive();
@ -660,12 +700,12 @@ function init(filename) {
}); });
// Open Settings modal // Open Settings modal
$("#setting").click(function() { $("#setting").click(function () {
$("#settings-modal").toggleClass("md-show"); $("#settings-modal").toggleClass("md-show");
}); });
// On Settings input change // On Settings input change
$("#settings input").on("change", function() { $("#settings input").on("change", function () {
// Get either the checked boolean or the assigned value // Get either the checked boolean or the assigned value
var value = this.type === "checkbox" ? this.checked : this.value; var value = this.type === "checkbox" ? this.checked : this.value;
@ -674,39 +714,40 @@ function init(filename) {
settings[this.name] = value; settings[this.name] = value;
if(["hflip", "vflip", "rotateTimes"].includes(this.name)) { if (["hflip", "vflip", "rotateTimes"].includes(this.name)) {
reloadImages(); reloadImages();
} else if(this.name === "direction") { } else if (this.name === "direction") {
updateDirectionButtons();
return updateProgress(); return updateProgress();
} }
updatePage(); updatePage();
updateScale(); updateScale();
}); });
// Close modal // Close modal
$(".closer, .overlay").click(function() { $(".closer, .overlay").click(function () {
$(".md-show").removeClass("md-show"); $(".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 // TOC thumbnail pagination
$("#thumbnails").on("click", "a", function() { $("#thumbnails").on("click", "a", function () {
currentImage = $(this).data("page") - 1; currentImage = $(this).data("page") - 1;
updatePage(); updatePage();
}); });
// Fullscreen mode // Fullscreen mode
if (typeof screenfull !== "undefined") { if (typeof screenfull !== "undefined") {
$("#fullscreen").click(function() { $("#fullscreen").click(function () {
screenfull.toggle($("#container")[0]); screenfull.toggle($("#container")[0]);
// Focus on main container so you can use up/down keys immediately after fullscreen // Focus on main container so you can use up/down keys immediately after fullscreen
$("#mainContent").focus(); $("#mainContent").focus();
}); });
if (screenfull.raw) { if (screenfull.raw) {
var $button = $("#fullscreen"); var $button = $("#fullscreen");
document.addEventListener(screenfull.raw.fullscreenchange, function() { document.addEventListener(screenfull.raw.fullscreenchange, function () {
screenfull.isFullscreen screenfull.isFullscreen
? $button.addClass("icon-resize-small").removeClass("icon-resize-full") ? $button.addClass("icon-resize-small").removeClass("icon-resize-full")
: $button.addClass("icon-resize-full").removeClass("icon-resize-small"); : $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 // Focus the scrollable area so that keyboard scrolling work as expected
$("#mainContent").focus(); $("#mainContent").focus();
$("#mainContent").swipe( { $("#mainContent").swipe({
swipeRight:function() { swipeRight: function () {
showLeftPage(); showLeftPage();
}, },
swipeLeft:function() { swipeLeft: function () {
showRightPage(); showRightPage();
}, },
}); });
$(".mainImage").click(function(evt) { $(".mainImage").click(function (evt) {
// Firefox does not support offsetX/Y so we have to manually calculate // Firefox does not support offsetX/Y, so we have to manually calculate
// where the user clicked in the image. // where the user clicked in the image.
var mainContentWidth = $("#mainContent").width(); var mainContentWidth = $("#mainContent").width();
var mainContentHeight = $("#mainContent").height(); 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) // 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(); 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 // Don't trigger the scroll for Single Page
} else if(scroll > prevScrollPosition) { } else if (scroll > prevScrollPosition) {
//Scroll Down //Scroll Down
if(currentImage + 1 < imageFiles.length) { if (currentImage + 1 < imageFiles.length) {
if(currentImageOffset(currentImage + 1) <= 1) { if (currentImageOffset(currentImage + 1) <= 1) {
currentImage++; currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
if ( currentImage >= imageFiles.length) {
currentImage = imageFiles.length - 1;
}
console.log(currentImage);
scrollTocToActive(); scrollTocToActive();
updateProgress(); updateProgress();
} }
} }
} else { } else {
//Scroll Up //Scroll Up
if(currentImage - 1 > -1 ) { if (currentImage - 1 > -1) {
if(currentImageOffset(currentImage - 1) >= 0) { if (currentImageOffset(currentImage - 1) >= 0) {
currentImage--; currentImage = Math.floor((imageFiles.length) / (viewLength-viewLength/(imageFiles.length)) * scroll, 0);
console.log(currentImage);
scrollTocToActive(); scrollTocToActive();
updateProgress(); updateProgress();
} }
} }
} }
// Update scroll position // Update scroll position
prevScrollPosition = scroll; prevScrollPosition = scroll;
}); });
@ -794,3 +843,31 @@ function init(filename) {
function currentImageOffset(imageIndex) { function currentImageOffset(imageIndex) {
return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top 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 { } else {
$("#parent").addClass('hidden') $("#parent").addClass('hidden')
} }
// console.log(data);
data.files.forEach(function(entry) { data.files.forEach(function(entry) {
if(entry.type === "dir") { if(entry.type === "dir") {
var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>"; var type = "<span class=\"glyphicon glyphicon-folder-close\"></span>";

View File

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

View File

@ -16,12 +16,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from tornado.wsgi import WSGIContainer from tornado.wsgi import WSGIContainer
import tornado import tornado
from tornado import escape from tornado import escape
from tornado import httputil from tornado import httputil
from tornado.ioloop import IOLoop
from typing import List, Tuple, Optional, Callable, Any, Dict, Text from typing import List, Tuple, Optional, Callable, Any, Dict, Text
from types import TracebackType from types import TracebackType
@ -34,61 +34,67 @@ if typing.TYPE_CHECKING:
class MyWSGIContainer(WSGIContainer): class MyWSGIContainer(WSGIContainer):
def __call__(self, request: httputil.HTTPServerRequest) -> None: def __call__(self, request: httputil.HTTPServerRequest) -> None:
data = {} # type: Dict[str, Any] if tornado.version_info < (6, 3, 0, -99):
response = [] # type: List[bytes] data = {} # type: Dict[str, Any]
response = [] # type: List[bytes]
def start_response( def start_response(
status: str, status: str,
headers: List[Tuple[str, str]], headers: List[Tuple[str, str]],
exc_info: Optional[ exc_info: Optional[
Tuple[ Tuple[
"Optional[Type[BaseException]]", "Optional[Type[BaseException]]",
Optional[BaseException], Optional[BaseException],
Optional[TracebackType], Optional[TracebackType],
] ]
] = None, ] = None,
) -> Callable[[bytes], Any]: ) -> Callable[[bytes], Any]:
data["status"] = status data["status"] = status
data["headers"] = headers data["headers"] = headers
return response.append return response.append
app_response = self.wsgi_application( app_response = self.wsgi_application(
MyWSGIContainer.environ(request), start_response 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: try:
response.extend(app_response) environ = WSGIContainer.environ(self, request)
body = b"".join(response) except TypeError as e:
finally: environ = WSGIContainer.environ(request)
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['RAW_URI'] = request.path environ['RAW_URI'] = request.path
return environ return environ

View File

@ -1014,7 +1014,7 @@ def series_list():
func.max(db.Books.series_index), db.Books.id) func.max(db.Books.series_index), db.Books.id)
.join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters()) .join(db.books_series_link).join(db.Series).filter(calibre_db.common_filters())
.group_by(text('books_series_link.series')) .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) .order_by(order)
.all()) .all())
return render_title_template('grid.html', entries=entries, folder='web.books_list', charlist=char_list, 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('.') title = title + " #" + '{0:.2f}'.format(book.series_index).rstrip('0').rstrip('.')
log.debug("Start comic reader for %d", book_id) log.debug("Start comic reader for %d", book_id)
return render_title_template('readcbr.html', comicfile=all_name, title=title, 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") 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"), flash(_("Oops! Selected book is unavailable. File does not exist or is not accessible"),
category="error") category="error")

View File

@ -1,31 +1,31 @@
# GDrive Integration # GDrive Integration
google-api-python-client>=1.7.11,<2.90.0 google-api-python-client>=1.7.11,<2.98.0
gevent>20.6.0,<23.0.0 gevent>20.6.0,<24.0.0
greenlet>=0.4.17,<2.1.0 greenlet>=0.4.17,<2.1.0
httplib2>=0.9.2,<0.23.0 httplib2>=0.9.2,<0.23.0
oauth2client>=4.0.0,<4.1.4 oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0 uritemplate>=3.0.0,<4.2.0
pyasn1-modules>=0.0.8,<0.4.0 pyasn1-modules>=0.0.8,<0.4.0
pyasn1>=0.1.9,<0.6.0 pyasn1>=0.1.9,<0.6.0
PyDrive2>=1.3.1,<1.16.0 PyDrive2>=1.3.1,<1.18.0
PyYAML>=3.12 PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0 rsa>=3.4.2,<4.10.0
# Gmail # Gmail
google-auth-oauthlib>=0.4.3,<0.9.0 google-auth-oauthlib>=0.4.3,<1.1.0
google-api-python-client>=1.7.11,<2.90.0 google-api-python-client>=1.7.11,<2.98.0
# goodreads # goodreads
goodreads>=0.3.2,<0.4.0 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 # ldap login
python-ldap>=3.0.0,<3.5.0 python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0
# oauth # oauth
Flask-Dance>=2.0.0,<6.3.0 Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.40.0 SQLAlchemy-Utils>=0.33.5,<0.42.0
# metadata extraction # metadata extraction
rarfile>=3.2 rarfile>=3.2
@ -33,8 +33,8 @@ scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0 markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1 html2text>=2020.1.16,<2022.1.1
python-dateutil>=2.1,<2.9.0 python-dateutil>=2.1,<2.9.0
beautifulsoup4>=4.0.1,<4.12.0 beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
# Comics # Comics
@ -42,4 +42,4 @@ natsort>=2.2.0,<8.4.0
comicapi>=2.2.0,<3.3.0 comicapi>=2.2.0,<3.3.0
# Kobo integration # 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 Werkzeug<3.0.0
APScheduler>=3.6.3,<3.11.0 APScheduler>=3.6.3,<3.11.0
Babel>=1.3,<3.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-Login>=0.3.2,<0.6.3
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
Flask>=1.0.2,<2.4.0 Flask>=1.0.2,<2.4.0
iso-639>=0.4.5,<0.5.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 pytz>=2016.10
requests>=2.11.1,<2.29.0 requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.0.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 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<5.0.0 lxml>=3.8.0,<5.0.0
flask-wtf>=0.14.2,<1.2.0 flask-wtf>=0.14.2,<1.2.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.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] [options]
include_package_data = True include_package_data = True
install_requires = install_requires =
Werkzeug<3.0.0
APScheduler>=3.6.3,<3.11.0 APScheduler>=3.6.3,<3.11.0
Babel>=1.3,<3.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-Login>=0.3.2,<0.6.3
Flask-Principal>=0.3.2,<0.5.1 Flask-Principal>=0.3.2,<0.5.1
Flask>=1.0.2,<2.4.0 Flask>=1.0.2,<2.4.0
iso-639>=0.4.5,<0.5.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 pytz>=2016.10
requests>=2.11.1,<2.29.0 requests>=2.28.0,<2.32.0
SQLAlchemy>=1.3.0,<2.0.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 Wand>=0.4.4,<0.7.0
unidecode>=0.04.19,<1.4.0 unidecode>=0.04.19,<1.4.0
lxml>=3.8.0,<5.0.0 lxml>=3.8.0,<5.0.0
flask-wtf>=0.14.2,<1.2.0 flask-wtf>=0.14.2,<1.2.0
chardet>=3.0.0,<4.1.0 chardet>=3.0.0,<4.1.0
advocate>=1.0.0,<1.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] [options.extras_require]
gdrive = gdrive =
google-api-python-client>=1.7.11,<2.90.0 google-api-python-client>=1.7.11,<2.98.0
gevent>20.6.0,<23.0.0 gevent>20.6.0,<24.0.0
greenlet>=0.4.17,<2.1.0 greenlet>=0.4.17,<2.1.0
httplib2>=0.9.2,<0.23.0 httplib2>=0.9.2,<0.23.0
oauth2client>=4.0.0,<4.1.4 oauth2client>=4.0.0,<4.1.4
uritemplate>=3.0.0,<4.2.0 uritemplate>=3.0.0,<4.2.0
pyasn1-modules>=0.0.8,<0.4.0 pyasn1-modules>=0.0.8,<0.4.0
pyasn1>=0.1.9,<0.6.0 pyasn1>=0.1.9,<0.6.0
PyDrive2>=1.3.1,<1.16.0 PyDrive2>=1.3.1,<1.18.0
PyYAML>=3.12 PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0 rsa>=3.4.2,<4.10.0
gmail = gmail =
google-auth-oauthlib>=0.4.3,<0.9.0 google-auth-oauthlib>=0.4.3,<1.1.0
google-api-python-client>=1.7.11,<2.90.0 google-api-python-client>=1.7.11,<2.98.0
goodreads = goodreads =
goodreads>=0.3.2,<0.4.0 goodreads>=0.3.2,<0.4.0
python-Levenshtein>=0.12.0,<0.21.0 python-Levenshtein>=0.12.0,<0.22.0
ldap = ldap =
python-ldap>=3.0.0,<3.5.0 python-ldap>=3.0.0,<3.5.0
Flask-SimpleLDAP>=1.4.0,<1.5.0 Flask-SimpleLDAP>=1.4.0,<1.5.0
oauth = oauth =
Flask-Dance>=2.0.0,<6.3.0 Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.40.0 SQLAlchemy-Utils>=0.33.5,<0.42.0
metadata = metadata =
rarfile>=3.2 rarfile>=3.2
scholarly>=1.2.0,<1.8 scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0 markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2022.1.1 html2text>=2020.1.16,<2022.1.1
python-dateutil>=2.1,<2.9.0 python-dateutil>=2.1,<2.9.0
beautifulsoup4>=4.0.1,<4.12.0 beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18 faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0 py7zr>=0.15.0,<0.21.0
comics = comics =
natsort>=2.2.0,<8.4.0 natsort>=2.2.0,<8.4.0
comicapi>=2.2.0,<3.3.0 comicapi>=2.2.0,<3.3.0
kobo = 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="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3" style="margin-top:50px;"> <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> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <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> </div>
<div class="row"> <div class="row">
<div class="col-xs-6 col-md-6 col-sm-offset-3"> <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> </div>
</div> </div>
@ -234,11 +234,11 @@
<tr id="su" class="passClass"> <tr id="su" class="failClass">
<td>TestBackupMetadata</td> <td>TestBackupMetadata</td>
<td class="text-center">22</td> <td class="text-center">22</td>
<td class="text-center">22</td> <td class="text-center">21</td>
<td class="text-center">0</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <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> <td>
<div class='testcase'>TestBackupMetadata - test_backup_all</div> <div class='testcase'>TestBackupMetadata - test_backup_all</div>
</td> </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> </tr>
@ -322,7 +342,7 @@
<tr id='pt2.9' class='hiddenRow bg-success'> <tr id='pt2.9' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_seriesindex</div> <div class='testcase'>TestBackupMetadata - test_backup_change_book_series_index</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </tr>
@ -1014,12 +1034,12 @@
<tr id="su" class="errorClass"> <tr id="su" class="skipClass">
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
<td class="text-center">20</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">0</td>
<td class="text-center">1</td>
<td class="text-center">2</td> <td class="text-center">2</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c12', 20)">Detail</a> <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> <td>
<div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cb7</div> <div class='testcase'>TestEditAdditionalBooks - test_upload_metadata_cb7</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<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>
</tr> </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>TestEditBooks</td>
<td class="text-center">38</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">0</td>
<td class="text-center">2</td>
<td class="text-center">2</td> <td class="text-center">2</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c13', 38)">Detail</a> <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> <td>
<div class='testcase'>TestEditBooks - test_upload_book_cb7</div> <div class='testcase'>TestEditBooks - test_upload_book_cb7</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<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>
</tr> </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> <td>
<div class='testcase'>TestEditBooks - test_upload_cover_hdd</div> <div class='testcase'>TestEditBooks - test_upload_cover_hdd</div>
</td> </td>
<td colspan='6'> <td colspan='6' align='center'>PASS</td>
<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>
</tr> </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>TestLoadMetadata</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c17', 1)">Detail</a> <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> <td>
<div class='testcase'>TestLoadMetadata - test_load_metadata</div> <div class='testcase'>TestLoadMetadata - test_load_metadata</div>
</td> </td>
<td colspan='6'> <td colspan='6'>
<div class="text-center"> <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> </div>
<!--css div popup start--> <!--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'> <div class='close_button pull-right'>
<button type="button" class="close" aria-label="Close" onfocus="this.blur();" <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> aria-hidden="true">&times;</span></button>
</div> </div>
<div class="text-left pull-left"> <div class="text-left pull-left">
<pre class="text-left">Traceback (most recent call last): <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 File &#34;/home/ozzie/Development/calibre-web-test/test/test_edit_books_metadata.py&#34;, line 84, in test_load_metadata
self.assertEqual(old_results, results) elif &#39;https://amazon.com/&#39; == results[20][&#39;source&#39;]:
AssertionError: Lists differ: [] != [{&#39;cover_element&#39;: &lt;selenium.webdriver.rem[10121 chars]4/&#39;}] IndexError: list index out of range</pre>
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>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</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"> <tr id="su" class="passClass">
<td>TestOPDSFeed</td> <td>TestOPDSFeed</td>
<td class="text-center">23</td> <td class="text-center">24</td>
<td class="text-center">23</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">0</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center"> <td class="text-center">
<a onclick="showClassDetail('c36', 23)">Detail</a> <a onclick="showClassDetail('c36', 24)">Detail</a>
</td> </td>
</tr> </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'> <tr id='pt36.20' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestOPDSFeed - test_opds_tags</div> <div class='testcase'>TestOPDSFeed - test_opds_stats</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </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'> <tr id='pt36.21' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestOPDSFeed - test_opds_top_rated</div> <div class='testcase'>TestOPDSFeed - test_opds_tags</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </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'> <tr id='pt36.22' class='hiddenRow bg-success'>
<td> <td>
<div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div> <div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
</td> </td>
<td colspan='6' align='center'>PASS</td> <td colspan='6' align='center'>PASS</td>
</tr> </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'> <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> <td>
<div class='testcase'>TestOPDSFeed - test_recently_added</div> <div class='testcase'>TestOPDSFeed - test_recently_added</div>
</td> </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>TestThumbnails</td>
<td class="text-center">8</td> <td class="text-center">8</td>
<td class="text-center">7</td> <td class="text-center">6</td>
<td class="text-center">0</td> <td class="text-center">1</td>
<td class="text-center">0</td> <td class="text-center">0</td>
<td class="text-center">1</td> <td class="text-center">1</td>
<td class="text-center"> <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> <td>
<div class='testcase'>TestThumbnails - test_sideloaded_book</div> <div class='testcase'>TestThumbnails - test_sideloaded_book</div>
</td> </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> </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"> <tr id='total_row' class="text-center bg-grey">
<td>Total</td> <td>Total</td>
<td>461</td> <td>462</td>
<td>448</td> <td>450</td>
<td>2</td>
<td>1</td> <td>1</td>
<td>3</td>
<td>9</td> <td>9</td>
<td>&nbsp;</td> <td>&nbsp;</td>
</tr> </tr>
@ -5269,7 +5252,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>Platform</th> <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> <td>Basic</td>
</tr> </tr>
@ -5293,7 +5276,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>Babel</th> <th>Babel</th>
<td>2.12.1</td> <td>2.13.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -5311,13 +5294,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>flask-babel</th> <th>flask-babel</th>
<td>3.0.1</td> <td>3.1.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>Flask-Limiter</th> <th>Flask-Limiter</th>
<td>3.3.1</td> <td>3.4.1</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -5335,13 +5318,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>Flask-WTF</th> <th>Flask-WTF</th>
<td>1.1.1</td> <td>1.1.2</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>greenlet</th> <th>greenlet</th>
<td>2.0.2</td> <td>3.0.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -5371,19 +5354,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>pypdf</th> <th>pypdf</th>
<td>3.7.1</td> <td>3.15.5</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>pytz</th> <th>pytz</th>
<td>2022.7.1</td> <td>2023.3.post1</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>requests</th> <th>requests</th>
<td>2.28.2</td> <td>2.31.0</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -5395,13 +5378,13 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>tornado</th> <th>tornado</th>
<td>6.2</td> <td>6.3.3</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
<tr> <tr>
<th>Unidecode</th> <th>Unidecode</th>
<td>1.3.6</td> <td>1.3.7</td>
<td>Basic</td> <td>Basic</td>
</tr> </tr>
@ -5419,7 +5402,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestBackupMetadataGdrive</td> <td>TestBackupMetadataGdrive</td>
</tr> </tr>
@ -5449,7 +5432,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestCliGdrivedb</td> <td>TestCliGdrivedb</td>
</tr> </tr>
@ -5479,7 +5462,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestEbookConvertCalibreGDrive</td> <td>TestEbookConvertCalibreGDrive</td>
</tr> </tr>
@ -5509,7 +5492,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestEbookConvertGDriveKepubify</td> <td>TestEbookConvertGDriveKepubify</td>
</tr> </tr>
@ -5551,7 +5534,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>rarfile</th> <th>rarfile</th>
<td>4.0</td> <td>4.1</td>
<td>TestEditAdditionalBooks</td> <td>TestEditAdditionalBooks</td>
</tr> </tr>
@ -5563,7 +5546,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestEditAuthorsGdrive</td> <td>TestEditAuthorsGdrive</td>
</tr> </tr>
@ -5599,7 +5582,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestEditBooksOnGdrive</td> <td>TestEditBooksOnGdrive</td>
</tr> </tr>
@ -5641,7 +5624,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>google-api-python-client</th> <th>google-api-python-client</th>
<td>2.97.0</td> <td>2.103.0</td>
<td>TestSetupGdrive</td> <td>TestSetupGdrive</td>
</tr> </tr>
@ -5677,19 +5660,19 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>python-Levenshtein</th> <th>python-Levenshtein</th>
<td>0.21.1</td> <td>0.23.0</td>
<td>TestGoodreads</td> <td>TestGoodreads</td>
</tr> </tr>
<tr> <tr>
<th>jsonschema</th> <th>jsonschema</th>
<td>4.19.0</td> <td>4.19.1</td>
<td>TestKoboSync</td> <td>TestKoboSync</td>
</tr> </tr>
<tr> <tr>
<th>jsonschema</th> <th>jsonschema</th>
<td>4.19.0</td> <td>4.19.1</td>
<td>TestKoboSyncBig</td> <td>TestKoboSyncBig</td>
</tr> </tr>
@ -5701,7 +5684,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
<tr> <tr>
<th>jsonschema</th> <th>jsonschema</th>
<td>4.19.0</td> <td>4.19.1</td>
<td>TestLdapLogin</td> <td>TestLdapLogin</td>
</tr> </tr>
@ -5731,7 +5714,7 @@ Diff is 10795 characters long. Set self.maxDiff to None to see it.</pre>
</div> </div>
<script> <script>
drawCircle(448, 1, 3, 9); drawCircle(450, 2, 1, 9);
showCase(5); showCase(5);
</script> </script>