From b831b9d6b2cd880e73b7521ef70cbc61c3d1ebbe Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Sat, 21 Dec 2019 20:42:26 -0500 Subject: [PATCH 1/5] Integrate with the official Kobo store endpoint so that no functionanility is lost by overriding the api_endpoint setting. Requests are either: * Redirected to the Kobo Store * Proxied to the Kobo Store * Proxied to the Kobo Store and merged with results from CalibreWeb. --- cps/kobo.py | 293 ++++++++++++++++++++++------------------------------ 1 file changed, 124 insertions(+), 169 deletions(-) diff --git a/cps/kobo.py b/cps/kobo.py index c44915c4..d11ab8e5 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -25,16 +25,27 @@ from datetime import datetime from time import gmtime, strftime from jsonschema import validate, exceptions -from flask import Blueprint, request, make_response, jsonify, json, current_app, url_for - +from flask import ( + Blueprint, + request, + make_response, + jsonify, + json, + current_app, + url_for, + redirect, +) from flask_login import login_required +from werkzeug.datastructures import Headers from sqlalchemy import func +import requests from . import config, logger, kobo_auth, db, helper from .web import download_required -#TODO: Test more formats :) . +# TODO: Test more formats :) . KOBO_SUPPORTED_FORMATS = {"KEPUB"} +KOBO_STOREAPI_URL = "https://storeapi.kobo.com" kobo = Blueprint("kobo", __name__, url_prefix="/kobo/") kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) @@ -55,6 +66,47 @@ def to_epoch_timestamp(datetime_object): return (datetime_object - datetime(1970, 1, 1)).total_seconds() +def get_store_url_for_current_request(): + # Programmatically modify the current url to point to the official Kobo store + base, sep, request_path_with_auth_token = request.full_path.rpartition("/kobo/") + auth_token, sep, request_path = request_path_with_auth_token.rstrip("?").partition( + "/" + ) + return KOBO_STOREAPI_URL + "/" + request_path + + +CONNECTION_SPECIFIC_HEADERS = [ + "connection", + "content-encoding", + "content-length", + "transfer-encoding", +] + + +def redirect_or_proxy_request(): + if request.method == "GET": + return redirect(get_store_url_for_current_request(), 307) + else: + # The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves. + outgoing_headers = Headers(request.headers) + outgoing_headers.remove("Host") + store_response = requests.request( + method=request.method, + url=get_store_url_for_current_request(), + headers=outgoing_headers, + data=request.get_data(), + allow_redirects=False, + ) + + response_headers = store_response.headers + for header_key in CONNECTION_SPECIFIC_HEADERS: + response_headers.pop(header_key, default=None) + + return make_response( + store_response.content, store_response.status_code, response_headers.items() + ) + + class SyncToken: """ The SyncToken is used to persist state accross requests. When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service. @@ -138,6 +190,14 @@ class SyncToken: books_last_modified=books_last_modified, ) + def set_kobo_store_header(self, store_headers): + store_headers.set(SyncToken.SYNC_TOKEN_HEADER, self.raw_kobo_store_token) + + def merge_from_store_response(self, store_response): + self.raw_kobo_store_token = store_response.headers.get( + SyncToken.SYNC_TOKEN_HEADER, "" + ) + def to_headers(self, headers): headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token() @@ -198,13 +258,40 @@ def HandleSyncRequest(): # Missing feature: Detect server-side book deletions. - # Missing feature: Join the response with results from the official Kobo store so that users can still buy and access books from the device store (particularly while on-the-road). + return generate_sync_response(request, sync_token, entitlements) + + +def generate_sync_response(request, sync_token, entitlements): + # We first merge in sync results from the official Kobo store. + outgoing_headers = Headers(request.headers) + outgoing_headers.remove("Host") + sync_token.set_kobo_store_header(outgoing_headers) + store_response = requests.request( + method=request.method, + url=get_store_url_for_current_request(), + headers=outgoing_headers, + data=request.get_data(), + ) + + store_entitlements = store_response.json() + entitlements += store_entitlements + sync_token.merge_from_store_response(store_response) response = make_response(jsonify(entitlements)) sync_token.to_headers(response.headers) - response.headers["x-kobo-sync-mode"] = "delta" - response.headers["x-kobo-apitoken"] = "e30=" + try: + # These headers could probably use some more investigation. + response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"] + response.headers["x-kobo-sync-mode"] = store_response.headers[ + "x-kobo-sync-mode" + ] + response.headers["x-kobo-recent-reads"] = store_response.headers[ + "x-kobo-recent-reads" + ] + except KeyError: + pass + return response @@ -216,7 +303,7 @@ def HandleMetadataRequest(book_uuid): book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() if not book or not book.data: log.info(u"Book %s not found in database", book_uuid) - return make_response("Book not found in database.", 404) + return redirect_or_proxy_request() metadata = get_metadata(book) return jsonify([metadata]) @@ -356,7 +443,7 @@ def HandleCoverImageRequest(book_uuid, horizontal, vertical, jpeg_quality, monoc book_uuid, use_generic_cover_on_failure=False ) if not book_cover: - return make_response() + return redirect(get_store_url_for_current_request(), 307) return book_cover @@ -365,173 +452,41 @@ def TopLevelEndpoint(): return make_response(jsonify({})) -@kobo.route("/v1/user/profile") -@kobo.route("/v1/user/loyalty/benefits") -@kobo.route("/v1/analytics/gettests/", methods=["GET", "POST"]) -@kobo.route("/v1/user/wishlist") -@kobo.route("/v1/user/") -@kobo.route("/v1/user/recommendations") -@kobo.route("/v1/products/") -@kobo.route("/v1/products//nextread") -@kobo.route("/v1/products/featured/") -@kobo.route("/v1/products/featured/") -@kobo.route("/v1/library/", methods=["DELETE", "GET"]) # TODO: implement -def HandleDummyRequest(dummy=None): - return make_response(jsonify({})) +# TODO: Implement the following routes +@kobo.route("/v1/library/", methods=["DELETE", "GET"]) +@kobo.route("/v1/library//state", methods=["PUT"]) +@kobo.route("/v1/library/tags", methods=["POST"]) +@kobo.route("/v1/library/tags/", methods=["POST"]) +@kobo.route("/v1/library/tags/", methods=["DELETE"]) +def HandleUnimplementedRequest(book_uuid=None, shelf_name=None, tag_id=None): + return redirect_or_proxy_request() -@kobo.route("/v1/auth/device", methods=["POST"]) -def HandleAuthRequest(): - # This AuthRequest isn't used for most of our usecases. - response = make_response( - jsonify( - { - "AccessToken": "abcde", - "RefreshToken": "abcde", - "TokenType": "Bearer", - "TrackingId": "abcde", - "UserKey": "abcdefgeh", - } - ) - ) - return response +@kobo.app_errorhandler(404) +def handle_404(err): + # This handler acts as a catch-all for endpoints that we don't have an interest in + # implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc) + return redirect_or_proxy_request() @kobo.route("/v1/initialization") def HandleInitRequest(): - resources = NATIVE_KOBO_RESOURCES( - calibre_web_url=url_for("web.index", _external=True).strip("/") + outgoing_headers = Headers(request.headers) + outgoing_headers.remove("Host") + store_response = requests.request( + method=request.method, + url=get_store_url_for_current_request(), + headers=outgoing_headers, + data=request.get_data(), ) - response = make_response(jsonify({"Resources": resources})) - response.headers["x-kobo-apitoken"] = "e30=" - return response + store_response_json = store_response.json() + if "Resources" in store_response_json: + kobo_resources = store_response_json["Resources"] -def NATIVE_KOBO_RESOURCES(calibre_web_url): - return { - "account_page": "https://secure.kobobooks.com/profile", - "account_page_rakuten": "https://my.rakuten.co.jp/", - "add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}", - "affiliaterequest": "https://storeapi.kobo.com/v1/affiliate", - "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"}, - "book": "https://storeapi.kobo.com/v1/products/books/{ProductId}", - "book_detail_page": "https://store.kobobooks.com/{culture}/ebook/{slug}", - "book_detail_page_rakuten": "http://books.rakuten.co.jp/rk/{crossrevisionid}", - "book_landing_page": "https://store.kobobooks.com/ebooks", - "book_subscription": "https://storeapi.kobo.com/v1/products/books/subscriptions", - "categories": "https://storeapi.kobo.com/v1/categories", - "categories_page": "https://store.kobobooks.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", - "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", - "daily_deal": "https://storeapi.kobo.com/v1/products/dailydeal", - "deals": "https://storeapi.kobo.com/v1/deals", - "delete_entitlement": "https://storeapi.kobo.com/v1/library/{Ids}", - "delete_tag": "https://storeapi.kobo.com/v1/library/tags/{TagId}", - "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", - "discovery_host": "https://discovery.kobobooks.com", - "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/", - "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", - }, - "fte_feedback": "https://storeapi.kobo.com/v1/products/ftefeedback", - "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": "http://www.kobo.com/help", - "image_host": calibre_web_url, - "image_url_quality_template": calibre_web_url - + "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg", - "image_url_template": calibre_web_url - + "/{ImageId}/{Width}/{Height}/false/image.jpg", - "kobo_audiobooks_enabled": "False", - "kobo_audiobooks_orange_deal_enabled": "False", - "kobo_audiobooks_subscriptions_enabled": "False", - "kobo_nativeborrow_enabled": "True", - "kobo_onestorelibrary_enabled": "False", - "kobo_redeem_enabled": "True", - "kobo_shelfie_enabled": "False", - "kobo_subscriptions_enabled": "False", - "kobo_superpoints_enabled": "False", - "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_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", - "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", - "post_analytics_event": "https://storeapi.kobo.com/v1/analytics/event", - "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}", - "quickbuy_checkout": "https://storeapi.kobo.com/v1/store/quickbuy/{PurchaseId}/checkout", - "quickbuy_create": "https://storeapi.kobo.com/v1/store/quickbuy/purchase", - "rating": "https://storeapi.kobo.com/v1/products/{ProductId}/rating/{Rating}", - "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/", - "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/", - "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", - "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", - "update_accessibility_to_preview": "https://storeapi.kobo.com/v1/library/{EntitlementIds}/preview", - "use_one_store": "False", - "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", - "user_ratings": "https://storeapi.kobo.com/v1/user/ratings", - "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", - } + calibre_web_url=url_for("web.index", _external=True).strip("/") + kobo_resources["image_host"] = calibre_web_url + kobo_resources["image_url_quality_template"] = calibre_web_url + "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg" + kobo_resources["image_url_template"] = calibre_web_url + "/{ImageId}/{Width}/{Height}/false/image.jpg" + + return make_response(store_response_json, store_response.status_code) From cdcb8a50d130a5cc61fc4ca52b0f1f029400cce3 Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Sun, 22 Dec 2019 12:40:31 -0500 Subject: [PATCH 2/5] Fix /reconnect endpoint, which was broken by https://github.com/janeczku/calibre-web/commit/006e596c7221729c9a37b79037fa72bb88506492 --- cps/db.py | 3 ++- cps/web.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cps/db.py b/cps/db.py index 5765bf68..d4b4a7ed 100755 --- a/cps/db.py +++ b/cps/db.py @@ -33,7 +33,7 @@ from sqlalchemy.ext.declarative import declarative_base session = None cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] cc_classes = {} - +engine = None Base = declarative_base() @@ -327,6 +327,7 @@ def update_title_sort(config, conn=None): def setup_db(config): dispose() + global engine if not config.config_calibre_dir: config.invalidate() diff --git a/cps/web.py b/cps/web.py index 7aa921e4..019887c7 100644 --- a/cps/web.py +++ b/cps/web.py @@ -44,7 +44,7 @@ from werkzeug.exceptions import default_exceptions from werkzeug.datastructures import Headers from werkzeug.security import generate_password_hash, check_password_hash -from . import constants, logger, isoLanguages, services, worker +from . import constants, config, logger, isoLanguages, services, worker from . import searched_ids, lm, babel, db, ub, config, get_locale, app from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \ @@ -786,7 +786,7 @@ def get_tasks_status(): def reconnect(): db.session.close() db.engine.dispose() - db.setup_db() + db.setup_db(config) return json.dumps({}) @web.route("/search", methods=["GET"]) From c238367b64da5af4fb194b4d7644b88443cbfbb8 Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Sun, 22 Dec 2019 12:58:03 -0500 Subject: [PATCH 3/5] Reload database on every call to v1/library/sync. This fixes an issue where side-loaded books appear in the sync response with no download urls. --- cps/db.py | 5 +++++ cps/kobo.py | 4 ++++ cps/web.py | 4 +--- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cps/db.py b/cps/db.py index d4b4a7ed..49c44701 100755 --- a/cps/db.py +++ b/cps/db.py @@ -429,3 +429,8 @@ def dispose(): if name.startswith("custom_column_") or name.startswith("books_custom_column_"): if table is not None: Base.metadata.remove(table) + +def reconnect_db(config): + session.close() + engine.dispose() + setup_db(config) \ No newline at end of file diff --git a/cps/kobo.py b/cps/kobo.py index d11ab8e5..840e9e51 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -227,6 +227,10 @@ def HandleSyncRequest(): new_books_last_created = sync_token.books_last_created entitlements = [] + # We reload the book database so that the user get's a fresh view of the library + # in case of external changes (e.g: adding a book through Calibre). + db.reconnect_db(config) + # sqlite gives unexpected results when performing the last_modified comparison without the datetime cast. # It looks like it's treating the db.Books.last_modified field as a string and may fail # the comparison because of the +00:00 suffix. diff --git a/cps/web.py b/cps/web.py index 019887c7..ed01991e 100644 --- a/cps/web.py +++ b/cps/web.py @@ -784,9 +784,7 @@ def get_tasks_status(): @app.route("/reconnect") def reconnect(): - db.session.close() - db.engine.dispose() - db.setup_db(config) + db.reconnect_db(config) return json.dumps({}) @web.route("/search", methods=["GET"]) From d81dbb13e46cbbac974fd0fb3dd8dc35a33379bd Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Sun, 22 Dec 2019 15:45:19 -0500 Subject: [PATCH 4/5] Support Epub downloads --- cps/kobo.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/cps/kobo.py b/cps/kobo.py index 840e9e51..6c67aae9 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -43,8 +43,7 @@ import requests from . import config, logger, kobo_auth, db, helper from .web import download_required -# TODO: Test more formats :) . -KOBO_SUPPORTED_FORMATS = {"KEPUB"} +KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]} KOBO_STOREAPI_URL = "https://storeapi.kobo.com" kobo = Blueprint("kobo", __name__, url_prefix="/kobo/") @@ -238,7 +237,7 @@ def HandleSyncRequest(): db.session.query(db.Books) .join(db.Data) .filter(func.datetime(db.Books.last_modified) > sync_token.books_last_modified) - .filter(db.Data.format.in_(KOBO_SUPPORTED_FORMATS)) + .filter(db.Data.format.in_(KOBO_FORMATS)) .all() ) for book in changed_entries: @@ -374,14 +373,17 @@ def get_metadata(book): download_urls = [] for book_data in book.data: - if book_data.format in KOBO_SUPPORTED_FORMATS: + if book_data.format not in KOBO_FORMATS: + continue + for kobo_format in KOBO_FORMATS[book_data.format]: download_urls.append( { - "Format": book_data.format, + "Format": kobo_format, "Size": book_data.uncompressed_size, "Url": get_download_url_for_book(book, book_data.format), + # The Kobo forma accepts platforms: (Generic, Android) + "Platform": "Generic", # "DrmType": "None", # Not required - "Platform": "Android", # Required field. } ) @@ -404,7 +406,7 @@ def get_metadata(book): "IsSocialEnabled": True, "Language": "en", "PhoneticPronunciations": {}, - "PublicationDate": "2019-02-03T00:25:03.0000000Z", # current_time(), + "PublicationDate": book.pubdate, "Publisher": {"Imprint": "", "Name": get_publisher(book),}, "RevisionId": book_uuid, "Title": book.title, @@ -488,9 +490,14 @@ def HandleInitRequest(): if "Resources" in store_response_json: kobo_resources = store_response_json["Resources"] - calibre_web_url=url_for("web.index", _external=True).strip("/") + calibre_web_url = url_for("web.index", _external=True).strip("/") kobo_resources["image_host"] = calibre_web_url - kobo_resources["image_url_quality_template"] = calibre_web_url + "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg" - kobo_resources["image_url_template"] = calibre_web_url + "/{ImageId}/{Width}/{Height}/false/image.jpg" + kobo_resources["image_url_quality_template"] = ( + calibre_web_url + + "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg" + ) + kobo_resources["image_url_template"] = ( + calibre_web_url + "/{ImageId}/{Width}/{Height}/false/image.jpg" + ) return make_response(store_response_json, store_response.status_code) From 9ec3ddd4928bcb7a82e9abd2c74c0710c7034d12 Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Sun, 22 Dec 2019 16:28:19 -0500 Subject: [PATCH 5/5] Fix the HandleCoverImage endpoint so that it requires login, and doesn't take unused parameters. --- cps/kobo.py | 23 ++++++++++++++--------- cps/kobo_auth.py | 11 +++++++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/cps/kobo.py b/cps/kobo.py index 6c67aae9..6acbc1d0 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -23,6 +23,10 @@ import uuid from base64 import b64decode, b64encode from datetime import datetime from time import gmtime, strftime +try: + from urllib import unquote +except ImportError: + from urllib.parse import unquote from jsonschema import validate, exceptions from flask import ( @@ -442,9 +446,10 @@ def reading_state(book): @kobo.route( - "//////image.jpg" + "//image.jpg" ) -def HandleCoverImageRequest(book_uuid, horizontal, vertical, jpeg_quality, monochrome): +@login_required +def HandleCoverImageRequest(book_uuid): book_cover = helper.get_book_cover_with_uuid( book_uuid, use_generic_cover_on_failure=False ) @@ -476,6 +481,7 @@ def handle_404(err): @kobo.route("/v1/initialization") +@login_required def HandleInitRequest(): outgoing_headers = Headers(request.headers) outgoing_headers.remove("Host") @@ -492,12 +498,11 @@ def HandleInitRequest(): calibre_web_url = url_for("web.index", _external=True).strip("/") kobo_resources["image_host"] = calibre_web_url - kobo_resources["image_url_quality_template"] = ( - calibre_web_url - + "/{ImageId}/{Width}/{Height}/{Quality}/{IsGreyscale}/image.jpg" - ) - kobo_resources["image_url_template"] = ( - calibre_web_url + "/{ImageId}/{Width}/{Height}/false/image.jpg" - ) + kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, + auth_token = kobo_auth.get_auth_token(), + book_uuid="{ImageId}")) + kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, + auth_token = kobo_auth.get_auth_token(), + book_uuid="{ImageId}")) return make_response(store_response_json, store_response.status_code) diff --git a/cps/kobo_auth.py b/cps/kobo_auth.py index 0b9eba6e..304e9eb2 100644 --- a/cps/kobo_auth.py +++ b/cps/kobo_auth.py @@ -81,10 +81,17 @@ def disable_failed_auth_redirect_for_blueprint(bp): lm.blueprint_login_views[bp.name] = None +def get_auth_token(): + if "auth_token" in g: + return g.get("auth_token") + else: + return None + + @lm.request_loader def load_user_from_kobo_request(request): - if "auth_token" in g: - auth_token = g.get("auth_token") + auth_token = get_auth_token() + if auth_token is not None: user = ( ub.session.query(ub.User) .join(ub.RemoteAuthToken)