diff --git a/cps/admin.py b/cps/admin.py index 086b614e8..0e0a61018 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -1838,6 +1838,9 @@ def _configuration_update_helper(): services.goodreads_support.connect(config.config_goodreads_api_key, config.config_use_goodreads) + # Google Books API configuration + reboot_required |=_config_string(to_save, "config_googlebooks_api_key") + _config_int(to_save, "config_updatechannel") # Reverse proxy login configuration diff --git a/cps/comic.py b/cps/comic.py index 271b1b456..f0a81b4a0 100644 --- a/cps/comic.py +++ b/cps/comic.py @@ -65,7 +65,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec cover_data = extension = None if original_file_extension.upper() == '.CBZ': cf = zipfile.ZipFile(tmp_file_name) - for name in cf.namelist(): + for name in sorted(cf.namelist()): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() @@ -74,7 +74,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec break elif original_file_extension.upper() == '.CBT': cf = tarfile.TarFile(tmp_file_name) - for name in cf.getnames(): + for name in sorted(cf.getnames()): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() @@ -85,7 +85,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec try: rarfile.UNRAR_TOOL = rar_executable cf = rarfile.RarFile(tmp_file_name) - for name in cf.namelist(): + for name in sorted(cf.namelist()): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() @@ -96,7 +96,7 @@ def _extract_cover_from_archive(original_file_extension, tmp_file_name, rar_exec log.error('Rarfile failed with error: {}'.format(ex)) elif original_file_extension.upper() == '.CB7' and use_7zip: cf = py7zr.SevenZipFile(tmp_file_name) - for name in cf.getnames(): + for name in sorted(cf.getnames()): ext = os.path.splitext(name) if len(ext) > 1: extension = ext[1].lower() diff --git a/cps/config_sql.py b/cps/config_sql.py index 7e7d033d1..bbca32415 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -117,6 +117,7 @@ class _Settings(_Base): config_use_goodreads = Column(Boolean, default=False) config_goodreads_api_key = Column(String) + config_googlebooks_api_key = Column(String, default='') config_register_email = Column(Boolean, default=False) config_login_type = Column(Integer, default=0) @@ -325,7 +326,7 @@ class ConfigSQL(object): def to_dict(self): storage = {} for k, v in self.__dict__.items(): - if k[0] != '_' and not k.endswith("_e") and not k == "cli": + if k[0] != '_' and not k.endswith("_e") and not k == "cli" and 'api' not in k.lower(): storage[k] = v return storage diff --git a/cps/editbooks.py b/cps/editbooks.py index 1999e7564..a2c39b9d9 100644 --- a/cps/editbooks.py +++ b/cps/editbooks.py @@ -406,7 +406,7 @@ def edit_book_param(param, vals, multi=False): kobo_sync_status.remove_synced_book(book.id) continue elif param == 'read_status': - error = helper.edit_book_read_status(book.id, vals['value'] == "True", True) + error = helper.edit_book_read_status(book.id, vals['value'] == "True") if error: if multi: out.append({"success":False, "msg":error}) diff --git a/cps/helper.py b/cps/helper.py index 28edb9f32..3d7121659 100644 --- a/cps/helper.py +++ b/cps/helper.py @@ -303,7 +303,7 @@ def get_sorted_author(value): return value2 -def edit_book_read_status(book_id, read_status=None, archived=False): +def edit_book_read_status(book_id, read_status=None): if not config.config_read_column: book = ub.session.query(ub.ReadBook).filter(and_(ub.ReadBook.user_id == int(current_user.id), ub.ReadBook.book_id == book_id)).first() @@ -327,7 +327,7 @@ def edit_book_read_status(book_id, read_status=None, archived=False): else: try: calibre_db.create_functions(config) - book = calibre_db.get_filtered_book(book_id, archived) + book = calibre_db.get_filtered_book(book_id, True) book_read_status = getattr(book, 'custom_column_' + str(config.config_read_column)) if len(book_read_status): if read_status is None: diff --git a/cps/kobo.py b/cps/kobo.py index 1a8dd9137..879e18f15 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -1014,6 +1014,8 @@ def handle_getests(): @kobo.route("/v1/products/dailydeal", methods=["GET", "POST"]) @kobo.route("/v1/products/deals", methods=["GET", "POST"]) @kobo.route("/v1/products", methods=["GET", "POST"]) +@kobo.route("/v1/products/", methods=["GET", "POST"]) +@kobo.route("/v1/products//", methods=["GET", "POST"]) @kobo.route("/v1/affiliate", methods=["GET", "POST"]) @kobo.route("/v1/deals", methods=["GET", "POST"]) def HandleProductsRequest(dummy=None): @@ -1201,6 +1203,9 @@ def NATIVE_KOBO_RESOURCES(): "image_host": "//cdn.kobo.com/book-images/", "image_url_quality_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg", "image_url_template": "https://cdn.kobo.com/book-images/{ImageId}/{Width}/{Height}/false/image.jpg", + "instapaper_enabled": "True", + "instapaper_env_url": "https://www.instapaper.com/api/kobo", + "instapaper_link_account_start": "https://authorize.kobo.com/{region}/{language}/linkinstapaper", "kobo_audiobooks_credit_redemption": "False", "kobo_audiobooks_enabled": "True", "kobo_audiobooks_orange_deal_enabled": "False", diff --git a/cps/metadata_provider/google.py b/cps/metadata_provider/google.py index 0f6be569f..d53d2e77e 100644 --- a/cps/metadata_provider/google.py +++ b/cps/metadata_provider/google.py @@ -23,7 +23,7 @@ from datetime import datetime import requests -from cps import logger +from cps import logger, config from cps.isoLanguages import get_lang3, get_language_name from cps.services.Metadata import MetaRecord, MetaSourceInfo, Metadata @@ -38,6 +38,7 @@ class Google(Metadata): BOOK_URL = "https://books.google.com/books?id=" SEARCH_URL = "https://www.googleapis.com/books/v1/volumes?q=" ISBN_TYPE = "ISBN_13" + API_KEY = "&key=" + config.config_googlebooks_api_key def search( self, query: str, generic_cover: str = "", locale: str = "en" @@ -50,7 +51,7 @@ class Google(Metadata): tokens = [quote(t.encode("utf-8")) for t in title_tokens] query = "+".join(tokens) try: - results = requests.get(Google.SEARCH_URL + query) + results = requests.get(Google.SEARCH_URL + query + Google.API_KEY) results.raise_for_status() except Exception as e: log.warning(e) diff --git a/cps/opds.py b/cps/opds.py index 6dea544a2..e8a405116 100644 --- a/cps/opds.py +++ b/cps/opds.py @@ -88,8 +88,8 @@ def feed_letter_books(book_id): letter, [db.Books.sort], True, config.config_read_column) - - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/new") @@ -101,7 +101,8 @@ def feed_new(): entries, __, pagination = calibre_db.fill_indexpage((int(off) / (int(config.config_books_per_page)) + 1), 0, db.Books, True, [db.Books.timestamp.desc()], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/discover") @@ -112,7 +113,8 @@ def feed_discover(): 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)) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/rated") @@ -125,7 +127,8 @@ def feed_best_rated(): db.Books, db.Books.ratings.any(db.Ratings.rating > 9), [db.Books.timestamp.desc()], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/hot") @@ -149,7 +152,8 @@ def feed_hot(): num_books = entries.__len__() pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, num_books) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/author") @@ -174,7 +178,8 @@ def feed_letter_author(book_id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, entries.count()) entries = entries.limit(config.config_books_per_page).offset(off).all() - return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_author', pagination=pagination, cc=cc) @opds.route("/opds/author/") @@ -197,7 +202,8 @@ def feed_publisherindex(): .limit(config.config_books_per_page).offset(off) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(calibre_db.session.query(db.Publishers).all())) - return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_publisher', pagination=pagination, cc=cc) @opds.route("/opds/publisher/") @@ -230,7 +236,8 @@ def feed_letter_category(book_id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, entries.count()) entries = entries.offset(off).limit(config.config_books_per_page).all() - return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_category', pagination=pagination, cc=cc) @opds.route("/opds/category/") @@ -263,7 +270,8 @@ def feed_letter_series(book_id): pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, entries.count()) entries = entries.offset(off).limit(config.config_books_per_page).all() - return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=entries, folder='opds.feed_series', pagination=pagination, cc=cc) @opds.route("/opds/series/") @@ -275,7 +283,8 @@ def feed_series(book_id): db.Books.series.any(db.Series.id == book_id), [db.Books.series_index], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/ratings") @@ -297,7 +306,8 @@ def feed_ratingindex(): element = list() for entry in entries: element.append(FeedObject(entry[0].id, _("{} Stars").format(entry.name))) - return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=element, folder='opds.feed_ratings', pagination=pagination, cc=cc) @opds.route("/opds/ratings/") @@ -321,7 +331,8 @@ def feed_formatindex(): element = list() for entry in entries: element.append(FeedObject(entry.format, entry.format)) - return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=element, folder='opds.feed_format', pagination=pagination, cc=cc) @opds.route("/opds/formats/") @@ -333,7 +344,8 @@ def feed_format(book_id): db.Books.data.any(db.Data.format == book_id.upper()), [db.Books.timestamp.desc()], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/language") @@ -351,7 +363,8 @@ def feed_languagesindex(): languages[0].name = isoLanguages.get_language_name(get_locale(), languages[0].lang_code) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(languages)) - return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=languages, folder='opds.feed_languages', pagination=pagination, cc=cc) @opds.route("/opds/language/") @@ -363,7 +376,8 @@ def feed_languages(book_id): db.Books.languages.any(db.Languages.id == book_id), [db.Books.timestamp.desc()], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) @opds.route("/opds/shelfindex") @@ -377,7 +391,8 @@ def feed_shelfindex(): number = len(shelf) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, number) - return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', listelements=shelf, folder='opds.feed_shelf', pagination=pagination, cc=cc) @opds.route("/opds/shelf/") @@ -417,7 +432,8 @@ def feed_shelf(book_id): except (OperationalError, InvalidRequestError) as e: ub.session.rollback() log.error_or_exception("Settings Database error: {}".format(e)) - return render_xml_template('feed.xml', entries=result, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc) @opds.route("/opds/download///") @@ -470,7 +486,8 @@ def feed_read_books(): 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) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc) @opds.route("/opds/unreadbooks") @@ -480,7 +497,8 @@ def feed_unread_books(): 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) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=result, pagination=pagination, cc=cc) class FeedObject: @@ -502,7 +520,8 @@ def feed_search(term): entries, __, ___ = calibre_db.get_search_results(term, config=config) entries_count = len(entries) if len(entries) > 0 else 1 pagination = Pagination(1, entries_count, entries_count) - return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', searchterm=term, entries=entries, pagination=pagination, cc=cc) else: return render_xml_template('feed.xml', searchterm="") @@ -524,7 +543,8 @@ def render_xml_dataset(data_table, book_id): getattr(db.Books, data_table.__tablename__).any(data_table.id == book_id), [db.Books.timestamp.desc()], True, config.config_read_column) - return render_xml_template('feed.xml', entries=entries, pagination=pagination) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) + return render_xml_template('feed.xml', entries=entries, pagination=pagination, cc=cc) def render_element_index(database_column, linked_table, folder): @@ -545,7 +565,9 @@ def render_element_index(database_column, linked_table, folder): elements.append({'id': entry.id, 'name': entry.id}) pagination = Pagination((int(off) / (int(config.config_books_per_page)) + 1), config.config_books_per_page, len(entries) + 1) + cc = calibre_db.get_cc_columns(config, filter_config_custom_read=True) return render_xml_template('feed.xml', letterelements=elements, folder=folder, - pagination=pagination) + pagination=pagination, + cc=cc) 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/services/worker.py b/cps/services/worker.py index a804f633b..2a0809cab 100644 --- a/cps/services/worker.py +++ b/cps/services/worker.py @@ -235,7 +235,7 @@ class CalibreTask: @property def dead(self): - """Determines whether this task can be garbage collected + """Determines whether or not this task can be garbage collected We have a separate dictating this because there may be certain tasks that want to override this """ diff --git a/cps/static/js/reading/locationchange-polyfill.js b/cps/static/js/reading/locationchange-polyfill.js deleted file mode 100644 index 4845ea846..000000000 --- a/cps/static/js/reading/locationchange-polyfill.js +++ /dev/null @@ -1,21 +0,0 @@ -// register new event emitter locationchange that fires on urlchange -// source: https://stackoverflow.com/a/52809105/21941129 -(() => { - let oldPushState = history.pushState; - history.pushState = function pushState() { - let ret = oldPushState.apply(this, arguments); - window.dispatchEvent(new Event('locationchange')); - return ret; - }; - - let oldReplaceState = history.replaceState; - history.replaceState = function replaceState() { - let ret = oldReplaceState.apply(this, arguments); - window.dispatchEvent(new Event('locationchange')); - return ret; - }; - - window.addEventListener('popstate', () => { - window.dispatchEvent(new Event('locationchange')); - }); -})(); diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index 4018db2e9..c8b77d8c6 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -163,6 +163,10 @@ {% endif %} +
+ + +
diff --git a/cps/templates/feed.xml b/cps/templates/feed.xml index eed76051f..6627daac4 100644 --- a/cps/templates/feed.xml +++ b/cps/templates/feed.xml @@ -63,7 +63,57 @@ term="{{tag.name}}" label="{{tag.name}}"/> {% endfor %} - {% if entry.Books.comments[0] %}{{entry.Books.comments[0].text|striptags}}{% endif %} +
+ {% if entry.Books.ratings.__len__() > 0 %} + RATING: {% for number in range((entry.Books.ratings[0].rating/2)|int(2)) %}★{% endfor %}
+ {% endif %} + {% if entry.Books.tags|length > 0 %} + TAGS: {% for tag in entry.Books.tags %}{{tag.name}}{{ ", " if not loop.last else "" }}{% endfor %}
+ {% endif %} + {% if entry.Books.series.__len__() > 0 %} + SERIES: {{entry.Books.series[0].name}} [{{entry.Books.series_index|formatfloat(2)}}]
+ {% endif %} + + {% if cc|length > 0 %} + {% for c in cc %} + {% if entry.Books['custom_column_' ~ c.id]|length > 0 %} + {{ c.name }}: + {% for column in entry.Books['custom_column_' ~ c.id] %} + {% if c.datatype == 'rating' %} + {{ (column.value / 2)|formatfloat }} + {% else %} + {% if c.datatype == 'bool' %} + {% if column.value == true %} + ✓ + {% else %} + ✕ + {% endif %} + {% else %} + {% if c.datatype == 'float' %} + {{ column.value|formatfloat(2) }} + {% elif c.datatype == 'datetime' %} + {{ column.value|formatdate }} + {% elif c.datatype == 'comments' %} + {{ column.value|safe }} + {% elif c.datatype == 'series' %} + {{ '%s [%s]' % (column.value, column.extra|formatfloat(2)) }} + {% elif c.datatype == 'text' %} + {{ column.value.strip() }}{% if not loop.last %}, {% endif %} + {% else %} + {{ column.value }} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} +
+ {% endif %} + {% endfor %} + {% endif %} + + {% if entry.Books.comments[0] %} +

{{entry.Books.comments[0].text}}

+ {% endif %} +
{% if entry.Books.has_cover %} diff --git a/cps/templates/layout.html b/cps/templates/layout.html index d433a5aaa..75d123676 100644 --- a/cps/templates/layout.html +++ b/cps/templates/layout.html @@ -41,8 +41,7 @@
{% endif %} {% if current_user.is_authenticated or g.allow_anonymous %} - -