|
|
|
@ -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/<auth_token>")
|
|
|
|
@ -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/<shelf_name>", methods=["POST"])
|
|
|
|
|
@kobo.route("/v1/library/tags/<tag_id>", 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/<dummy>", 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/<dummy>", 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",
|
|
|
|
|
}
|
|
|
|
|