fixes from tests

This commit is contained in:
Ozzieisaacs 2019-12-30 15:16:09 +01:00
parent 1c18a788f4
commit 1c630eb604
5 changed files with 175 additions and 192 deletions

View File

@ -33,7 +33,7 @@ from sqlalchemy.ext.declarative import declarative_base
session = None session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series']
cc_classes = {} cc_classes = {}
engine = None
Base = declarative_base() Base = declarative_base()
@ -288,7 +288,7 @@ class Books(Base):
@property @property
def atom_timestamp(self): 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): class Custom_Columns(Base):
__tablename__ = 'custom_columns' __tablename__ = 'custom_columns'
@ -327,6 +327,7 @@ def update_title_sort(config, conn=None):
def setup_db(config): def setup_db(config):
dispose() dispose()
global engine
if not config.config_calibre_dir: if not config.config_calibre_dir:
config.invalidate() config.invalidate()
@ -428,3 +429,8 @@ def dispose():
if name.startswith("custom_column_") or name.startswith("books_custom_column_"): if name.startswith("custom_column_") or name.startswith("books_custom_column_"):
if table is not None: if table is not None:
Base.metadata.remove(table) Base.metadata.remove(table)
def reconnect_db(config):
session.close()
engine.dispose()
setup_db(config)

View File

