fixes from tests
This commit is contained in:
parent
1c18a788f4
commit
1c630eb604
5 changed files with 175 additions and 192 deletions
10
cps/db.py
10
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()
|
||||
|
||||
|
@ -288,7 +288,7 @@ class Books(Base):
|
|||
|
||||
@property
|
||||
def atom_timestamp(self):
|
||||
return (self.timestamp or '').replace(' ', 'T')
|
||||
return (self.timestamp.strftime('%Y-%m-%dT%H:%M:%S+00:00') or '')
|
||||
|
||||
class Custom_Columns(Base):
|
||||
__tablename__ = 'custom_columns'
|
||||
|
@ -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()
|
||||
|
@ -428,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)
|
||||
|
|
325
cps/kobo.py
325
cps/kobo.py
|
@ -23,18 +23,32 @@ 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 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 :) .
|
||||
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/<auth_token>")
|
||||
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo)
|
||||
|
@ -55,6 +69,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 +193,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()
|
||||
|
||||
|
@ -167,6 +230,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.
|
||||
|
@ -174,7 +241,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:
|
||||
|
@ -198,13 +265,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 +310,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])
|
||||
|
@ -283,14 +377,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.
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -313,7 +410,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,
|
||||
|
@ -349,14 +446,15 @@ def reading_state(book):
|
|||
|
||||
|
||||
@kobo.route(
|
||||
"/<book_uuid>/<horizontal>/<vertical>/<jpeg_quality>/<monochrome>/image.jpg"
|
||||
"/<book_uuid>/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
|
||||
)
|
||||
if not book_cover:
|
||||
return make_response()
|
||||
return redirect(get_store_url_for_current_request(), 307)
|
||||
return book_cover
|
||||
|
||||
|
||||
|
@ -365,173 +463,46 @@ 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")
|
||||
@login_required
|
||||
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"] = 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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -276,7 +276,7 @@ def feed_languages(book_id):
|
|||
isoLanguages.get(part3=entry.languages[index].lang_code).name)'''
|
||||
return render_xml_template('feed.xml', entries=entries, pagination=pagination)
|
||||
|
||||
@opds.route("/opds/shelfindex/", defaults={'public': 0})
|
||||
@opds.route("/opds/shelfindex", defaults={'public': 0})
|
||||
@opds.route("/opds/shelfindex/<string:public>")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_shelfindex(public):
|
||||
|
@ -378,14 +378,14 @@ def render_xml_template(*args, **kwargs):
|
|||
def feed_get_cover(book_id):
|
||||
return get_book_cover(book_id)
|
||||
|
||||
@opds.route("/opds/readbooks/")
|
||||
@opds.route("/opds/readbooks")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_read_books():
|
||||
off = request.args.get("offset") or 0
|
||||
return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True)
|
||||
|
||||
|
||||
@opds.route("/opds/unreadbooks/")
|
||||
@opds.route("/opds/unreadbooks")
|
||||
@requires_basic_auth_if_no_ano
|
||||
def feed_unread_books():
|
||||
off = request.args.get("offset") or 0
|
||||
|
|
15
cps/web.py
15
cps/web.py
|
@ -43,7 +43,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, \
|
||||
|
@ -93,12 +93,12 @@ def error_http(error):
|
|||
|
||||
|
||||
def internal_error(error):
|
||||
__, __, tb = sys.exc_info()
|
||||
# __, __, tb = sys.exc_info()
|
||||
return render_template('http_error.html',
|
||||
error_code="Internal Server Error",
|
||||
error_name=str(error),
|
||||
issue=True,
|
||||
error_stack=traceback.format_tb(tb),
|
||||
error_stack=traceback.format_exc().split("\n"),
|
||||
instance=config.config_calibre_web_title
|
||||
), 500
|
||||
|
||||
|
@ -790,9 +790,7 @@ def get_tasks_status():
|
|||
|
||||
@app.route("/reconnect")
|
||||
def reconnect():
|
||||
db.session.close()
|
||||
db.engine.dispose()
|
||||
db.setup_db()
|
||||
db.reconnect_db(config)
|
||||
return json.dumps({})
|
||||
|
||||
@web.route("/search", methods=["GET"])
|
||||
|
@ -961,7 +959,7 @@ def advanced_search():
|
|||
series=series, title=_(u"search"), cc=cc, page="advsearch")
|
||||
|
||||
|
||||
def render_read_books(page, are_read, as_xml=False, order=None):
|
||||
def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs):
|
||||
order = order or []
|
||||
if not config.config_read_column:
|
||||
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\
|
||||
|
@ -984,7 +982,8 @@ def render_read_books(page, are_read, as_xml=False, order=None):
|
|||
entries, random, pagination = fill_indexpage(page, db.Books, db_filter, order)
|
||||
|
||||
if as_xml:
|
||||
xml = render_title_template('feed.xml', entries=entries, pagination=pagination)
|
||||
currtime = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%S+00:00")
|
||||
xml = render_template(current_time=currtime, instance=config.config_calibre_web_title, *args, **kwargs)
|
||||
response = make_response(xml)
|
||||
response.headers["Content-Type"] = "application/xml; charset=utf-8"
|
||||
return response
|
||||
|
|
Loading…
Reference in a new issue