From 0955c6d6fb1d835799a2da419045c4e374f58c86 Mon Sep 17 00:00:00 2001 From: Ozzieisaacs Date: Sun, 26 Jan 2020 14:42:53 +0100 Subject: [PATCH] Update Kobo --- cps/kobo.py | 220 ++++++++++++++++++++++++++++----- cps/reverseproxy.py | 7 ++ cps/server.py | 2 +- cps/templates/config_edit.html | 12 ++ cps/ub.py | 3 +- 5 files changed, 213 insertions(+), 31 deletions(-) diff --git a/cps/kobo.py b/cps/kobo.py index 7e1cbc8e..6a2969c2 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -31,7 +31,7 @@ from flask import ( request, make_response, jsonify, - json, + current_app, url_for, redirect, ) @@ -44,7 +44,7 @@ from . import config, logger, kobo_auth, db, helper from .services import SyncToken as SyncToken from .web import download_required -KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]} +KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["KEPUB"]} KOBO_STOREAPI_URL = "https://storeapi.kobo.com" kobo = Blueprint("kobo", __name__, url_prefix="/kobo/") @@ -74,6 +74,7 @@ def redirect_or_proxy_request(): if request.method == "GET": return redirect(get_store_url_for_current_request(), 307) if request.method == "DELETE": + log.info('Delete Book') return make_response(jsonify({})) else: # The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves. @@ -124,6 +125,7 @@ def HandleSyncRequest(): .filter(db.Data.format.in_(KOBO_FORMATS)) .all() ) + for book in changed_entries: entitlement = { "BookEntitlement": create_book_entitlement(book), @@ -144,30 +146,29 @@ def HandleSyncRequest(): sync_token.books_last_modified = new_books_last_modified # Missing feature: Detect server-side book deletions. - 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) + #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) - try: + # sync_token.to_headers(request.headers) + # sync_token.to_headers(response.headers) + '''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[ @@ -177,7 +178,7 @@ def generate_sync_response(request, sync_token, entitlements): "x-kobo-recent-reads" ] except KeyError: - pass + pass''' return response @@ -197,12 +198,22 @@ def HandleMetadataRequest(book_uuid): def get_download_url_for_book(book, book_format): - return url_for( - "web.download_link", - book_id=book.id, - book_format=book_format.lower(), - _external=True, - ) + if not current_app.wsgi_app.is_proxied: + log.debug('Received unproxied request, changed request port to server port') + return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format( + url_scheme=request.environ['wsgi.url_scheme'], + url_base=request.environ['SERVER_NAME'], + url_port=config.config_port, + book_id=book.id, + book_format=book_format.lower() + ) + else: + return url_for( + "web.download_link", + book_id=book.id, + book_format=book_format.lower(), + _external=True, + ) def create_book_entitlement(book): @@ -255,11 +266,11 @@ def get_series(book): def get_metadata(book): download_urls = [] - for book_data in book.data: if book_data.format not in KOBO_FORMATS: continue for kobo_format in KOBO_FORMATS[book_data.format]: + # log.debug('Id: %s, Format: %s' % (book.id, kobo_format)) download_urls.append( { "Format": kobo_format, @@ -349,17 +360,29 @@ def TopLevelEndpoint(): @kobo.route("/v1/library/tags/", methods=["POST"]) @kobo.route("/v1/library/tags/", methods=["DELETE"]) def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None): + log.debug("Alternative Request received:") return redirect_or_proxy_request() +# TODO: Implement the following routes +@kobo.route("/v1/user/loyalty/", methods=["GET", "POST"]) +@kobo.route("/v1/user/profile", methods=["GET", "POST"]) +@kobo.route("/v1/user/wishlist", methods=["GET", "POST"]) +@kobo.route("/v1/user/recommendations", methods=["GET", "POST"]) +@kobo.route("/v1/analytics/", methods=["GET", "POST"]) +def HandleUserRequest(dummy=None): + log.debug("Unimplemented Request received: %s", request.base_url) + return make_response(jsonify({})) + # return redirect_or_proxy_request() @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) + log.debug("Unknown Request received: %s", request.base_url) return redirect_or_proxy_request() -@kobo.route("/v1/initialization") +'''@kobo.route("/v1/initialization") @login_required def HandleInitRequest(): outgoing_headers = Headers(request.headers) @@ -374,7 +397,6 @@ def HandleInitRequest(): store_response_json = store_response.json() if "Resources" in store_response_json: kobo_resources = store_response_json["Resources"] - calibre_web_url = url_for("web.index", _external=True).strip("/") kobo_resources["image_host"] = calibre_web_url kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, @@ -385,3 +407,143 @@ def HandleInitRequest(): book_uuid="{ImageId}")) return make_response(store_response_json, store_response.status_code) +''' + +@kobo.route("/v1/initialization") +def HandleInitRequest(): + resources = NATIVE_KOBO_RESOURCES( + calibre_web_url=url_for("web.index", _external=True).strip("/") + ) + response = make_response(jsonify({"Resources": resources})) + response.headers["x-kobo-apitoken"] = "e30=" + return response + + +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", + } diff --git a/cps/reverseproxy.py b/cps/reverseproxy.py index 25bbe77b..30af5dbf 100644 --- a/cps/reverseproxy.py +++ b/cps/reverseproxy.py @@ -60,10 +60,13 @@ class ReverseProxied(object): def __init__(self, application): self.app = application + self.proxied = False def __call__(self, environ, start_response): + self.proxied = False script_name = environ.get('HTTP_X_SCRIPT_NAME', '') if script_name: + self.proxied = True environ['SCRIPT_NAME'] = script_name path_info = environ.get('PATH_INFO', '') if path_info and path_info.startswith(script_name): @@ -76,3 +79,7 @@ class ReverseProxied(object): if servr: environ['HTTP_HOST'] = servr return self.app(environ, start_response) + + @property + def is_proxied(self): + return self.proxied diff --git a/cps/server.py b/cps/server.py index 43792ecd..15197591 100755 --- a/cps/server.py +++ b/cps/server.py @@ -24,7 +24,7 @@ import signal import socket try: - from gevent.pywsgi import WSGIServer + from gevent.pylwsgi import WSGIServer from gevent.pool import Pool from gevent import __version__ as _version VERSION = 'Gevent ' + _version diff --git a/cps/templates/config_edit.html b/cps/templates/config_edit.html index b0ad49f7..ae923418 100644 --- a/cps/templates/config_edit.html +++ b/cps/templates/config_edit.html @@ -169,6 +169,18 @@ + {% if feature_support['kobo'] %} +
+ + +
+
+
+ + +
+
+ {% endif %} {% if feature_support['goodreads'] %}
diff --git a/cps/ub.py b/cps/ub.py index 8564ef21..638eed8f 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -464,7 +464,8 @@ def migrate_Database(session): def clean_database(session): # Remove expired remote login tokens now = datetime.datetime.now() - session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).delete() + session.query(RemoteAuthToken).filter(now > RemoteAuthToken.expiration).\ + filter(RemoteAuthToken.token_type !=1 ).delete() session.commit()