Update Kobo

pull/1179/head
Ozzieisaacs 5 years ago
parent cd9bb56db5
commit 0955c6d6fb

@ -31,7 +31,7 @@ from flask import (
request, request,
make_response, make_response,
jsonify, jsonify,
json, current_app,
url_for, url_for,
redirect, redirect,
) )
@ -44,7 +44,7 @@ from . import config, logger, kobo_auth, db, helper
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
from .web import download_required 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_STOREAPI_URL = "https://storeapi.kobo.com"
kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>") kobo = Blueprint("kobo", __name__, url_prefix="/kobo/<auth_token>")
@ -74,6 +74,7 @@ def redirect_or_proxy_request():
if request.method == "GET": if request.method == "GET":
return redirect(get_store_url_for_current_request(), 307) return redirect(get_store_url_for_current_request(), 307)
if request.method == "DELETE": if request.method == "DELETE":
log.info('Delete Book')
return make_response(jsonify({})) return make_response(jsonify({}))
else: else:
# The Kobo device turns other request types into GET requests on redirects, so we instead proxy to the Kobo store ourselves. # 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)) .filter(db.Data.format.in_(KOBO_FORMATS))
.all() .all()
) )
for book in changed_entries: for book in changed_entries:
entitlement = { entitlement = {
"BookEntitlement": create_book_entitlement(book), "BookEntitlement": create_book_entitlement(book),
@ -144,30 +146,29 @@ def HandleSyncRequest():
sync_token.books_last_modified = new_books_last_modified sync_token.books_last_modified = new_books_last_modified
# Missing feature: Detect server-side book deletions. # Missing feature: Detect server-side book deletions.
return generate_sync_response(request, sync_token, entitlements) return generate_sync_response(request, sync_token, entitlements)
def 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. # We first merge in sync results from the official Kobo store.
outgoing_headers = Headers(request.headers) #outgoing_headers = Headers(request.headers)
outgoing_headers.remove("Host") #outgoing_headers.remove("Host")
sync_token.set_kobo_store_header(outgoing_headers) #sync_token.set_kobo_store_header(outgoing_headers)
store_response = requests.request( #store_response = requests.request(
method=request.method, # method=request.method,
url=get_store_url_for_current_request(), # url=get_store_url_for_current_request(),
headers=outgoing_headers, # headers=outgoing_headers,
data=request.get_data(), # data=request.get_data(),
) #)
store_entitlements = store_response.json() #store_entitlements = store_response.json()
entitlements += store_entitlements #entitlements += store_entitlements
sync_token.merge_from_store_response(store_response) #sync_token.merge_from_store_response(store_response)
response = make_response(jsonify(entitlements)) response = make_response(jsonify(entitlements))
# sync_token.to_headers(request.headers)
sync_token.to_headers(response.headers) # sync_token.to_headers(response.headers)
try: '''try:
# These headers could probably use some more investigation. # These headers could probably use some more investigation.
response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"] response.headers["x-kobo-sync"] = store_response.headers["x-kobo-sync"]
response.headers["x-kobo-sync-mode"] = store_response.headers[ 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" "x-kobo-recent-reads"
] ]
except KeyError: except KeyError:
pass pass'''
return response return response
@ -197,12 +198,22 @@ def HandleMetadataRequest(book_uuid):
def get_download_url_for_book(book, book_format): def get_download_url_for_book(book, book_format):
return url_for( if not current_app.wsgi_app.is_proxied:
"web.download_link", log.debug('Received unproxied request, changed request port to server port')
book_id=book.id, return "{url_scheme}://{url_base}:{url_port}/download/{book_id}/{book_format}".format(
book_format=book_format.lower(), url_scheme=request.environ['wsgi.url_scheme'],
_external=True, 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): def create_book_entitlement(book):
@ -255,11 +266,11 @@ def get_series(book):
def get_metadata(book): def get_metadata(book):
download_urls = [] download_urls = []
for book_data in book.data: for book_data in book.data:
if book_data.format not in KOBO_FORMATS: if book_data.format not in KOBO_FORMATS:
continue continue
for kobo_format in KOBO_FORMATS[book_data.format]: for kobo_format in KOBO_FORMATS[book_data.format]:
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
download_urls.append( download_urls.append(
{ {
"Format": kobo_format, "Format": kobo_format,
@ -349,17 +360,29 @@ def TopLevelEndpoint():
@kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"]) @kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"]) @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"])
def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None): def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None):
log.debug("Alternative Request received:")
return redirect_or_proxy_request() 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) @kobo.app_errorhandler(404)
def handle_404(err): def handle_404(err):
# This handler acts as a catch-all for endpoints that we don't have an interest in # 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) # implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
log.debug("Unknown Request received: %s", request.base_url)
return redirect_or_proxy_request() return redirect_or_proxy_request()
@kobo.route("/v1/initialization") '''@kobo.route("/v1/initialization")
@login_required @login_required
def HandleInitRequest(): def HandleInitRequest():
outgoing_headers = Headers(request.headers) outgoing_headers = Headers(request.headers)
@ -374,7 +397,6 @@ def HandleInitRequest():
store_response_json = store_response.json() store_response_json = store_response.json()
if "Resources" in store_response_json: if "Resources" in store_response_json:
kobo_resources = store_response_json["Resources"] 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_host"] = calibre_web_url
kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True, kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True,
@ -385,3 +407,143 @@ def HandleInitRequest():
book_uuid="{ImageId}")) book_uuid="{ImageId}"))
return make_response(store_response_json, store_response.status_code) 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",
}

@ -60,10 +60,13 @@ class ReverseProxied(object):
def __init__(self, application): def __init__(self, application):
self.app = application self.app = application
self.proxied = False
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
self.proxied = False
script_name = environ.get('HTTP_X_SCRIPT_NAME', '') script_name = environ.get('HTTP_X_SCRIPT_NAME', '')
if script_name: if script_name:
self.proxied = True
environ['SCRIPT_NAME'] = script_name environ['SCRIPT_NAME'] = script_name
path_info = environ.get('PATH_INFO', '') path_info = environ.get('PATH_INFO', '')
if path_info and path_info.startswith(script_name): if path_info and path_info.startswith(script_name):
@ -76,3 +79,7 @@ class ReverseProxied(object):
if servr: if servr:
environ['HTTP_HOST'] = servr environ['HTTP_HOST'] = servr
return self.app(environ, start_response) return self.app(environ, start_response)
@property
def is_proxied(self):
return self.proxied

@ -24,7 +24,7 @@ import signal
import socket import socket
try: try:
from gevent.pywsgi import WSGIServer from gevent.pylwsgi import WSGIServer
from gevent.pool import Pool from gevent.pool import Pool
from gevent import __version__ as _version from gevent import __version__ as _version
VERSION = 'Gevent ' + _version VERSION = 'Gevent ' + _version

@ -169,6 +169,18 @@
<input type="checkbox" id="config_remote_login" name="config_remote_login" {% if config.config_remote_login %}checked{% endif %}> <input type="checkbox" id="config_remote_login" name="config_remote_login" {% if config.config_remote_login %}checked{% endif %}>
<label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label> <label for="config_remote_login">{{_('Enable remote login ("magic link")')}}</label>
</div> </div>
{% if feature_support['kobo'] %}
<div class="form-group">
<input type="checkbox" id="config_kobo_sync" name="config_kobo_sync" data-control="kobo-settings" {% if config.config_kobo_sync %}checked{% endif %}>
<label for="config_kobo_sync">{{_('Enable Kobo sync')}}</label>
</div>
<div data-related="kobo-settings">
<div class="form-group">
<input type="checkbox" id="config_kobo_proxy" name="config_kobo_proxy" {% if config.config_kobo_proxy %}checked{% endif %}>
<label for="config_kobo_proxy">{{_('Proxy unknown requests to Kobo Store')}}</label>
</div>
</div>
{% endif %}
{% if feature_support['goodreads'] %} {% if feature_support['goodreads'] %}
<div class="form-group"> <div class="form-group">
<input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if config.config_use_goodreads %}checked{% endif %}> <input type="checkbox" id="config_use_goodreads" name="config_use_goodreads" data-control="goodreads-settings" {% if config.config_use_goodreads %}checked{% endif %}>

@ -464,7 +464,8 @@ def migrate_Database(session):
def clean_database(session): def clean_database(session):
# Remove expired remote login tokens # Remove expired remote login tokens
now = datetime.datetime.now() 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() session.commit()

Loading…
Cancel
Save