diff --git a/cps/redirect.py b/cps/redirect.py index 7f504b987..3fd3e4943 100644 --- a/cps/redirect.py +++ b/cps/redirect.py @@ -29,7 +29,7 @@ from urllib.parse import urlparse, urljoin -from flask import request, url_for, redirect, current_app +from flask import request, url_for, current_app def is_safe_url(target): diff --git a/cps/services/goodreads_support.py b/cps/services/goodreads_support.py index c3bdbacbe..06b218bf8 100644 --- a/cps/services/goodreads_support.py +++ b/cps/services/goodreads_support.py @@ -22,7 +22,7 @@ import requests from goodreads.client import GoodreadsClient from goodreads.request import GoodreadsRequest -import xmltodict +from lxml import etree try: import Levenshtein @@ -33,6 +33,39 @@ from .. import logger from ..clean_html import clean_string + + +def etree_to_dict(t): + """ + Convert lxml ElementTree to a nested dict (similar to xmltodict). + """ + d = {t.tag: {} if t.attrib else None} + children = list(t) + + if children: + dd = {} + for dc in map(etree_to_dict, children): + for k, v in dc.items(): + if k in dd: + if not isinstance(dd[k], list): + dd[k] = [dd[k]] + dd[k].append(v) + else: + dd[k] = v + d = {t.tag: dd} + + if t.attrib: + d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) + + text = (t.text or '').strip() + if text: + if children or t.attrib: + d[t.tag]['#text'] = text + else: + d[t.tag] = text + + return d + class my_GoodreadsClient(GoodreadsClient): def request(self, *args, **kwargs): @@ -59,7 +92,9 @@ class my_GoodreadsRequest(GoodreadsRequest): if resp.status_code != 200: raise GoodreadsRequestException(resp.reason, self.path) if self.req_format == 'xml': - data_dict = xmltodict.parse(resp.content) + root = etree.fromstring(resp.content) + data_dict = etree_to_dict(root) + return data_dict['GoodreadsResponse'] else: raise Exception("Invalid format") diff --git a/cps/web.py b/cps/web.py index 31e124e93..dc75754e6 100644 --- a/cps/web.py +++ b/cps/web.py @@ -812,14 +812,19 @@ def index(page): return render_books_list("newest", sort_param, 1, page) -@web.route('//', defaults={'page': 1, 'book_id': 1}) -@web.route('///', defaults={'page': 1, 'book_id': 1}) -@web.route('///', defaults={'page': 1}) -@web.route('////') @login_required_if_no_ano def books_list(data, sort_param, book_id, page): return render_books_list(data, sort_param, book_id, page) +# Limit number of routes to avoid redirects +data =["rated", "discover", "unread", "read", "hot", "download", "author", "publisher", "series", "ratings", "formats", + "category", "language", "archived", "search", "advsearch", "newest"] +for d in data: + web.add_url_rule('/{}/'.format(d), view_func=books_list, defaults={'page': 1, 'book_id': 1, "data": d}) + web.add_url_rule('/{}//'.format(d), view_func=books_list, defaults={'page': 1, 'book_id': 1, "data": d}) + web.add_url_rule('/{}//'.format(d), view_func=books_list, defaults={'page': 1, "data": d}) + web.add_url_rule('/{}///'.format(d), defaults={"data": d}, view_func=books_list) + @web.route("/table") @user_login_required diff --git a/test/Calibre-Web TestSummary_Linux.html b/test/Calibre-Web TestSummary_Linux.html index b7c549592..0ac0e55fd 100644 --- a/test/Calibre-Web TestSummary_Linux.html +++ b/test/Calibre-Web TestSummary_Linux.html @@ -37,20 +37,20 @@
-

Start Time: 2025-09-09 21:15:46

+

Start Time: 2025-09-21 09:23:41

-

Stop Time: 2025-09-10 04:34:48

+

Stop Time: 2025-09-21 16:46:10

-

Duration: 6h 7 min

+

Duration: 6h 10 min

@@ -1670,12 +1670,12 @@ - + TestEditAuthorsGdrive 7 - 6 + 7 + 0 0 - 1 0 Detail @@ -1738,32 +1738,11 @@ - +
TestEditAuthorsGdrive - test_rename_capital_on_upload
- -
- ERROR -
- - - - + PASS @@ -2757,11 +2736,11 @@ IndexError: list index out of range - + TestKoboSync 12 - 12 - 0 + 11 + 1 0 0 @@ -2852,11 +2831,31 @@ IndexError: list index out of range - +
TestKoboSync - test_sync_shelf
- PASS + +
+ FAIL +
+ + + + @@ -3083,13 +3082,13 @@ IndexError: list index out of range TestSecurity - 5 - 5 + 6 + 6 0 0 0 - Detail + Detail @@ -3139,6 +3138,15 @@ IndexError: list index out of range + + + +
TestSecurity - test_x_forwarded_host
+ + PASS + + + @@ -4782,11 +4790,11 @@ AssertionError: False is not true - + TestThumbnails 8 - 7 - 0 + 6 + 1 0 1 @@ -4859,11 +4867,31 @@ AssertionError: False is not true - +
TestThumbnails - test_sideloaded_book
- PASS + +
+ FAIL +
+ + + + @@ -6069,10 +6097,10 @@ AssertionError: False is not true Total - 537 + 538 523 - 5 - 2 + 7 + 1 7   @@ -6101,7 +6129,7 @@ AssertionError: False is not true Platform - Linux 6.8.0-79-generic #79-Ubuntu SMP PREEMPT_DYNAMIC Tue Aug 12 14:42:46 UTC 2025 x86_64 x86_64 + Linux 6.8.0-83-generic #83-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 5 21:46:54 UTC 2025 x86_64 x86_64 Basic @@ -6281,13 +6309,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestBackupMetadataGdrive httplib2 - 0.30.0 + 0.31.0 TestBackupMetadataGdrive @@ -6311,13 +6339,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestCliGdrivedb httplib2 - 0.30.0 + 0.31.0 TestCliGdrivedb @@ -6341,13 +6369,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestEbookConvertCalibreGDrive httplib2 - 0.30.0 + 0.31.0 TestEbookConvertCalibreGDrive @@ -6371,13 +6399,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestEbookConvertGDriveKepubify httplib2 - 0.30.0 + 0.31.0 TestEbookConvertGDriveKepubify @@ -6413,13 +6441,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestEditAuthorsGdrive httplib2 - 0.30.0 + 0.31.0 TestEditAuthorsGdrive @@ -6449,13 +6477,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestEditBooksOnGdrive httplib2 - 0.30.0 + 0.31.0 TestEditBooksOnGdrive @@ -6491,13 +6519,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestEmbedMetadataGdrive httplib2 - 0.30.0 + 0.31.0 TestEmbedMetadataGdrive @@ -6521,13 +6549,13 @@ AssertionError: False is not true google-api-python-client - 2.181.0 + 2.182.0 TestSetupGdrive httplib2 - 0.30.0 + 0.31.0 TestSetupGdrive @@ -6623,7 +6651,7 @@ AssertionError: False is not true