@ -23,18 +23,32 @@ import uuid
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from datetime import datetime from datetime import datetime
from time import gmtime, strftime from time import gmtime, strftime
try:
from urllib import unquote
except ImportError:
from urllib.parse import unquote
from jsonschema import validate, exceptions 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 flask_login import login_required
from werkzeug.datastructures import Headers
from sqlalchemy import func from sqlalchemy import func
import requests
from . import config, logger, kobo_auth, db, helper from . import config, logger, kobo_auth, db, helper
from .web import download_required from .web import download_required
#TODO: Test more formats :) . KOBO_FORMATS = {"KEPUB": ["KEPUB"], "EPUB": ["EPUB", "EPUB3"]}
KOBO_SUPPORTED_FORMATS = {"KEPUB"} 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>")
kobo_auth.disable_failed_auth_redirect_for_blueprint(kobo) 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() 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: class SyncToken:
""" The SyncToken is used to persist state accross requests. """ 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. 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, 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): def to_headers(self, headers):
headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token() headers[SyncToken.SYNC_TOKEN_HEADER] = self.build_sync_token()
@ -167,6 +230,10 @@ def HandleSyncRequest():
new_books_last_created = sync_token.books_last_created new_books_last_created = sync_token.books_last_created
entitlements = [] 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. # 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 # 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. # the comparison because of the +00:00 suffix.
@ -174,7 +241,7 @@ def HandleSyncRequest():
db.session.query(db.Books) db.session.query(db.Books)
.join(db.Data) .join(db.Data)
.filter(func.datetime(db.Books.last_modified) > sync_token.books_last_modified) .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() .all()
) )
for book in changed_entries: for book in changed_entries:
@ -198,13 +265,40 @@ def HandleSyncRequest():
# Missing feature: Detect server-side book deletions. # 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)) response = make_response(jsonify(entitlements))
sync_token.to_headers(response.headers) sync_token.to_headers(response.headers)
response.headers["x-kobo-sync-mode"] = "delta" try:
response.headers["x-kobo-apitoken"] = "e30=" # 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 return response
@ -216,7 +310,7 @@ def HandleMetadataRequest(book_uuid):
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
if not book or not book.data: if not book or not book.data:
log.info(u"Book %s not found in database", book_uuid) 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) metadata = get_metadata(book)
return jsonify([metadata]) return jsonify([metadata])
@ -283,14 +377,17 @@ def get_metadata(book):
download_urls = [] download_urls = []
for book_data in book.data: 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( download_urls.append(
{ {
"Format": book_data.format, "Format": kobo_format,
"Size": book_data.uncompressed_size, "Size": book_data.uncompressed_size,
"Url": get_download_url_for_book(book, book_data.format), "Url": get_download_url_for_book(book, book_data.format),
# The Kobo forma accepts platforms: (Generic, Android)
"Platform": "Generic",
# "DrmType": "None", # Not required # "DrmType": "None", # Not required
"Platform": "Android", # Required field.
} }
) )
@ -313,7 +410,7 @@ def get_metadata(book):
"IsSocialEnabled": True, "IsSocialEnabled": True,
"Language": "en", "Language": "en",
"PhoneticPronunciations": {}, "PhoneticPronunciations": {},
"PublicationDate": "2019-02-03T00:25:03.0000000Z", # current_time(), "PublicationDate": book.pubdate,
"Publisher": {"Imprint": "", "Name": get_publisher(book),}, "Publisher": {"Imprint": "", "Name": get_publisher(book),},
"RevisionId": book_uuid, "RevisionId": book_uuid,
"Title": book.title, "Title": book.title,
@ -349,14 +446,15 @@ def reading_state(book):
@kobo.route( @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_cover = helper.get_book_cover_with_uuid(
book_uuid, use_generic_cover_on_failure=False book_uuid, use_generic_cover_on_failure=False
) )
if not book_cover: if not book_cover:
return make_response() return redirect(get_store_url_for_current_request(), 307)
return book_cover return book_cover
@ -365,173 +463,46 @@ def TopLevelEndpoint():
return make_response(jsonify({})) return make_response(jsonify({}))
@kobo.route("/v1/user/profile") # TODO: Implement the following routes
@kobo.route("/v1/user/loyalty/benefits") @kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
@kobo.route("/v1/analytics/gettests/", methods=["GET", "POST"]) @kobo.route("/v1/library/<book_uuid>/state", methods=["PUT"])
@kobo.route("/v1/user/wishlist") @kobo.route("/v1/library/tags", methods=["POST"])
@kobo.route("/v1/user/<dummy>") @kobo.route("/v1/library/tags/<shelf_name>", methods=["POST"])
@kobo.route("/v1/user/recommendations") @kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE"])
@kobo.route("/v1/products/<dummy>") def HandleUnimplementedRequest(book_uuid=None, shelf_name=None, tag_id=None):
@kobo.route("/v1/products/<dummy>/nextread") return redirect_or_proxy_request()
@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({}))
@kobo.route("/v1/auth/device", methods=["POST"]) @kobo.app_errorhandler(404)
def HandleAuthRequest(): def handle_404(err):
# This AuthRequest isn't used for most of our usecases. # This handler acts as a catch-all for endpoints that we don't have an interest in
response = make_response( # implementing (e.g: v1/analytics/gettests, v1/user/recommendations, etc)
jsonify( return redirect_or_proxy_request()
{
"AccessToken": "abcde",
"RefreshToken": "abcde",
"TokenType": "Bearer",
"TrackingId": "abcde",
"UserKey": "abcdefgeh",
}
)
)
return response
@kobo.route("/v1/initialization") @kobo.route("/v1/initialization")
@login_required
def HandleInitRequest(): def HandleInitRequest():
resources = NATIVE_KOBO_RESOURCES( outgoing_headers = Headers(request.headers)
calibre_web_url=url_for("web.index", _external=True).strip("/") 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): calibre_web_url = url_for("web.index", _external=True).strip("/")
return { kobo_resources["image_host"] = calibre_web_url
"account_page": "https://secure.kobobooks.com/profile", kobo_resources["image_url_quality_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True,
"account_page_rakuten": "https://my.rakuten.co.jp/", auth_token = kobo_auth.get_auth_token(),
"add_entitlement": "https://storeapi.kobo.com/v1/library/{RevisionIds}", book_uuid="{ImageId}"))
"affiliaterequest": "https://storeapi.kobo.com/v1/affiliate", kobo_resources["image_url_template"] = unquote(url_for("kobo.HandleCoverImageRequest", _external=True,
"audiobook_subscription_orange_deal_inclusion_url": "https://authorize.kobo.com/inclusion", auth_token = kobo_auth.get_auth_token(),
"authorproduct_recommendations": "https://storeapi.kobo.com/v1/products/books/authors/recommendations", book_uuid="{ImageId}"))
"autocomplete": "https://storeapi.kobo.com/v1/products/autocomplete",
"blackstone_header": {"key": "x-amz-request-payer", "value": "requester"}, return make_response(store_response_json, store_response.status_code)
"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",
}

