Fix for #2983 (visible OPDS categories reflect settings in web UI)

Merge branch 'feature/OPDS-access'
This commit is contained in:
Ozzie Isaacs 2024-05-12 08:57:55 +02:00
commit 6760d6971c
6 changed files with 168 additions and 114 deletions

View File

@ -483,8 +483,10 @@ def autodetect_calibre_binaries():
else:
calibre_path = ["/opt/calibre/"]
for element in calibre_path:
supported_binary_paths = [os.path.join(element, binary) for binary in constants.SUPPORTED_CALIBRE_BINARIES.values()]
if all(os.path.isfile(binary_path) and os.access(binary_path, os.X_OK) for binary_path in supported_binary_paths):
supported_binary_paths = [os.path.join(element, binary)
for binary in constants.SUPPORTED_CALIBRE_BINARIES.values()]
if all(os.path.isfile(binary_path) and os.access(binary_path, os.X_OK)
for binary_path in supported_binary_paths):
values = [process_wait([binary_path, "--version"],
pattern=r'\(calibre (.*)\)') for binary_path in supported_binary_paths]
if all(values):

View File

@ -24,14 +24,14 @@ import datetime
import json
from urllib.parse import unquote_plus
from flask import Blueprint, request, render_template, make_response, abort, Response
from flask import Blueprint, request, render_template, make_response, abort, Response, g
from flask_login import current_user
from flask_babel import get_locale
from flask_babel import gettext as _
from sqlalchemy.sql.expression import func, text, or_, and_, true
from sqlalchemy.exc import InvalidRequestError, OperationalError
from . import logger, config, db, calibre_db, ub, isoLanguages
from . import logger, config, db, calibre_db, ub, isoLanguages, constants
from .usermanagement import requires_basic_auth_if_no_ano
from .helper import get_download_link, get_book_cover
from .pagination import Pagination
@ -94,6 +94,8 @@ def feed_letter_books(book_id):
@opds.route("/opds/new")
@requires_basic_auth_if_no_ano
def feed_new():
if not current_user.check_visibility(constants.SIDEBAR_RECENT):
abort(404)
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, True, [db.Books.timestamp.desc()],
@ -104,6 +106,8 @@ def feed_new():
@opds.route("/opds/discover")
@requires_basic_auth_if_no_ano
def feed_discover():
if not current_user.check_visibility(constants.SIDEBAR_RANDOM):
abort(404)
query = calibre_db.generate_linked_query(config.config_read_column, db.Books)
entries = query.filter(calibre_db.common_filters()).order_by(func.random()).limit(config.config_books_per_page)
pagination = Pagination(1, config.config_books_per_page, int(config.config_books_per_page))
@ -113,6 +117,8 @@ def feed_discover():
@opds.route("/opds/rated")
@requires_basic_auth_if_no_ano
def feed_best_rated():
if not current_user.check_visibility(constants.SIDEBAR_BEST_RATED):
abort(404)
off = request.args.get("offset") or 0
entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0,
db.Books, db.Books.ratings.any(db.Ratings.rating > 9),
@ -124,6 +130,8 @@ def feed_best_rated():
@opds.route("/opds/hot")
@requires_basic_auth_if_no_ano
def feed_hot():
if not current_user.check_visibility(constants.SIDEBAR_HOT):
abort(404)
off = request.args.get("offset") or 0
all_books = ub.session.query(ub.Downloads, func.count(ub.Downloads.book_id)).order_by(
func.count(ub.Downloads.book_id).desc()).group_by(ub.Downloads.book_id)
@ -146,12 +154,16 @@ def feed_hot():
@opds.route("/opds/author")
@requires_basic_auth_if_no_ano
def feed_authorindex():
if not current_user.check_visibility(constants.SIDEBAR_AUTHOR):
abort(404)
return render_element_index(db.Authors.sort, db.books_authors_link, 'opds.feed_letter_author')
@opds.route("/opds/author/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_author(book_id):
if not current_user.check_visibility(constants.SIDEBAR_AUTHOR):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Authors.sort).startswith(book_id)
entries = calibre_db.session.query(db.Authors).join(db.books_authors_link).join(db.Books)\
@ -173,6 +185,8 @@ def feed_author(book_id):
@opds.route("/opds/publisher")
@requires_basic_auth_if_no_ano
def feed_publisherindex():
if not current_user.check_visibility(constants.SIDEBAR_PUBLISHER):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Publishers)\
.join(db.books_publishers_link)\
@ -194,12 +208,16 @@ def feed_publisher(book_id):
@opds.route("/opds/category")
@requires_basic_auth_if_no_ano
def feed_categoryindex():
if not current_user.check_visibility(constants.SIDEBAR_CATEGORY):
abort(404)
return render_element_index(db.Tags.name, db.books_tags_link, 'opds.feed_letter_category')
@opds.route("/opds/category/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_category(book_id):
if not current_user.check_visibility(constants.SIDEBAR_CATEGORY):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Tags.name).startswith(book_id)
entries = calibre_db.session.query(db.Tags)\
@ -223,12 +241,16 @@ def feed_category(book_id):
@opds.route("/opds/series")
@requires_basic_auth_if_no_ano
def feed_seriesindex():
if not current_user.check_visibility(constants.SIDEBAR_SERIES):
abort(404)
return render_element_index(db.Series.sort, db.books_series_link, 'opds.feed_letter_series')
@opds.route("/opds/series/letter/<book_id>")
@requires_basic_auth_if_no_ano
def feed_letter_series(book_id):
if not current_user.check_visibility(constants.SIDEBAR_SERIES):
abort(404)
off = request.args.get("offset") or 0
letter = true() if book_id == "00" else func.upper(db.Series.sort).startswith(book_id)
entries = calibre_db.session.query(db.Series)\
@ -258,6 +280,8 @@ def feed_series(book_id):
@opds.route("/opds/ratings")
@requires_basic_auth_if_no_ano
def feed_ratingindex():
if not current_user.check_visibility(constants.SIDEBAR_RATING):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Ratings, func.count('books_ratings_link.book').label('count'),
(db.Ratings.rating / 2).label('name')) \
@ -284,6 +308,8 @@ def feed_ratings(book_id):
@opds.route("/opds/formats")
@requires_basic_auth_if_no_ano
def feed_formatindex():
if not current_user.check_visibility(constants.SIDEBAR_FORMAT):
abort(404)
off = request.args.get("offset") or 0
entries = calibre_db.session.query(db.Data).join(db.Books)\
.filter(calibre_db.common_filters()) \
@ -291,7 +317,6 @@ def feed_formatindex():
.order_by(db.Data.format).all()
pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page,
len(entries))
element = list()
for entry in entries:
element.append(FeedObject(entry.format, entry.format))
@ -314,6 +339,8 @@ def feed_format(book_id):
@opds.route("/opds/language/")
@requires_basic_auth_if_no_ano
def feed_languagesindex():
if not current_user.check_visibility(constants.SIDEBAR_LANGUAGE):
abort(404)
off = request.args.get("offset") or 0
if current_user.filter_language() == "all":
languages = calibre_db.speaking_language()
@ -341,6 +368,8 @@ def feed_languages(book_id):
@opds.route("/opds/shelfindex")
@requires_basic_auth_if_no_ano
def feed_shelfindex():
if not (current_user.is_authenticated or g.allow_anonymous):
abort(404)
off = request.args.get("offset") or 0
shelf = ub.session.query(ub.Shelf).filter(
or_(ub.Shelf.is_public == 1, ub.Shelf.user_id == current_user.id)).order_by(ub.Shelf.name).all()
@ -353,6 +382,8 @@ def feed_shelfindex():
@opds.route("/opds/shelf/<int:book_id>")
@requires_basic_auth_if_no_ano
def feed_shelf(book_id):
if not (current_user.is_authenticated or g.allow_anonymous):
abort(404)
off = request.args.get("offset") or 0
if current_user.is_anonymous:
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1,
@ -436,6 +467,8 @@ def feed_get_cover(book_id):
@opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano
def feed_read_books():
if not (current_user.check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not current_user.is_anonymous):
return abort(403)
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@ -444,6 +477,8 @@ def feed_read_books():
@opds.route("/opds/unreadbooks")
@requires_basic_auth_if_no_ano
def feed_unread_books():
if not (current_user.check_visibility(constants.SIDEBAR_READ_AND_UNREAD) and not current_user.is_anonymous):
return abort(403)
off = request.args.get("offset") or 0
result, pagination = render_read_books(int(off) / (int(config.config_books_per_page)) + 1, False, True)
return render_xml_template('feed.xml', entries=result, pagination=pagination)
@ -477,7 +512,7 @@ def feed_search(term):
def render_xml_template(*args, **kwargs):
# ToDo: return time in current timezone similar to %z
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs)
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, constants=constants.sidebar_settings, *args, **kwargs)
response = make_response(xml)
response.headers["Content-Type"] = "application/atom+xml; charset=utf-8"
return response

