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.
This commit is contained in:
		
							parent
							
								
									d6a9746824
								
							
						
					
					
						commit
						b831b9d6b2
					
				
							
								
								
									
										293
									
								
								cps/kobo.py
									
									
									
									
									
								
							
							
						
						
									
										293
									
								
								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/<auth_token>")
 | 
			
		||||
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/<dummy>")
 | 
			
		||||
@kobo.route("/v1/user/recommendations")
 | 
			
		||||
@kobo.route("/v1/products/<dummy>")
 | 
			
		||||
@kobo.route("/v1/products/<dummy>/nextread")
 | 
			
		||||
@kobo.route("/v1/products/featured/<dummy>")
 | 
			
		||||
@kobo.route("/v1/products/featured/")
 | 
			
		||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])  # TODO: implement
 | 
			
		||||
def HandleDummyRequest(dummy=None):
 | 
			
		||||
    return make_response(jsonify({}))
 | 
			
		||||
# TODO: Implement the following routes
 | 
			
		||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
 | 
			
		||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["PUT"])
 | 
			
		||||
@kobo.route("/v1/library/tags", methods=["POST"])
 | 
			
		||||
@kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
 | 
			
		||||
@kobo.route("/v1/library/tags/<tag_id>", 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user