View File

@ -81,10 +81,17 @@ def disable_failed_auth_redirect_for_blueprint(bp):
lm.blueprint_login_views[bp.name] = None 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 @lm.request_loader
def load_user_from_kobo_request(request): def load_user_from_kobo_request(request):
if "auth_token" in g: auth_token = get_auth_token()
auth_token = g.get("auth_token") if auth_token is not None:
user = ( user = (
ub.session.query(ub.User) ub.session.query(ub.User)
.join(ub.RemoteAuthToken) .join(ub.RemoteAuthToken)

View File

@ -276,7 +276,7 @@ def feed_languages(book_id):
isoLanguages.get(part3=entry.languages[index].lang_code).name)''' isoLanguages.get(part3=entry.languages[index].lang_code).name)'''
return render_xml_template('feed.xml', entries=entries, pagination=pagination) 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>") @opds.route("/opds/shelfindex/<string:public>")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_shelfindex(public): def feed_shelfindex(public):
@ -378,14 +378,14 @@ def render_xml_template(*args, **kwargs):
def feed_get_cover(book_id): def feed_get_cover(book_id):
return get_book_cover(book_id) return get_book_cover(book_id)
@opds.route("/opds/readbooks/") @opds.route("/opds/readbooks")
@requires_basic_auth_if_no_ano @requires_basic_auth_if_no_ano
def feed_read_books(): def feed_read_books():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0
return render_read_books(int(off) / (int(config.config_books_per_page)) + 1, True, True) 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 @requires_basic_auth_if_no_ano
def feed_unread_books(): def feed_unread_books():
off = request.args.get("offset") or 0 off = request.args.get("offset") or 0

View File

@ -43,7 +43,7 @@ from werkzeug.exceptions import default_exceptions
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers
from werkzeug.security import generate_password_hash, check_password_hash 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 . import searched_ids, lm, babel, db, ub, config, get_locale, app
from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download from .gdriveutils import getFileFromEbooksFolder, do_gdrive_download
from .helper import common_filters, get_search_results, fill_indexpage, speaking_language, check_valid_domain, \ 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): def internal_error(error):
__, __, tb = sys.exc_info() # __, __, tb = sys.exc_info()
return render_template('http_error.html', return render_template('http_error.html',
error_code="Internal Server Error", error_code="Internal Server Error",
error_name=str(error), error_name=str(error),
issue=True, issue=True,
error_stack=traceback.format_tb(tb), error_stack=traceback.format_exc().split("\n"),
instance=config.config_calibre_web_title instance=config.config_calibre_web_title
), 500 ), 500
@ -790,9 +790,7 @@ def get_tasks_status():
@app.route("/reconnect") @app.route("/reconnect")
def reconnect(): def reconnect():
db.session.close() db.reconnect_db(config)
db.engine.dispose()
db.setup_db()
return json.dumps({}) return json.dumps({})
@web.route("/search", methods=["GET"]) @web.route("/search", methods=["GET"])
@ -961,7 +959,7 @@ def advanced_search():
series=series, title=_(u"search"), cc=cc, page="advsearch") 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 [] order = order or []
if not config.config_read_column: if not config.config_read_column:
readBooks = ub.session.query(ub.ReadBook).filter(ub.ReadBook.user_id == int(current_user.id))\ 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) entries, random, pagination = fill_indexpage(page, db.Books, db_filter, order)
if as_xml: 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 = make_response(xml)
response.headers["Content-Type"] = "application/xml; charset=utf-8" response.headers["Content-Type"] = "application/xml; charset=utf-8"
return response return response