View File

@ -22,6 +22,7 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books sorted alphabetically')}}</content>
</entry>
{% if current_user.check_visibility(g.constants.SIDEBAR_HOT) %}
<entry>
<title>{{_('Hot Books')}}</title>
<link href="{{url_for('opds.feed_hot')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -29,6 +30,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Downloads.')}}</content>
</entry>
{%endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_BEST_RATED) %}
<entry>
<title>{{_('Top Rated Books')}}</title>
<link href="{{url_for('opds.feed_best_rated')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -36,6 +39,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Popular publications from this catalog based on Rating.')}}</content>
</entry>
{%endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_RECENT) %}
<entry>
<title>{{_('Recently added Books')}}</title>
<link href="{{url_for('opds.feed_new')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -43,6 +48,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('The latest Books')}}</content>
</entry>
{%endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_RANDOM) %}
<entry>
<title>{{_('Random Books')}}</title>
<link href="{{url_for('opds.feed_discover')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -50,7 +57,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Show Random Books')}}</content>
</entry>
{% if not current_user.is_anonymous %}
{%endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_READ_AND_UNREAD) and not current_user.is_anonymous %}
<entry>
<title>{{_('Read Books')}}</title>
<link href="{{url_for('opds.feed_read_books')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -66,6 +74,7 @@
<content type="text">{{_('Unread Books')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_AUTHOR) %}
<entry>
<title>{{_('Authors')}}</title>
<link href="{{url_for('opds.feed_authorindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -73,6 +82,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Author')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_PUBLISHER) %}
<entry>
<title>{{_('Publishers')}}</title>
<link href="{{url_for('opds.feed_publisherindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -80,6 +91,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by publisher')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_CATEGORY) %}
<entry>
<title>{{_('Categories')}}</title>
<link href="{{url_for('opds.feed_categoryindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -87,6 +100,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by category')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_SERIES) %}
<entry>
<title>{{_('Series')}}</title>
<link href="{{url_for('opds.feed_seriesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -94,6 +109,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by series')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_LANGUAGE) %}
<entry>
<title>{{_('Languages')}}</title>
<link href="{{url_for('opds.feed_languagesindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -101,6 +118,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Languages')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_RATING) %}
<entry>
<title>{{_('Ratings')}}</title>
<link href="{{url_for('opds.feed_ratingindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -108,7 +127,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by Rating')}}</content>
</entry>
{% endif %}
{% if current_user.check_visibility(g.constants.SIDEBAR_FORMAT) %}
<entry>
<title>{{_('File formats')}}</title>
<link href="{{url_for('opds.feed_formatindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -116,6 +136,8 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books ordered by file formats')}}</content>
</entry>
{% endif %}
{% if current_user.is_authenticated or g.allow_anonymous %}
<entry>
<title>{{_('Shelves')}}</title>
<link href="{{url_for('opds.feed_shelfindex')}}" type="application/atom+xml;profile=opds-catalog"/>
@ -123,4 +145,5 @@
<updated>{{ current_time }}</updated>
<content type="text">{{_('Books organized in shelves')}}</content>
</entry>
{% endif %}
</feed>

