1
0
mirror of https://github.com/janeczku/calibre-web synced 2024-11-28 12:30:00 +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

@ -287,5 +287,8 @@ class WebServer(object):
if self.wsgiserver:
if _GEVENT:
self.wsgiserver.close()
else:
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,6 +71,7 @@ var settings = {
fitMode: kthoom.Key.B,
theme: "light",
direction: 0, // 0 = Left to Right, 1 = Right to Left
nextPage: 0, // 0 = Reset to Top, 1 = Remember Position
scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
};
@ -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 {
@ -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,6 +651,16 @@ 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);
@ -677,6 +717,7 @@ function init(filename) {
if (["hflip", "vflip", "rotateTimes"].includes(this.name)) {
reloadImages();
} else if (this.name === "direction") {
updateDirectionButtons();
return updateProgress();
}
@ -726,7 +767,7 @@ function init(filename) {
},
});
$(".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.
var mainContentWidth = $("#mainContent").width();
var mainContentHeight = $("#mainContent").height();
@ -764,13 +805,21 @@ function init(filename) {
// Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
$("#mainContent").scroll(function (){
var scroll = $("#mainContent").scrollTop();
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) {
//Scroll Down
if (currentImage + 1 < imageFiles.length) {
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();
updateProgress();
}
@ -779,13 +828,13 @@ function init(filename) {
//Scroll Up
if (currentImage - 1 > -1) {
if (currentImageOffset(currentImage - 1) >= 0) {
currentImage--;
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">
@ -171,6 +155,15 @@
</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>
@ -188,10 +181,25 @@
</div>
</div>
<div class="overlay"></div>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<script>
$('input[name="direction"]').change(function() {
updateArrows();
});
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,6 +34,7 @@ if typing.TYPE_CHECKING:
class MyWSGIContainer(WSGIContainer):
def __call__(self, request: httputil.HTTPServerRequest) -> None:
if tornado.version_info < (6, 3, 0, -99):
data = {} # type: Dict[str, Any]
response = [] # type: List[bytes]
@ -53,7 +54,7 @@ class MyWSGIContainer(WSGIContainer):
return response.append
app_response = self.wsgi_application(
MyWSGIContainer.environ(request), start_response
MyWSGIContainer.environ(self, request), start_response
)
try:
response.extend(app_response)
@ -85,9 +86,14 @@ class MyWSGIContainer(WSGIContainer):
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)
@staticmethod
def environ(request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
def environ(self, request: httputil.HTTPServerRequest) -> Dict[Text, Any]:
try:
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>