diff --git a/README.md b/README.md index c992b026c..2d9c7d185 100755 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ Calibre-Web is a web app that offers a clean and intuitive interface for browsin - **Python Version**: Ensure you have Python 3.7 or newer. - **Imagemagick**: Required for cover extraction from EPUBs. Windows users may also need to install [Ghostscript](https://ghostscript.com/releases/gsdnld.html) for PDF cover extraction. -- **Windows Users**: Install [libmagic](https://gnuwin32.sourceforge.net/downlinks/file.php) for 32bit python or [libmagic for 64bit python](https://github.com/nscaife/file-windows/releases/tag/20170108), depending on your Python version. Make sure these files are included in your path. - **Optional Tools**: - **Calibre desktop program**: Recommended for on-the-fly conversion and metadata editing. Set the path to Calibre’s converter tool on the setup page. - **Kepubify tool**: Needed for Kobo device support. Download the tool and place the binary in `/opt/kepubify` on Linux or `C:\Program Files\kepubify` on Windows. diff --git a/cps.py b/cps.py index 039c2a5e1..d7cb393a2 100755 --- a/cps.py +++ b/cps.py @@ -43,7 +43,7 @@ def hide_console_windows(): if __name__ == '__main__': if os.name == "nt": - hide_console_windows() + pass # hide_console_windows() main() diff --git a/cps/about.py b/cps/about.py index b43c97b9b..d76fc5103 100644 --- a/cps/about.py +++ b/cps/about.py @@ -23,7 +23,7 @@ import sys import platform import sqlite3 -import importlib +from importlib.metadata import metadata from collections import OrderedDict import flask @@ -41,7 +41,7 @@ req = dep_check.load_dependencies(False) opt = dep_check.load_dependencies(True) for i in (req + opt): modules[i[1]] = i[0] -modules['Jinja2'] = importlib.metadata.version("jinja2") +modules['Jinja2'] = metadata("jinja2")["Version"] if sys.version_info < (3, 12): modules['pySqlite'] = sqlite3.version modules['SQLite'] = sqlite3.sqlite_version diff --git a/cps/admin.py b/cps/admin.py index 6252b3178..6076e86d6 100644 --- a/cps/admin.py +++ b/cps/admin.py @@ -219,7 +219,7 @@ def admin(): form_date += timedelta(hours=int(commit[20:22]), minutes=int(commit[23:])) commit = format_datetime(form_date - tz, format='short') else: - commit = version.replace("b", " Beta") + commit = version['version'].replace("b", " Beta") all_user = ub.session.query(ub.User).all() # email_settings = mail_config.get_mail_settings() diff --git a/cps/basic.py b/cps/basic.py new file mode 100644 index 000000000..b01210109 --- /dev/null +++ b/cps/basic.py @@ -0,0 +1,90 @@ +# This file is part of the Calibre-Web (https://github.com/janeczku/calibre-web) +# Copyright (C) 2018-2019 OzzieIsaacs, cervinko, jkrehm, bodybybuddha, ok11, +# andy29485, idalin, Kyosfonica, wuqi, Kennyl, lemmsh, +# falgh1, grunjol, csitko, ytils, xybydy, trasba, vrabe, +# ruben-herold, marblepebble, JackED42, SiphonSquirrel, +# apetresc, nanu-c, mutschler, carderne +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +from cps.pagination import Pagination +from flask import Blueprint +from flask_babel import gettext as _ +from flask_babel import get_locale +from flask import request, redirect, url_for + +from . import logger, isoLanguages +from . import db, config +from . import calibre_db +from .usermanagement import login_required_if_no_ano +from .render_template import render_title_template +from .web import get_sort_function + +try: + from natsort import natsorted as sort +except ImportError: + sort = sorted # Just use regular sort then, may cause issues with badly named pages in cbz/cbr files + +basic = Blueprint('basic', __name__) + +log = logger.create() + + +@basic.route("/basic", methods=["GET"]) +@login_required_if_no_ano +def index(): + term = request.args.get("query", "") # default to showing all books + limit = 15 + page = int(request.args.get("page") or 1) + off = (page - 1) * limit + order = get_sort_function("stored", "search") + join = db.books_series_link, db.Books.id == db.books_series_link.c.book, db.Series + entries, result_count, pagination = calibre_db.get_search_results(term, + config, + off, + order, + limit, + *join) + return render_title_template('basic_index.html', + searchterm=term, + pagination=pagination, + query=term, + adv_searchterm=term, + entries=entries, + result_count=result_count, + title=_("Search"), + page="search", + order=order[1]) + + +@basic.route("/basic_book/") +@login_required_if_no_ano +def show_book(book_id): + entries = calibre_db.get_book_read_archived(book_id, config.config_read_column, allow_show_archived=True) + if entries: + entry = entries[0] + for lang_index in range(0, len(entry.languages)): + entry.languages[lang_index].language_name = isoLanguages.get_language_name(get_locale(), entry.languages[ + lang_index].lang_code) + entry.ordered_authors = calibre_db.order_authors([entry]) + + return render_title_template('basic_detail.html', + entry=entry, + is_xhr=request.headers.get('X-Requested-With') == 'XMLHttpRequest', + title=entry.title, + page="book") + else: + log.debug("Selected book is unavailable. File does not exist or is not accessible") + return redirect(url_for("basic.index")) diff --git a/cps/config_sql.py b/cps/config_sql.py index c810431b1..3607f0af5 100644 --- a/cps/config_sql.py +++ b/cps/config_sql.py @@ -483,6 +483,8 @@ def autodetect_calibre_binaries(): "C:\\program files(x86)\\calibre\\", "C:\\program files(x86)\\calibre2\\", "C:\\program files\\calibre2\\"] + elif sys.platform.startswith("freebsd"): + calibre_path = ["/usr/local/bin/"] else: calibre_path = ["/opt/calibre/"] for element in calibre_path: @@ -513,6 +515,8 @@ def autodetect_unrar_binary(): if sys.platform == "win32": calibre_path = ["C:\\program files\\WinRar\\unRAR.exe", "C:\\program files(x86)\\WinRar\\unRAR.exe"] + elif sys.platform.startswith("freebsd"): + calibre_path = ["/usr/local/bin/unrar"] else: calibre_path = ["/usr/bin/unrar"] for element in calibre_path: @@ -525,6 +529,8 @@ def autodetect_kepubify_binary(): if sys.platform == "win32": calibre_path = ["C:\\program files\\kepubify\\kepubify-windows-64Bit.exe", "C:\\program files(x86)\\kepubify\\kepubify-windows-64Bit.exe"] + elif sys.platform.startswith("freebsd"): + calibre_path = ["/usr/local/bin/kepubify"] else: calibre_path = ["/opt/kepubify/kepubify-linux-64bit", "/opt/kepubify/kepubify-linux-32bit"] for element in calibre_path: diff --git a/cps/constants.py b/cps/constants.py index 0f03364ac..48b5a1060 100644 --- a/cps/constants.py +++ b/cps/constants.py @@ -172,8 +172,7 @@ BookMeta = namedtuple('BookMeta', 'file_path, extension, title, author, cover, d 'series_id, languages, publisher, pubdate, identifiers') # python build process likes to have x.y.zbw -> b for beta and w a counting number -STABLE_VERSION = '0.6.24b' -# _version = STABLE_VERSION['version'] +STABLE_VERSION = '0.6.25b' NIGHTLY_VERSION = dict() NIGHTLY_VERSION[0] = '$Format:%H$' diff --git a/cps/cw_advocate/api.py b/cps/cw_advocate/api.py index c9ed5f589..5a5ac2039 100644 --- a/cps/cw_advocate/api.py +++ b/cps/cw_advocate/api.py @@ -113,7 +113,7 @@ def get(url, **kwargs): """Sends a GET request. :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. + :param **kwargs: Optional arguments that ``request`` takes. :return: :class:`Response ` object :rtype: requests.Response """ @@ -122,84 +122,6 @@ def get(url, **kwargs): return request('get', url, **kwargs) -'''def options(url, **kwargs): - """Sends a OPTIONS request. - - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - kwargs.setdefault('allow_redirects', True) - return request('options', url, **kwargs) - - -def head(url, **kwargs): - """Sends a HEAD request. - - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - kwargs.setdefault('allow_redirects', False) - return request('head', url, **kwargs) - - -def post(url, data=None, json=None, **kwargs): - """Sends a POST request. - - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. - :param json: (optional) json data to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - return request('post', url, data=data, json=json, **kwargs) - - -def put(url, data=None, **kwargs): - """Sends a PUT request. - - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - return request('put', url, data=data, **kwargs) - - -def patch(url, data=None, **kwargs): - """Sends a PATCH request. - - :param url: URL for the new :class:`Request` object. - :param data: (optional) Dictionary, bytes, or file-like object to send in the body of the :class:`Request`. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - return request('patch', url, data=data, **kwargs) - - -def delete(url, **kwargs): - """Sends a DELETE request. - - :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. - :return: :class:`Response ` object - :rtype: requests.Response - """ - - return request('delete', url, **kwargs)''' - - class RequestsAPIWrapper: """Provides a `requests.api`-like interface with a specific validator""" diff --git a/cps/db.py b/cps/db.py index 557fdc752..c0a3fc971 100644 --- a/cps/db.py +++ b/cps/db.py @@ -102,6 +102,16 @@ class Identifiers(Base): type = Column(String(collation='NOCASE'), nullable=False, default="isbn") val = Column(String(collation='NOCASE'), nullable=False) book = Column(Integer, ForeignKey('books.id'), nullable=False) + amazon = { + "jp": "co.jp", + "uk": "co.uk", + "us": "com", + "au": "com.au", + "be": "com.be", + "br": "com.br", + "tr": "com.tr", + "mx": "com.mx", + } def __init__(self, val, id_type, book): super().__init__() @@ -114,7 +124,11 @@ class Identifiers(Base): if format_type == 'amazon': return "Amazon" elif format_type.startswith("amazon_"): - return "Amazon.{0}".format(format_type[7:].lower().replace("uk","co.uk")) + label_amazon = "Amazon.{0}" + country_code = format_type[7:].lower() + if country_code not in self.amazon: + return label_amazon.format(country_code) + return label_amazon.format(self.amazon[country_code]) elif format_type == "isbn": return "ISBN" elif format_type == "doi": @@ -149,7 +163,11 @@ class Identifiers(Base): if format_type == "amazon" or format_type == "asin": return "https://amazon.com/dp/{0}".format(self.val) elif format_type.startswith('amazon_'): - return "https://amazon.{0}/dp/{1}".format(format_type[7:].lower().replace("uk","co.uk"), self.val) + link_amazon = "https://amazon.{0}/dp/{1}" + country_code = format_type[7:].lower() + if country_code not in self.amazon: + return link_amazon.format(country_code, self.val) + return link_amazon.format(self.amazon[country_code], self.val) elif format_type == "isbn": return "https://www.worldcat.org/isbn/{0}".format(self.val) elif format_type == "doi": @@ -658,6 +676,7 @@ class CalibreDB: connect_args={'check_same_thread': False}, poolclass=StaticPool) with cls.engine.begin() as connection: + connection.execute(text('PRAGMA cache_size = 10000;')) connection.execute(text("attach database '{}' as calibre;".format(dbpath))) connection.execute(text("attach database '{}' as app_settings;".format(app_db_path))) diff --git a/cps/debug_info.py b/cps/debug_info.py index 3907f2a75..66762e1cb 100644 --- a/cps/debug_info.py +++ b/cps/debug_info.py @@ -23,11 +23,10 @@ import zipfile import json from io import BytesIO from flask_babel.speaklater import LazyString - +from importlib.metadata import metadata import os from flask import send_file -import importlib from . import logger, config from .about import collect_stats @@ -50,7 +49,7 @@ def assemble_logfiles(file_name): with open(f, 'rb') as fd: shutil.copyfileobj(fd, wfd) wfd.seek(0) - version = importlib.metadata.version("flask") + version = metadata("flask")["Version"] if int(version.split('.')[0]) < 2: return send_file(wfd, as_attachment=True, @@ -74,7 +73,7 @@ def send_debug(): for fp in file_list: zf.write(fp, os.path.basename(fp)) memory_zip.seek(0) - version = importlib.metadata.version("flask") + version = metadata("flask")["Version"] if int(version.split('.')[0]) < 2: return send_file(memory_zip, as_attachment=True, diff --git a/cps/dep_check.py b/cps/dep_check.py index 2f1569133..f1c290ade 100644 --- a/cps/dep_check.py +++ b/cps/dep_check.py @@ -39,8 +39,24 @@ def load_dependencies(optional=False): with open(req_path, 'r') as f: for line in f: if not line.startswith('#') and not line == '\n' and not line.startswith('git'): - res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?', line.strip()) + res = re.match(r'(.*?)([<=>\s]+)([\d\.]+),?\s?([<=>\s]+)?([\d\.]+)?(?:\s?;\s?' + r'(?:(python_version)\s?([<=>]+)\s?\'([\d\.]+)\'|' + r'(sys_platform)\s?([\!=]+)\s?\'([\w]+)\'))?', line.strip()) try: + if res.group(7) and res.group(8): + val = res.group(8).split(".") + if not eval(str(sys.version_info[0]) + "." + "{:02d}".format(sys.version_info[1]) + + res.group(7) + val[0] + "." + "{:02d}".format(int(val[1]))): + continue + elif res.group(10) and res.group(11): + # only installed if platform is eqal, don't check if platform is not equal + if res.group(10) == "==": + if sys.platform != res.group(11): + continue + # installed if platform is not eqal, don't check if platform is equal + elif res.group(10) == "!=": + if sys.platform == res.group(11): + continue if getattr(sys, 'frozen', False): dep_version = exe_deps[res.group(1).lower().replace('_', '-')] else: diff --git a/cps/error_handler.py b/cps/error_handler.py index 92d3e8760..0df983329 100644 --- a/cps/error_handler.py +++ b/cps/error_handler.py @@ -37,17 +37,31 @@ def error_http(error): error_code="Error {0}".format(error.code), error_name=error.name, issue=False, + goto_admin=False, unconfigured=not config.db_configured, instance=config.config_calibre_web_title ), error.code def internal_error(error): + if (isinstance(error.original_exception, AttributeError) and + error.original_exception.args[0] == "'NoneType' object has no attribute 'query'" + and error.original_exception.name == "query"): + return render_template('http_error.html', + error_code="Database Error", + error_name='The library used is invalid or has permission errors', + issue=False, + goto_admin=True, + unconfigured=False, + error_stack="", + instance=config.config_calibre_web_title + ), 500 return render_template('http_error.html', error_code="500 Internal Server Error", error_name='The server encountered an internal error and was unable to complete your ' 'request. There is an error in the application.', issue=True, + goto_admin=False, unconfigured=False, error_stack=traceback.format_exc().split("\n"), instance=config.config_calibre_web_title diff --git a/cps/isoLanguages.py b/cps/isoLanguages.py index ad4e9698a..a34cd34a0 100644 --- a/cps/isoLanguages.py +++ b/cps/isoLanguages.py @@ -101,6 +101,6 @@ def get_lang3(lang): ret_value = lang else: ret_value = "" - except KeyError: + except (KeyError, AttributeError): ret_value = lang return ret_value diff --git a/cps/jinjia.py b/cps/jinjia.py index 3db3e88a3..cb51b2bc0 100644 --- a/cps/jinjia.py +++ b/cps/jinjia.py @@ -43,6 +43,8 @@ def url_for_other_page(page): args = request.view_args.copy() args['page'] = page for get, val in request.args.items(): + if get == "page": + continue args[get] = val return url_for(request.endpoint, **args) @@ -111,7 +113,7 @@ def yesno(value, yes, no): @jinjia.app_template_filter('formatfloat') def formatfloat(value, decimals=1): - if not value: + if not value or (isinstance(value, str) and not value.is_numeric()): return value formated_value = ('{0:.' + str(decimals) + 'f}').format(value) if formated_value.endswith('.' + "0" * decimals): @@ -119,20 +121,6 @@ def formatfloat(value, decimals=1): return formated_value -'''@jinjia.app_template_filter('formatseriesindex') -def formatseriesindex_filter(series_index): - if series_index: - try: - if int(series_index) - series_index == 0: - return int(series_index) - else: - return series_index - except (ValueError, TypeError): - return series_index - return 0 -''' - - @jinjia.app_template_filter('escapedlink') def escapedlink_filter(url, text): return "{}".format(url, escape(text)) diff --git a/cps/kobo.py b/cps/kobo.py index 501cbb47f..34bf5be4f 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -1126,25 +1126,37 @@ def download_book(book_id, book_format): def NATIVE_KOBO_RESOURCES(): return { - "account_page": "https://secure.kobobooks.com/profile", + "account_page": "https://www.kobo.com/account/settings", "account_page_rakuten": "https://my.rakuten.co.jp/", + "add_device": "https://storeapi.kobo.com/v1/user/add-device", "add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}", "affiliaterequest": "https://storeapi.kobo.com/v1/affiliate", + "assets": "https://storeapi.kobo.com/v1/assets", + "audiobook": "https://storeapi.kobo.com/v1/products/audiobooks/{ProductId}", + "audiobook_detail_page": "https://www.kobo.com/{region}/{language}/audiobook/{slug}", + "audiobook_landing_page": "https://www.kobo.com/{region}/{language}/audiobooks", + "audiobook_preview": "https://storeapi.kobo.com/v1/products/audiobooks/{Id}/preview", + "audiobook_purchase_withcredit": "https://storeapi.kobo.com/v1/store/audiobook/{Id}", "audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion", "authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations", "autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete", - "blackstone_header": {"key": "x-amz-request-payer", "value": "requester"}, + "blackstone_header": { + "key": "x-amz-request-payer", + "value": "requester" + }, "book": "https://storeapi.kobo.com/v1/products/books/{ProductId}", - "book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}", - "book_detail_page_rakuten": "https://books.rakuten.co.jp/rk/{crossrevisionid}", - "book_landing_page": "https://store.kobobooks.com/ebooks", + "book_detail_page": "https://www.kobo.com/{region}/{language}/ebook/{slug}", + "book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}", + "book_landing_page": "https://www.kobo.com/ebooks", "book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions", + "browse_history": "https://storeapi.kobo.com/v1/user/browsehistory", "categories": "https://storeapi.kobo.com/v1/categories", - "categories_page": "https://store.kobobooks.com/ebooks/categories", + "categories_page": "https://www.kobo.com/ebooks/categories", "category": "https://storeapi.kobo.com/v1/categories/{CategoryId}", "category_featured_lists": "https://storeapi.kobo.com/v1/categories/{CategoryId}/featured", "category_products": "https://storeapi.kobo.com/v1/categories/{CategoryId}/products", "checkout_borrowed_book": "https://storeapi.kobo.com/v1/library/borrow", + "client_authd_referral": "https://authorize.kobo.com/api/AuthenticatedReferral/client/v1/getLink", "configuration_data": "https://storeapi.kobo.com/v1/configuration", "content_access_book": "https://storeapi.kobo.com/v1/products/books/{ProductId}/access", "customer_care_live_chat": "https://v2.zopim.com/widget/livechat.html?key=Y6gwUmnu4OATxN3Tli4Av9bYN319BTdO", @@ -1155,92 +1167,109 @@ def NATIVE_KOBO_RESOURCES(): "delete_tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/items/delete", "device_auth": "https://storeapi.kobo.com/v1/auth/device", "device_refresh": "https://storeapi.kobo.com/v1/auth/refresh", - "dictionary_host": "https://kbdownload1-a.akamaihd.net", + "dictionary_host": "https://ereaderfiles.kobo.com", "discovery_host": "https://discovery.kobobooks.com", + "ereaderdevices": "https://storeapi.kobo.com/v2/products/EReaderDeviceFeeds", "eula_page": "https://www.kobo.com/termsofuse?style=onestore", "exchange_auth": "https://storeapi.kobo.com/v1/auth/exchange", "external_book": "https://storeapi.kobo.com/v1/products/books/external/{Ids}", - "facebook_sso_page": - "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://store.kobobooks.com/", + "facebook_sso_page": "https://authorize.kobo.com/signin/provider/Facebook/login?returnUrl=http://kobo.com/", "featured_list": "https://storeapi.kobo.com/v1/products/featured/{FeaturedListId}", "featured_lists": "https://storeapi.kobo.com/v1/products/featured", "free_books_page": { "EN": "https://www.kobo.com/{region}/{language}/p/free-ebooks", "FR": "https://www.kobo.com/{region}/{language}/p/livres-gratuits", "IT": "https://www.kobo.com/{region}/{language}/p/libri-gratuiti", - "NL": "https://www.kobo.com/{region}/{language}/" - "List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg", - "PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis", + "NL": "https://www.kobo.com/{region}/{language}/List/bekijk-het-overzicht-van-gratis-ebooks/QpkkVWnUw8sxmgjSlCbJRg", + "PT": "https://www.kobo.com/{region}/{language}/p/livros-gratis" }, "fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback", + "funnel_metrics": "https://storeapi.kobo.com/v1/funnelmetrics", + "get_download_keys": "https://storeapi.kobo.com/v1/library/downloadkeys", + "get_download_link": "https://storeapi.kobo.com/v1/library/downloadlink", "get_tests_request": "https://storeapi.kobo.com/v1/analytics/gettests", "giftcard_epd_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem-ereader", "giftcard_redeem_url": "https://www.kobo.com/{storefront}/{language}/redeem", - "help_page": "https://www.kobo.com/help", - "kobo_audiobooks_enabled": "False", + "gpb_flow_enabled": "False", + "help_page": "http://www.kobo.com/help", + "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", + "kobo_audiobooks_credit_redemption": "False", + "kobo_audiobooks_enabled": "True", "kobo_audiobooks_orange_deal_enabled": "False", "kobo_audiobooks_subscriptions_enabled": "False", - "kobo_nativeborrow_enabled": "True", + "kobo_display_price": "True", + "kobo_dropbox_link_account_enabled": "False", + "kobo_google_tax": "False", + "kobo_googledrive_link_account_enabled": "False", + "kobo_nativeborrow_enabled": "False", + "kobo_onedrive_link_account_enabled": "False", "kobo_onestorelibrary_enabled": "False", + "kobo_privacyCentre_url": "https://www.kobo.com/privacy", "kobo_redeem_enabled": "True", "kobo_shelfie_enabled": "False", - "kobo_subscriptions_enabled": "False", - "kobo_superpoints_enabled": "False", + "kobo_subscriptions_enabled": "True", + "kobo_superpoints_enabled": "True", "kobo_wishlist_enabled": "True", "library_book": "https://storeapi.kobo.com/v1/user/library/books/{LibraryItemId}", "library_items": "https://storeapi.kobo.com/v1/user/library", "library_metadata": "https://storeapi.kobo.com/v1/library/{Ids}/metadata", "library_prices": "https://storeapi.kobo.com/v1/user/library/previews/prices", - "library_stack": "https://storeapi.kobo.com/v1/user/library/stacks/{LibraryItemId}", + "library_search": "https://storeapi.kobo.com/v1/library/search", "library_sync": "https://storeapi.kobo.com/v1/library/sync", - "love_dashboard_page": "https://store.kobobooks.com/{culture}/kobosuperpoints", - "love_points_redemption_page": - "https://store.kobobooks.com/{culture}/KoboSuperPointsRedemption?productId={ProductId}", - "magazine_landing_page": "https://store.kobobooks.com/emagazines", + "love_dashboard_page": "https://www.kobo.com/{region}/{language}/kobosuperpoints", + "love_points_redemption_page": "https://www.kobo.com/{region}/{language}/KoboSuperPointsRedemption?productId={ProductId}", + "magazine_landing_page": "https://www.kobo.com/emagazines", + "more_sign_in_options": "https://authorize.kobo.com/signin?returnUrl=http://kobo.com/#allProviders", + "notebooks": "https://storeapi.kobo.com/api/internal/notebooks", "notifications_registration_issue": "https://storeapi.kobo.com/v1/notifications/registration", "oauth_host": "https://oauth.kobo.com", - "overdrive_account": "https://auth.overdrive.com/account", - "overdrive_library": "https://{libraryKey}.auth.overdrive.com/library", - "overdrive_library_finder_host": "https://libraryfinder.api.overdrive.com", - "overdrive_thunder_host": "https://thunder.api.overdrive.com", - "password_retrieval_page": "https://www.kobobooks.com/passwordretrieval.html", + "password_retrieval_page": "https://www.kobo.com/passwordretrieval.html", + "personalizedrecommendations": "https://storeapi.kobo.com/v2/users/personalizedrecommendations", + "pocket_link_account_start": "https://authorize.kobo.com/{region}/{language}/linkpocket", "post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event", + "ppx_purchasing_url": "https://purchasing.kobo.com", "privacy_page": "https://www.kobo.com/privacypolicy?style=onestore", "product_nextread": "https://storeapi.kobo.com/v1/products/{ProductIds}/nextread", "product_prices": "https://storeapi.kobo.com/v1/products/{ProductIds}/prices", "product_recommendations": "https://storeapi.kobo.com/v1/products/{ProductId}/recommendations", "product_reviews": "https://storeapi.kobo.com/v1/products/{ProductIds}/reviews", "products": "https://storeapi.kobo.com/v1/products", - "provider_external_sign_in_page": - "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://store.kobobooks.com/", - "purchase_buy": "https://www.kobo.com/checkout/createpurchase/", - "purchase_buy_templated": "https://www.kobo.com/{culture}/checkout/createpurchase/{ProductId}", + "productsv2": "https://storeapi.kobo.com/v2/products", + "provider_external_sign_in_page": "https://authorize.kobo.com/ExternalSignIn/{providerName}?returnUrl=http://kobo.com/", "quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout", "quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase", + "rakuten_token_exchange": "https://storeapi.kobo.com/v1/auth/rakuten_token_exchange", "rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}", + "reading_services_host": "https://readingservices.kobo.com", "reading_state": "https://storeapi.kobo.com/v1/library/{Ids}/state", - "redeem_interstitial_page": "https://store.kobobooks.com", - "registration_page": "https://authorize.kobo.com/signup?returnUrl=http://store.kobobooks.com/", + "redeem_interstitial_page": "https://www.kobo.com", + "registration_page": "https://authorize.kobo.com/signup?returnUrl=http://kobo.com/", "related_items": "https://storeapi.kobo.com/v1/products/{Id}/related", "remaining_book_series": "https://storeapi.kobo.com/v1/products/books/series/{SeriesId}", "rename_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}", "review": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}", "review_sentiment": "https://storeapi.kobo.com/v1/products/reviews/{ReviewId}/sentiment/{Sentiment}", "shelfie_recommendations": "https://storeapi.kobo.com/v1/user/recommendations/shelfie", - "sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://store.kobobooks.com/", + "sign_in_page": "https://authorize.kobo.com/signin?returnUrl=http://kobo.com/", "social_authorization_host": "https://social.kobobooks.com:8443", "social_host": "https://social.kobobooks.com", - "stacks_host_productId": "https://store.kobobooks.com/collections/byproductid/", "store_home": "www.kobo.com/{region}/{language}", - "store_host": "store.kobobooks.com", - "store_newreleases": "https://store.kobobooks.com/{culture}/List/new-releases/961XUjtsU0qxkFItWOutGA", - "store_search": "https://store.kobobooks.com/{culture}/Search?Query={query}", - "store_top50": "https://store.kobobooks.com/{culture}/ebooks/Top", + "store_host": "www.kobo.com", + "store_newreleases": "https://www.kobo.com/{region}/{language}/List/new-releases/961XUjtsU0qxkFItWOutGA", + "store_search": "https://www.kobo.com/{region}/{language}/Search?Query={query}", + "store_top50": "https://www.kobo.com/{region}/{language}/ebooks/Top", + "subs_landing_page": "https://www.kobo.com/{region}/{language}/plus", + "subs_management_page": "https://www.kobo.com/{region}/{language}/account/subscriptions", + "subs_plans_page": "https://www.kobo.com/{region}/{language}/plus/plans", + "subs_purchase_buy_templated": "https://www.kobo.com/{region}/{language}/Checkoutoption/{ProductId}/{TierId}", "tag_items": "https://storeapi.kobo.com/v1/library/tags/{TagId}/Items", "tags": "https://storeapi.kobo.com/v1/library/tags", "taste_profile": "https://storeapi.kobo.com/v1/products/tasteprofile", + "terms_of_sale_page": "https://authorize.kobo.com/{region}/{language}/terms/termsofsale", "update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview", - "use_one_store": "False", + "use_one_store": "True", "user_loyalty_benefits": "https://storeapi.kobo.com/v1/user/loyalty/benefits", "user_platform": "https://storeapi.kobo.com/v1/user/platform", "user_profile": "https://storeapi.kobo.com/v1/user/profile", @@ -1248,6 +1277,6 @@ def NATIVE_KOBO_RESOURCES(): "user_recommendations": "https://storeapi.kobo.com/v1/user/recommendations", "user_reviews": "https://storeapi.kobo.com/v1/user/reviews", "user_wishlist": "https://storeapi.kobo.com/v1/user/wishlist", - "userguide_host": "https://kbdownload1-a.akamaihd.net", - "wishlist_page": "https://store.kobobooks.com/{region}/{language}/account/wishlist", + "userguide_host": "https://ereaderfiles.kobo.com", + "wishlist_page": "https://www.kobo.com/{region}/{language}/account/wishlist" } diff --git a/cps/main.py b/cps/main.py index 1b3b40b4f..44e563f93 100644 --- a/cps/main.py +++ b/cps/main.py @@ -31,6 +31,7 @@ def main(): app = create_app() from .web import web + from .basic import basic from .opds import opds from .admin import admi from .gdrive import gdrive @@ -64,6 +65,7 @@ def main(): app.register_blueprint(search) app.register_blueprint(tasks) app.register_blueprint(web) + app.register_blueprint(basic) app.register_blueprint(opds) limiter.limit("3/minute", key_func=request_username)(opds) app.register_blueprint(jinjia) diff --git a/cps/static/css/basic.css b/cps/static/css/basic.css new file mode 100644 index 000000000..7fe9e5b8a --- /dev/null +++ b/cps/static/css/basic.css @@ -0,0 +1,108 @@ +body { + margin: 0; +} + +nav { + height: 75px; + padding: 5px 20px; + width: 100%; + display: table; + box-sizing: border-box; + border-bottom: 1px solid black; +} + +nav > * { + display: inline-block; + display: table-cell; + vertical-align: middle; + float: none; + text-align: center; + width: auto; +} + +nav > *:first-child { + text-align: left; + width: 1%; +} + +.theme{ + text-align: center; + margin: 10px; +} +nav > *:last-child { + text-align: right; + width: 1%; +} + +nav > a { + color: black; + margin: 0 20px; +} + +.search { + margin: auto auto; +} + +form > input { + width: 18ch; + padding-left: 4px; +} + +form > * { + height: 50px; + background-color: white; + border-radius: 0; + border: 1px solid #ccc; + padding: 0; + margin: 0; + display: inline-block; + vertical-align: top; +} + +form > span { + margin-left: -5px; +} + +button { + border: none; + padding: 0 10px; + margin: 0; + width: 160px; + height: 100%; + background-color: white; +} + +.body { + padding: 5px 20px; +} + +a { + color: black; +} + +img { + width: 150px; + height: 250px; + object-fit: cover; +} + +.listing { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-right: 20px; +} + +.pagination { + padding: 10px 0; + height: 20px; + font-weight: 700; +} + +.pagination > div { + float: left; +} + +.pagination > div:last-child { + float: right; +} diff --git a/cps/static/js/caliBlur.js b/cps/static/js/caliBlur.js old mode 100755 new mode 100644 diff --git a/cps/tasks/convert.py b/cps/tasks/convert.py old mode 100755 new mode 100644 diff --git a/cps/templates/basic_detail.html b/cps/templates/basic_detail.html new file mode 100644 index 000000000..1996497ba --- /dev/null +++ b/cps/templates/basic_detail.html @@ -0,0 +1,81 @@ +{% extends "basic_layout.html" %} + +{% block body %} +
+ +

{{ entry.title }}

+
+ {% for author in entry.ordered_authors %} +

{{ author.name.replace("|",",") }}

+ {% endfor %} +
+ +
+ +
+ +{% if current_user.role_download() %} + {% if entry.data|length %} +
+

Download

+ {% for format in entry.data %} +

+ + {{ format.format }} ({{ format.uncompressed_size|filesizeformat }}) +

+ {% endfor %} +
+ {% endif %} +{% endif %} + +

Details

+ +{% if entry.series|length > 0 %} +

{{ _("Book %(index)s of %(range)s", index=entry.series_index | formatfloat(2), range=(entry.series[0].name)|safe) }}

+{% endif %} + +{% if entry.languages|length > 0 %} +
+

+ + {{_('Language')}}: {% for language in entry.languages %}{{language.language_name}}{% if not loop.last %}, {% endif %}{% endfor %} + +

+
+{% endif %} + +{% if entry.identifiers|length > 0 %} +
+

+ + {% for identifier in entry.identifiers %} +

{{ identifier.format_type() }}: {{ identifier|escape }}

+ {% endfor %} +

+
+{% endif %} + +{% if entry.publishers|length > 0 %} +
+

+ {{ _('Publisher') }}: + {{ entry.publishers[0].name }} + +

+
+{% endif %} + +{% if (entry.pubdate|string)[:10] != '0101-01-01' %} +
+

{{ _('Published') }}: {{ entry.pubdate|formatdate }}

+
+{% endif %} + +{% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %} +
+

{{ _('Description:') }}

+ {{ entry.comments[0].text|safe }} +
+{% endif %} +
+{% endblock %} diff --git a/cps/templates/basic_index.html b/cps/templates/basic_index.html new file mode 100644 index 000000000..ae88087f7 --- /dev/null +++ b/cps/templates/basic_index.html @@ -0,0 +1,32 @@ +{% extends "basic_layout.html" %} +{% block body %} + + + +{% if entries|length < 1 %} +

{{_('No Results Found')}}

+{% endif %} + +{% for entry in entries %} + {% if entry.Books.authors %} + {% set author = entry.Books.authors[0].name.replace('|',',')|shortentitle(30) %} + {% else %} + {% set author = '' %} + {% endif %} + +

{{ author }} - {{entry.Books.title|shortentitle}}

+
+{% endfor %} + +{% endblock %} diff --git a/cps/templates/basic_layout.html b/cps/templates/basic_layout.html new file mode 100644 index 000000000..b3000be83 --- /dev/null +++ b/cps/templates/basic_layout.html @@ -0,0 +1,47 @@ + + + + +{{instance}} | {{title}} + + + + + + +
+
+ {% if current_user.is_authenticated or g.allow_anonymous %} + + + {% endif %} +
+
+
+ {% block body %} + {% endblock %} +
+ + + diff --git a/cps/templates/book_edit.html b/cps/templates/book_edit.html index 1e4a1bc45..71d62ca4e 100644 --- a/cps/templates/book_edit.html +++ b/cps/templates/book_edit.html @@ -86,7 +86,7 @@
- +
{% for identifier in book.identifiers %} @@ -94,6 +94,7 @@ {% endfor %} +
{{_('Remove')}}
{{_('Add Identifier')}}
diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html old mode 100755 new mode 100644 diff --git a/cps/templates/detail.html b/cps/templates/detail.html index 1eb81e30f..b273ef9ee 100644 --- a/cps/templates/detail.html +++ b/cps/templates/detail.html @@ -1,4 +1,12 @@ {% extends is_xhr|yesno("fragment.html", "layout.html") %} +{% block header %} + + + {% if entry.comments|length > 0 and entry.comments[0].text|length > 0 %} + + + {% endif %} +{% endblock %} {% block body %}
diff --git a/cps/templates/http_error.html b/cps/templates/http_error.html index 26c3c379e..1c3e17a80 100644 --- a/cps/templates/http_error.html +++ b/cps/templates/http_error.html @@ -48,7 +48,11 @@
{% if g.current_theme == 1 %} -
+
{% endif %} {% if current_user.is_authenticated or g.allow_anonymous %} -