View File

@ -5,8 +5,8 @@ greenlet>=0.4.17,<3.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
pyasn1-modules>=0.0.8,<0.5.0
pyasn1>=0.1.9,<0.7.0
PyDrive2>=1.3.1,<1.20.0
PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0
@ -28,11 +28,11 @@ Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.42.0
# metadata extraction
rarfile>=3.2,<4.2
rarfile>=3.2,<5.0
scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2024.2.26
python-dateutil>=2.1,<2.9.0
python-dateutil>=2.1,<2.10.0
beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0
@ -42,4 +42,4 @@ natsort>=2.2.0,<8.5.0
comicapi>=2.2.0,<3.3.0
# Kobo integration
jsonschema>=3.2.0,<4.22.0
jsonschema>=3.2.0,<4.23.0

View File

@ -73,8 +73,8 @@ gdrive =
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
pyasn1-modules>=0.0.8,<0.5.0
pyasn1>=0.1.9,<0.7.0
PyDrive2>=1.3.1,<1.20.0
PyYAML>=3.12,<6.1
rsa>=3.4.2,<4.10.0
@ -91,11 +91,11 @@ oauth =
Flask-Dance>=2.0.0,<7.1.0
SQLAlchemy-Utils>=0.33.5,<0.42.0
metadata =
rarfile>=3.2,<4.2
rarfile>=3.2,<5.0
scholarly>=1.2.0,<1.8
markdown2>=2.0.0,<2.5.0
html2text>=2020.1.16,<2024.2.26
python-dateutil>=2.1,<2.9.0
python-dateutil>=2.1,<2.10.0
beautifulsoup4>=4.0.1,<4.13.0
faust-cchardet>=2.1.18,<2.1.20
py7zr>=0.15.0,<0.21.0
@ -103,5 +103,5 @@ comics =
natsort>=2.2.0,<8.5.0
comicapi>=2.2.0,<3.3.0
kobo =
jsonschema>=3.2.0,<4.22.0
jsonschema>=3.2.0,<4.23.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>2024-05-10 20:24:40</p>
<p class='text-justify attribute'><strong>Start Time: </strong>2024-05-11 18:39:24</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>2024-05-11 03:33:47</p>
<p class='text-justify attribute'><strong>Stop Time: </strong>2024-05-12 01:48:22</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 58 min</p>
<p class='text-justify attribute'><strong>Duration: </strong>5h 59 min</p>
</div>
</div>
</div>
@ -234,11 +234,11 @@
<tr id="su" class="failClass">
<tr id="su" class="passClass">
<td>TestBackupMetadata</td>
<td class="text-center">21</td>
<td class="text-center">20</td>
<td class="text-center">1</td>
<td class="text-center">21</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">
@ -320,35 +320,11 @@
<tr id="ft2.9" class="none bg-danger">
<tr id='pt2.9' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestBackupMetadata - test_backup_change_book_series_index</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft2.9')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft2.9" 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.9').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 135, in test_backup_change_book_series_index
self.assertEqual(metadata[&#39;series&#39;][&#39;content&#39;], &#34;tEst&#34;)
AssertionError: &#39;test&#39; != &#39;tEst&#39;
- test
? ^
+ tEst
? ^</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -2594,11 +2570,11 @@ IndexError: list index out of range</pre>
<tr id="su" class="failClass">
<tr id="su" class="passClass">
<td>TestGoodreads</td>
<td class="text-center">3</td>
<td class="text-center">2</td>
<td class="text-center">1</td>
<td class="text-center">3</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">0</td>
<td class="text-center">
@ -2608,31 +2584,11 @@ IndexError: list index out of range</pre>
<tr id="ft28.1" class="none bg-danger">
<tr id='pt28.1' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestGoodreads - test_author_page</div>
</td>
<td colspan='6'>
<div class="text-center">
<a class="popup_link text-center" onfocus='blur()' onclick="showTestDetail('div_ft28.1')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft28.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_ft28.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_goodreads.py&#34;, line 100, in test_author_page
self.assertTrue(self.check_element_on_page((By.CLASS_NAME, &#34;author-photo&#34;)))
AssertionError: False is not true</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3577,13 +3533,13 @@ AssertionError: False is not true</pre>
<tr id="su" class="passClass">
<td>TestOPDSFeed</td>
<td class="text-center">24</td>
<td class="text-center">24</td>
<td class="text-center">26</td>
<td class="text-center">26</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('c38', 24)">Detail</a>
<a onclick="showClassDetail('c38', 26)">Detail</a>
</td>
</tr>
@ -3591,7 +3547,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.1' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds</div>
<div class='testcase'>TestOPDSFeed - test_access_right_guest</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3600,7 +3556,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.2' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_author</div>
<div class='testcase'>TestOPDSFeed - test_access_right_user</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3609,7 +3565,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.3' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_books</div>
<div class='testcase'>TestOPDSFeed - test_opds</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3618,7 +3574,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.4' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_calibre_companion</div>
<div class='testcase'>TestOPDSFeed - test_opds_author</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3627,7 +3583,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.5' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_colon_password</div>
<div class='testcase'>TestOPDSFeed - test_opds_books</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3636,7 +3592,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.6' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_cover</div>
<div class='testcase'>TestOPDSFeed - test_opds_calibre_companion</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3645,7 +3601,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.7' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_download_book</div>
<div class='testcase'>TestOPDSFeed - test_opds_colon_password</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3654,7 +3610,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.8' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_formats</div>
<div class='testcase'>TestOPDSFeed - test_opds_cover</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3663,7 +3619,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.9' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_guest_user</div>
<div class='testcase'>TestOPDSFeed - test_opds_download_book</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3672,7 +3628,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.10' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_hot</div>
<div class='testcase'>TestOPDSFeed - test_opds_formats</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3681,7 +3637,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.11' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_language</div>
<div class='testcase'>TestOPDSFeed - test_opds_guest_user</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3690,7 +3646,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.12' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_non_admin</div>
<div class='testcase'>TestOPDSFeed - test_opds_hot</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3699,7 +3655,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.13' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_publisher</div>
<div class='testcase'>TestOPDSFeed - test_opds_language</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3708,7 +3664,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.14' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_random</div>
<div class='testcase'>TestOPDSFeed - test_opds_non_admin</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3717,7 +3673,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.15' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_ratings</div>
<div class='testcase'>TestOPDSFeed - test_opds_publisher</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3726,7 +3682,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.16' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_read_unread</div>
<div class='testcase'>TestOPDSFeed - test_opds_random</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3735,7 +3691,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.17' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_search</div>
<div class='testcase'>TestOPDSFeed - test_opds_ratings</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3744,7 +3700,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.18' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_series</div>
<div class='testcase'>TestOPDSFeed - test_opds_read_unread</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3753,7 +3709,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.19' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_shelf_access</div>
<div class='testcase'>TestOPDSFeed - test_opds_search</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3762,7 +3718,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.20' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_stats</div>
<div class='testcase'>TestOPDSFeed - test_opds_series</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3771,7 +3727,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.21' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_tags</div>
<div class='testcase'>TestOPDSFeed - test_opds_shelf_access</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3780,7 +3736,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.22' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
<div class='testcase'>TestOPDSFeed - test_opds_stats</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3789,7 +3745,7 @@ AssertionError: False is not true</pre>
<tr id='pt38.23' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_unicode_user</div>
<div class='testcase'>TestOPDSFeed - test_opds_tags</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
@ -3797,6 +3753,24 @@ AssertionError: False is not true</pre>
<tr id='pt38.24' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_opds_top_rated</div>
</td>
<td colspan='6' align='center'>PASS</td>
</tr>
<tr id='pt38.25' 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='pt38.26' class='hiddenRow bg-success'>
<td>
<div class='testcase'>TestOPDSFeed - test_recently_added</div>
</td>
@ -4447,11 +4421,11 @@ AssertionError: False is not true</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">
@ -4524,11 +4498,31 @@ AssertionError: False is not true</pre>
<tr id='pt50.8' class='hiddenRow bg-success'>
<tr id="ft50.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_ft50.8')">FAIL</a>
</div>
<!--css div popup start-->
<div id="div_ft50.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_ft50.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 326, in test_sideloaded_book
self.assertGreaterEqual(diff(BytesIO(list_cover), BytesIO(new_list_cover), delete_diff_file=True), 0.04)
AssertionError: 0.03372577030812325 not greater than or equal to 0.04</pre>
</div>
<div class="clearfix"></div>
</div>
<!--css div popup end-->
</td>
</tr>
@ -5611,9 +5605,9 @@ AssertionError: False is not true</pre>
<tr id='total_row' class="text-center bg-grey">
<td>Total</td>
<td>492</td>
<td>477</td>
<td>4</td>
<td>494</td>
<td>480</td>
<td>3</td>
<td>1</td>
<td>10</td>
<td>&nbsp;</td>
@ -5799,7 +5793,7 @@ AssertionError: False is not true</pre>
<tr>
<th>google-api-python-client</th>
<td>2.128.0</td>
<td>2.129.0</td>
<td>TestBackupMetadataGdrive</td>
</tr>
@ -5829,7 +5823,7 @@ AssertionError: False is not true</pre>
<tr>
<th>google-api-python-client</th>
<td>2.128.0</td>
<td>2.129.0</td>
<td>TestCliGdrivedb</td>
</tr>
@ -5859,7 +5853,7 @@ AssertionError: False is not true</pre>
<tr>
<th>google-api-python-client</th>
<td>2.128.0</td>
<td>2.129.0</td>
<td>TestEbookConvertCalibreGDrive</td>
</tr>
@ -6141,7 +6135,7 @@ AssertionError: False is not true</pre>
</div>
<script>
drawCircle(477, 4, 1, 10);
drawCircle(480, 3, 1, 10);
showCase(5);
</script>