mirror of
https://github.com/janeczku/calibre-web
synced 2024-11-17 23:34:53 +00:00
[Kobo] Add Shelf/Collection support.
Implements the v1/library/tags set of endpoints to sync CalibreWeb shelves with the Kobo device. Drive-by: Refactors shelf.py to consolidate how user permissions are checked. Drive-by: Fix issue with the sync endpoint that arrises when a book is hard-deleted.
This commit is contained in:
parent
3e1c34efe6
commit
41a3623fcc
242
cps/kobo.py
242
cps/kobo.py
@ -17,9 +17,11 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import datetime
|
|
||||||
import sys
|
|
||||||
import base64
|
import base64
|
||||||
|
import datetime
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import uuid
|
import uuid
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
@ -45,7 +47,7 @@ from sqlalchemy import func
|
|||||||
from sqlalchemy.sql.expression import and_, or_
|
from sqlalchemy.sql.expression import and_, or_
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from . import config, logger, kobo_auth, db, helper, ub
|
from . import config, logger, kobo_auth, db, helper, shelf as shelf_lib, ub
|
||||||
from .services import SyncToken as SyncToken
|
from .services import SyncToken as SyncToken
|
||||||
from .web import download_required
|
from .web import download_required
|
||||||
from .kobo_auth import requires_kobo_auth
|
from .kobo_auth import requires_kobo_auth
|
||||||
@ -120,6 +122,7 @@ def redirect_or_proxy_request():
|
|||||||
def convert_to_kobo_timestamp_string(timestamp):
|
def convert_to_kobo_timestamp_string(timestamp):
|
||||||
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
return timestamp.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
|
||||||
@kobo.route("/v1/library/sync")
|
@kobo.route("/v1/library/sync")
|
||||||
@requires_kobo_auth
|
@requires_kobo_auth
|
||||||
@download_required
|
@download_required
|
||||||
@ -203,7 +206,8 @@ def HandleSyncRequest():
|
|||||||
ub.KoboReadingState.user_id == current_user.id,
|
ub.KoboReadingState.user_id == current_user.id,
|
||||||
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
|
ub.KoboReadingState.book_id.notin_(reading_states_in_new_entitlements))))
|
||||||
for kobo_reading_state in changed_reading_states.all():
|
for kobo_reading_state in changed_reading_states.all():
|
||||||
book = db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one()
|
book = db.session.query(db.Books).filter(db.Books.id == kobo_reading_state.book_id).one_or_none()
|
||||||
|
if book:
|
||||||
sync_results.append({
|
sync_results.append({
|
||||||
"ChangedReadingState": {
|
"ChangedReadingState": {
|
||||||
"ReadingState": get_kobo_reading_state_response(book, kobo_reading_state)
|
"ReadingState": get_kobo_reading_state_response(book, kobo_reading_state)
|
||||||
@ -211,17 +215,15 @@ def HandleSyncRequest():
|
|||||||
})
|
})
|
||||||
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
new_reading_state_last_modified = max(new_reading_state_last_modified, kobo_reading_state.last_modified)
|
||||||
|
|
||||||
|
sync_shelves(sync_token, sync_results)
|
||||||
|
|
||||||
sync_token.books_last_created = new_books_last_created
|
sync_token.books_last_created = new_books_last_created
|
||||||
sync_token.books_last_modified = new_books_last_modified
|
sync_token.books_last_modified = new_books_last_modified
|
||||||
sync_token.archive_last_modified = new_archived_last_modified
|
sync_token.archive_last_modified = new_archived_last_modified
|
||||||
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
sync_token.reading_state_last_modified = new_reading_state_last_modified
|
||||||
|
|
||||||
if config.config_kobo_proxy:
|
|
||||||
return generate_sync_response(request, sync_token, sync_results)
|
return generate_sync_response(request, sync_token, sync_results)
|
||||||
|
|
||||||
return make_response(jsonify(sync_results))
|
|
||||||
# Missing feature: Detect server-side book deletions.
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sync_response(request, sync_token, sync_results):
|
def generate_sync_response(request, sync_token, sync_results):
|
||||||
extra_headers = {}
|
extra_headers = {}
|
||||||
@ -392,6 +394,222 @@ def get_metadata(book):
|
|||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/tags", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
# Creates a Shelf with the given items, and returns the shelf's uuid.
|
||||||
|
def HandleTagCreate():
|
||||||
|
shelf_request = request.json
|
||||||
|
name, items = None, None
|
||||||
|
try:
|
||||||
|
name = shelf_request["Name"]
|
||||||
|
items = shelf_request["Items"]
|
||||||
|
except KeyError:
|
||||||
|
log.debug("Received malformed v1/library/tags request.")
|
||||||
|
abort(400, description="Malformed tags POST request. Data is missing 'Name' or 'Items' field")
|
||||||
|
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.name) == name, ub.Shelf.user_id ==
|
||||||
|
current_user.id).one_or_none()
|
||||||
|
if shelf and not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||||
|
abort(401, description="User is unauthaurized to edit shelf.")
|
||||||
|
|
||||||
|
if not shelf:
|
||||||
|
shelf = ub.Shelf(user_id=current_user.id, name=name, uuid=uuid.uuid4())
|
||||||
|
ub.session.add(shelf)
|
||||||
|
|
||||||
|
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||||
|
if items_unknown_to_calibre:
|
||||||
|
log.debug("Received request to add unknown books to a collection. Silently ignoring items.")
|
||||||
|
ub.session.commit()
|
||||||
|
|
||||||
|
return make_response(jsonify(str(shelf.uuid)), 201)
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/tags/<tag_id>", methods=["DELETE", "PUT"])
|
||||||
|
def HandleTagUpdate(tag_id):
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.uuid) == tag_id,
|
||||||
|
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||||
|
if not shelf:
|
||||||
|
log.debug("Received Kobo tag update request on a collection unknown to CalibreWeb")
|
||||||
|
if config.config_kobo_proxy:
|
||||||
|
return redirect_or_proxy_request()
|
||||||
|
else:
|
||||||
|
abort(404, description="Collection isn't known to CalibreWeb")
|
||||||
|
|
||||||
|
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||||
|
abort(401, description="User is unauthaurized to edit shelf.")
|
||||||
|
|
||||||
|
if request.method == "DELETE":
|
||||||
|
shelf_lib.delete_shelf_helper(shelf)
|
||||||
|
else:
|
||||||
|
shelf_request = request.json
|
||||||
|
name = None
|
||||||
|
try:
|
||||||
|
name = shelf_request["Name"]
|
||||||
|
except KeyError:
|
||||||
|
log.debug("Received malformed v1/library/tags rename request.")
|
||||||
|
abort(400, description="Malformed tags POST request. Data is missing 'Name' field")
|
||||||
|
|
||||||
|
shelf.name = name
|
||||||
|
ub.session.merge(shelf)
|
||||||
|
ub.session.commit()
|
||||||
|
|
||||||
|
return make_response(' ', 200)
|
||||||
|
|
||||||
|
|
||||||
|
# Adds items to the given shelf.
|
||||||
|
def add_items_to_shelf(items, shelf):
|
||||||
|
book_ids_already_in_shelf = set([book_shelf.book_id for book_shelf in shelf.books])
|
||||||
|
items_unknown_to_calibre = []
|
||||||
|
for item in items:
|
||||||
|
if item["Type"] != "ProductRevisionTagItem":
|
||||||
|
items_unknown_to_calibre.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.uuid == item["RevisionId"]).one_or_none()
|
||||||
|
if not book:
|
||||||
|
items_unknown_to_calibre.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
book_id = book.id
|
||||||
|
if book_id not in book_ids_already_in_shelf:
|
||||||
|
shelf.books.append(ub.BookShelf(book_id=book_id))
|
||||||
|
return items_unknown_to_calibre
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/tags/<tag_id>/items", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def HandleTagAddItem(tag_id):
|
||||||
|
tag_request = request.json
|
||||||
|
items = None
|
||||||
|
try:
|
||||||
|
items = tag_request["Items"]
|
||||||
|
except KeyError:
|
||||||
|
log.debug("Received malformed v1/library/tags/<tag_id>/items/delete request.")
|
||||||
|
abort(400, description="Malformed tags POST request. Data is missing 'Items' field")
|
||||||
|
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(and_(ub.Shelf.uuid) == tag_id,
|
||||||
|
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||||
|
if not shelf:
|
||||||
|
log.debug("Received Kobo request on a collection unknown to CalibreWeb")
|
||||||
|
abort(404, description="Collection isn't known to CalibreWeb")
|
||||||
|
|
||||||
|
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||||
|
abort(401, description="User is unauthaurized to edit shelf.")
|
||||||
|
|
||||||
|
items_unknown_to_calibre = add_items_to_shelf(items, shelf)
|
||||||
|
if items_unknown_to_calibre:
|
||||||
|
log.debug("Received request to add an unknown book to a collecition. Silently ignoring item.")
|
||||||
|
|
||||||
|
ub.session.merge(shelf)
|
||||||
|
ub.session.commit()
|
||||||
|
|
||||||
|
return make_response('', 201)
|
||||||
|
|
||||||
|
|
||||||
|
@kobo.route("/v1/library/tags/<tag_id>/items/delete", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def HandleTagRemoveItem(tag_id):
|
||||||
|
tag_request = request.json
|
||||||
|
items = None
|
||||||
|
try:
|
||||||
|
items = tag_request["Items"]
|
||||||
|
except KeyError:
|
||||||
|
log.debug("Received malformed v1/library/tags/<tag_id>/items/delete request.")
|
||||||
|
abort(400, description="Malformed tags POST request. Data is missing 'Items' field")
|
||||||
|
|
||||||
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.uuid == tag_id,
|
||||||
|
ub.Shelf.user_id == current_user.id).one_or_none()
|
||||||
|
if not shelf:
|
||||||
|
log.debug(
|
||||||
|
"Received a request to remove an item from a Collection unknown to CalibreWeb.")
|
||||||
|
abort(404, description="Collection isn't known to CalibreWeb")
|
||||||
|
|
||||||
|
if not shelf_lib.check_shelf_edit_permissions(shelf):
|
||||||
|
abort(401, description="User is unauthaurized to edit shelf.")
|
||||||
|
|
||||||
|
items_unknown_to_calibre = []
|
||||||
|
for item in items:
|
||||||
|
if item["Type"] != "ProductRevisionTagItem":
|
||||||
|
items_unknown_to_calibre.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.uuid == item["RevisionId"]).one_or_none()
|
||||||
|
if not book:
|
||||||
|
items_unknown_to_calibre.append(item)
|
||||||
|
continue
|
||||||
|
|
||||||
|
shelf.books.filter(ub.BookShelf.book_id == book.id).delete()
|
||||||
|
ub.session.commit()
|
||||||
|
|
||||||
|
if items_unknown_to_calibre:
|
||||||
|
log.debug("Received request to remove an unknown book to a collecition. Silently ignoring item.")
|
||||||
|
|
||||||
|
return make_response('', 200)
|
||||||
|
|
||||||
|
|
||||||
|
# Add new, changed, or deleted shelves to the sync_results.
|
||||||
|
# Note: Public shelves that aren't owned by the user aren't supported.
|
||||||
|
def sync_shelves(sync_token, sync_results):
|
||||||
|
new_tags_last_modified = sync_token.tags_last_modified
|
||||||
|
|
||||||
|
for shelf in ub.session.query(ub.ShelfArchive).filter(func.datetime(ub.ShelfArchive.last_modified) > sync_token.tags_last_modified, ub.ShelfArchive.user_id == current_user.id):
|
||||||
|
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||||
|
|
||||||
|
sync_results.append({
|
||||||
|
"DeletedTag": {
|
||||||
|
"Tag": {
|
||||||
|
"Id": shelf.uuid,
|
||||||
|
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for shelf in ub.session.query(ub.Shelf).filter(func.datetime(ub.Shelf.last_modified) > sync_token.tags_last_modified, ub.Shelf.user_id == current_user.id):
|
||||||
|
if not shelf_lib.check_shelf_view_permissions(shelf):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_tags_last_modified = max(shelf.last_modified, new_tags_last_modified)
|
||||||
|
|
||||||
|
tag = create_kobo_tag(shelf)
|
||||||
|
if not tag:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if shelf.created > sync_token.tags_last_modified:
|
||||||
|
sync_results.append({
|
||||||
|
"NewTag": tag
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
sync_results.append({
|
||||||
|
"ChangedTag": tag
|
||||||
|
})
|
||||||
|
sync_token.tags_last_modified = new_tags_last_modified
|
||||||
|
ub.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
# Creates a Kobo "Tag" object from a ub.Shelf object
|
||||||
|
def create_kobo_tag(shelf):
|
||||||
|
tag = {
|
||||||
|
"Created": convert_to_kobo_timestamp_string(shelf.created),
|
||||||
|
"Id": shelf.uuid,
|
||||||
|
"Items": [],
|
||||||
|
"LastModified": convert_to_kobo_timestamp_string(shelf.last_modified),
|
||||||
|
"Name": shelf.name,
|
||||||
|
"Type": "UserTag"
|
||||||
|
}
|
||||||
|
for book_shelf in shelf.books:
|
||||||
|
book = db.session.query(db.Books).filter(db.Books.id == book_shelf.book_id).one_or_none()
|
||||||
|
if not book:
|
||||||
|
log.info(u"Book (id: %s) in BookShelf (id: %s) not found in book database", book_shelf.book_id, shelf.id)
|
||||||
|
return None
|
||||||
|
tag["Items"].append(
|
||||||
|
{
|
||||||
|
"RevisionId": book.uuid,
|
||||||
|
"Type": "ProductRevisionTagItem"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"Tag": tag}
|
||||||
|
|
||||||
|
|
||||||
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
@kobo.route("/v1/library/<book_uuid>/state", methods=["GET", "PUT"])
|
||||||
@login_required
|
@login_required
|
||||||
def HandleStateRequest(book_uuid):
|
def HandleStateRequest(book_uuid):
|
||||||
@ -589,10 +807,7 @@ def HandleBookDeletionRequest(book_uuid):
|
|||||||
|
|
||||||
# TODO: Implement the following routes
|
# TODO: Implement the following routes
|
||||||
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
@kobo.route("/v1/library/<dummy>", methods=["DELETE", "GET"])
|
||||||
@kobo.route("/v1/library/tags", methods=["POST"])
|
def HandleUnimplementedRequest(dummy=None):
|
||||||
@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("Unimplemented Library Request received: %s", request.base_url)
|
log.debug("Unimplemented Library Request received: %s", request.base_url)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
@ -612,6 +827,7 @@ def HandleUserRequest(dummy=None):
|
|||||||
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/recommendations", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/nextread", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
@kobo.route("/v1/products/<dummy>/reviews", methods=["GET", "POST"])
|
||||||
|
@kobo.route("/v1/products/books/series/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
@kobo.route("/v1/products/books/<dummy>", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
@kobo.route("/v1/products/dailydeal", methods=["GET", "POST"])
|
||||||
@kobo.route("/v1/products", methods=["GET", "POST"])
|
@kobo.route("/v1/products", methods=["GET", "POST"])
|
||||||
@ -624,7 +840,7 @@ def HandleProductsRequest(dummy=None):
|
|||||||
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)
|
log.debug("Unknown Request received: %s, method: %s, data: %s", request.base_url, request.method, request.data)
|
||||||
return redirect_or_proxy_request()
|
return redirect_or_proxy_request()
|
||||||
|
|
||||||
|
|
||||||
|
@ -326,12 +326,12 @@ def feed_shelfindex():
|
|||||||
def feed_shelf(book_id):
|
def feed_shelf(book_id):
|
||||||
off = request.args.get("offset") or 0
|
off = request.args.get("offset") or 0
|
||||||
if current_user.is_anonymous:
|
if current_user.is_anonymous:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == book_id, not ub.Shelf.deleted).first()
|
||||||
else:
|
else:
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
||||||
ub.Shelf.id == book_id),
|
ub.Shelf.id == book_id),
|
||||||
and_(ub.Shelf.is_public == 1,
|
and_(ub.Shelf.is_public == 1,
|
||||||
ub.Shelf.id == book_id))).first()
|
ub.Shelf.id == book_id)), not ub.Shelf.deleted).first()
|
||||||
result = list()
|
result = list()
|
||||||
# user is allowed to access shelf
|
# user is allowed to access shelf
|
||||||
if shelf:
|
if shelf:
|
||||||
|
@ -78,6 +78,7 @@ class SyncToken():
|
|||||||
"books_last_created": {"type": "string"},
|
"books_last_created": {"type": "string"},
|
||||||
"archive_last_modified": {"type": "string"},
|
"archive_last_modified": {"type": "string"},
|
||||||
"reading_state_last_modified": {"type": "string"},
|
"reading_state_last_modified": {"type": "string"},
|
||||||
|
"tags_last_modified": {"type": "string"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,13 +89,14 @@ class SyncToken():
|
|||||||
books_last_modified=datetime.min,
|
books_last_modified=datetime.min,
|
||||||
archive_last_modified=datetime.min,
|
archive_last_modified=datetime.min,
|
||||||
reading_state_last_modified=datetime.min,
|
reading_state_last_modified=datetime.min,
|
||||||
|
tags_last_modified=datetime.min,
|
||||||
):
|
):
|
||||||
self.raw_kobo_store_token = raw_kobo_store_token
|
self.raw_kobo_store_token = raw_kobo_store_token
|
||||||
self.books_last_created = books_last_created
|
self.books_last_created = books_last_created
|
||||||
self.books_last_modified = books_last_modified
|
self.books_last_modified = books_last_modified
|
||||||
self.archive_last_modified = archive_last_modified
|
self.archive_last_modified = archive_last_modified
|
||||||
self.reading_state_last_modified = reading_state_last_modified
|
self.reading_state_last_modified = reading_state_last_modified
|
||||||
|
self.tags_last_modified = tags_last_modified
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_headers(headers):
|
def from_headers(headers):
|
||||||
@ -128,6 +130,7 @@ class SyncToken():
|
|||||||
books_last_created = get_datetime_from_json(data_json, "books_last_created")
|
books_last_created = get_datetime_from_json(data_json, "books_last_created")
|
||||||
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
archive_last_modified = get_datetime_from_json(data_json, "archive_last_modified")
|
||||||
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
reading_state_last_modified = get_datetime_from_json(data_json, "reading_state_last_modified")
|
||||||
|
tags_last_modified = get_datetime_from_json(data_json, "tags_last_modified")
|
||||||
except TypeError:
|
except TypeError:
|
||||||
log.error("SyncToken timestamps don't parse to a datetime.")
|
log.error("SyncToken timestamps don't parse to a datetime.")
|
||||||
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
return SyncToken(raw_kobo_store_token=raw_kobo_store_token)
|
||||||
@ -137,7 +140,8 @@ class SyncToken():
|
|||||||
books_last_created=books_last_created,
|
books_last_created=books_last_created,
|
||||||
books_last_modified=books_last_modified,
|
books_last_modified=books_last_modified,
|
||||||
archive_last_modified=archive_last_modified,
|
archive_last_modified=archive_last_modified,
|
||||||
reading_state_last_modified=reading_state_last_modified
|
reading_state_last_modified=reading_state_last_modified,
|
||||||
|
tags_last_modified=tags_last_modified
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_kobo_store_header(self, store_headers):
|
def set_kobo_store_header(self, store_headers):
|
||||||
@ -159,7 +163,8 @@ class SyncToken():
|
|||||||
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
"books_last_modified": to_epoch_timestamp(self.books_last_modified),
|
||||||
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
"books_last_created": to_epoch_timestamp(self.books_last_created),
|
||||||
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
"archive_last_modified": to_epoch_timestamp(self.archive_last_modified),
|
||||||
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified)
|
"reading_state_last_modified": to_epoch_timestamp(self.reading_state_last_modified),
|
||||||
|
"tags_last_modified": to_epoch_timestamp(self.tags_last_modified)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return b64encode_json(token)
|
return b64encode_json(token)
|
||||||
|
97
cps/shelf.py
97
cps/shelf.py
@ -35,6 +35,24 @@ from .helper import common_filters
|
|||||||
shelf = Blueprint('shelf', __name__)
|
shelf = Blueprint('shelf', __name__)
|
||||||
log = logger.create()
|
log = logger.create()
|
||||||
|
|
||||||
|
def check_shelf_edit_permissions(cur_shelf):
|
||||||
|
if not cur_shelf.is_public and not cur_shelf.user_id == int(current_user.id):
|
||||||
|
log.error("User %s not allowed to edit shelf %s", current_user, cur_shelf)
|
||||||
|
return False
|
||||||
|
if cur_shelf.is_public and not current_user.role_edit_shelfs():
|
||||||
|
log.info("User %s not allowed to edit public shelves", current_user)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def check_shelf_view_permissions(cur_shelf):
|
||||||
|
if cur_shelf.is_public:
|
||||||
|
return True
|
||||||
|
if current_user.is_anonymous or cur_shelf.user_id != current_user.id:
|
||||||
|
log.error("User is unauthorized to view non-public shelf: %s", cur_shelf)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
@shelf.route("/shelf/add/<int:shelf_id>/<int:book_id>")
|
||||||
@login_required
|
@login_required
|
||||||
@ -48,21 +66,13 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Invalid shelf specified", 400
|
return "Invalid shelf specified", 400
|
||||||
|
|
||||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
|
flash(_(u"Sorry you are not allowed to add a book to the the shelf: %(shelfname)s", shelfname=shelf.name),
|
||||||
category="error")
|
category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
|
return "Sorry you are not allowed to add a book to the the shelf: %s" % shelf.name, 403
|
||||||
|
|
||||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
|
||||||
log.info("User %s not allowed to edit public shelves", current_user)
|
|
||||||
if not xhr:
|
|
||||||
flash(_(u"You are not allowed to edit public shelves"), category="error")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
return "User is not allowed to edit public shelves", 403
|
|
||||||
|
|
||||||
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
book_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||||
ub.BookShelf.book_id == book_id).first()
|
ub.BookShelf.book_id == book_id).first()
|
||||||
if book_in_shelf:
|
if book_in_shelf:
|
||||||
@ -78,8 +88,8 @@ def add_to_shelf(shelf_id, book_id):
|
|||||||
else:
|
else:
|
||||||
maxOrder = maxOrder[0]
|
maxOrder = maxOrder[0]
|
||||||
|
|
||||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1)
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book_id, order=maxOrder + 1))
|
||||||
ub.session.add(ins)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Book has been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
@ -99,16 +109,10 @@ def search_to_shelf(shelf_id):
|
|||||||
flash(_(u"Invalid shelf specified"), category="error")
|
flash(_(u"Invalid shelf specified"), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if not shelf.is_public and not shelf.user_id == int(current_user.id):
|
if not check_shelf_edit_permissions(shelf):
|
||||||
log.error("User %s not allowed to add a book to %s", current_user, shelf)
|
|
||||||
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
flash(_(u"You are not allowed to add a book to the the shelf: %(name)s", name=shelf.name), category="error")
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
if shelf.is_public and not current_user.role_edit_shelfs():
|
|
||||||
log.error("User %s not allowed to edit public shelves", current_user)
|
|
||||||
flash(_(u"User is not allowed to edit public shelves"), category="error")
|
|
||||||
return redirect(url_for('web.index'))
|
|
||||||
|
|
||||||
if current_user.id in searched_ids and searched_ids[current_user.id]:
|
if current_user.id in searched_ids and searched_ids[current_user.id]:
|
||||||
books_for_shelf = list()
|
books_for_shelf = list()
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).all()
|
||||||
@ -135,8 +139,8 @@ def search_to_shelf(shelf_id):
|
|||||||
|
|
||||||
for book in books_for_shelf:
|
for book in books_for_shelf:
|
||||||
maxOrder = maxOrder + 1
|
maxOrder = maxOrder + 1
|
||||||
ins = ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder)
|
shelf.books.append(ub.BookShelf(shelf=shelf.id, book_id=book, order=maxOrder))
|
||||||
ub.session.add(ins)
|
ub.session.merge(shelf)
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
flash(_(u"Books have been added to shelf: %(sname)s", sname=shelf.name), category="success")
|
||||||
else:
|
else:
|
||||||
@ -163,8 +167,7 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
# true 0 x 1
|
# true 0 x 1
|
||||||
# false 0 x 0
|
# false 0 x 0
|
||||||
|
|
||||||
if (not shelf.is_public and shelf.user_id == int(current_user.id)) \
|
if check_shelf_edit_permissions(shelf):
|
||||||
or (shelf.is_public and current_user.role_edit_shelfs()):
|
|
||||||
book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
book_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id,
|
||||||
ub.BookShelf.book_id == book_id).first()
|
ub.BookShelf.book_id == book_id).first()
|
||||||
|
|
||||||
@ -185,7 +188,6 @@ def remove_from_shelf(shelf_id, book_id):
|
|||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
return "", 204
|
return "", 204
|
||||||
else:
|
else:
|
||||||
log.error("User %s not allowed to remove a book from %s", current_user, shelf)
|
|
||||||
if not xhr:
|
if not xhr:
|
||||||
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
flash(_(u"Sorry you are not allowed to remove a book from this shelf: %(sname)s", sname=shelf.name),
|
||||||
category="error")
|
category="error")
|
||||||
@ -275,41 +277,33 @@ def edit_shelf(shelf_id):
|
|||||||
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
return render_title_template('shelf_edit.html', shelf=shelf, title=_(u"Edit a shelf"), page="shelfedit")
|
||||||
|
|
||||||
|
|
||||||
|
def delete_shelf_helper(cur_shelf):
|
||||||
|
if not cur_shelf or not check_shelf_edit_permissions(cur_shelf):
|
||||||
|
return
|
||||||
|
shelf_id = cur_shelf.id
|
||||||
|
ub.session.delete(cur_shelf)
|
||||||
|
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
||||||
|
ub.session.add(ub.ShelfArchive(uuid = cur_shelf.uuid, user_id = cur_shelf.uuid))
|
||||||
|
ub.session.commit()
|
||||||
|
log.info("successfully deleted %s", cur_shelf)
|
||||||
|
|
||||||
|
|
||||||
@shelf.route("/shelf/delete/<int:shelf_id>")
|
@shelf.route("/shelf/delete/<int:shelf_id>")
|
||||||
@login_required
|
@login_required
|
||||||
def delete_shelf(shelf_id):
|
def delete_shelf(shelf_id):
|
||||||
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
cur_shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
deleted = None
|
delete_shelf_helper(cur_shelf)
|
||||||
if current_user.role_admin():
|
|
||||||
deleted = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).delete()
|
|
||||||
else:
|
|
||||||
if (not cur_shelf.is_public and cur_shelf.user_id == int(current_user.id)) \
|
|
||||||
or (cur_shelf.is_public and current_user.role_edit_shelfs()):
|
|
||||||
deleted = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).delete()
|
|
||||||
|
|
||||||
if deleted:
|
|
||||||
ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id).delete()
|
|
||||||
ub.session.commit()
|
|
||||||
log.info("successfully deleted %s", cur_shelf)
|
|
||||||
return redirect(url_for('web.index'))
|
return redirect(url_for('web.index'))
|
||||||
|
|
||||||
# @shelf.route("/shelfdown/<int:shelf_id>")
|
# @shelf.route("/shelfdown/<int:shelf_id>")
|
||||||
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
@shelf.route("/shelf/<int:shelf_id>", defaults={'shelf_type': 1})
|
||||||
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
@shelf.route("/shelf/<int:shelf_id>/<int:shelf_type>")
|
||||||
def show_shelf(shelf_type, shelf_id):
|
def show_shelf(shelf_type, shelf_id):
|
||||||
if current_user.is_anonymous:
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
|
||||||
else:
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).first()
|
|
||||||
result = list()
|
result = list()
|
||||||
# user is allowed to access shelf
|
# user is allowed to access shelf
|
||||||
if shelf:
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
page = "shelf.html" if shelf_type == 1 else 'shelfdown.html'
|
||||||
|
|
||||||
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
books_in_shelf = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id)\
|
||||||
@ -344,15 +338,10 @@ def order_shelf(shelf_id):
|
|||||||
setattr(book, 'order', to_save[str(book.book_id)])
|
setattr(book, 'order', to_save[str(book.book_id)])
|
||||||
counter += 1
|
counter += 1
|
||||||
ub.session.commit()
|
ub.session.commit()
|
||||||
if current_user.is_anonymous:
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.is_public == 1, ub.Shelf.id == shelf_id).first()
|
shelf = ub.session.query(ub.Shelf).filter(ub.Shelf.id == shelf_id).first()
|
||||||
else:
|
|
||||||
shelf = ub.session.query(ub.Shelf).filter(or_(and_(ub.Shelf.user_id == int(current_user.id),
|
|
||||||
ub.Shelf.id == shelf_id),
|
|
||||||
and_(ub.Shelf.is_public == 1,
|
|
||||||
ub.Shelf.id == shelf_id))).first()
|
|
||||||
result = list()
|
result = list()
|
||||||
if shelf:
|
if shelf and check_shelf_view_permissions(shelf):
|
||||||
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
books_in_shelf2 = ub.session.query(ub.BookShelf).filter(ub.BookShelf.shelf == shelf_id) \
|
||||||
.order_by(ub.BookShelf.order.asc()).all()
|
.order_by(ub.BookShelf.order.asc()).all()
|
||||||
for book in books_in_shelf2:
|
for book in books_in_shelf2:
|
||||||
|
38
cps/ub.py
38
cps/ub.py
@ -21,6 +21,7 @@ from __future__ import division, print_function, unicode_literals
|
|||||||
import os
|
import os
|
||||||
import datetime
|
import datetime
|
||||||
import itertools
|
import itertools
|
||||||
|
import uuid
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
|
|
||||||
from flask import g
|
from flask import g
|
||||||
@ -35,8 +36,8 @@ except ImportError:
|
|||||||
from sqlalchemy import create_engine, exc, exists, event
|
from sqlalchemy import create_engine, exc, exists, event
|
||||||
from sqlalchemy import Column, ForeignKey
|
from sqlalchemy import Column, ForeignKey
|
||||||
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float
|
from sqlalchemy import String, Integer, SmallInteger, Boolean, DateTime, Float
|
||||||
from sqlalchemy.orm import backref, foreign, relationship, remote, sessionmaker, Session
|
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import backref, foreign, relationship, remote, sessionmaker, Session
|
||||||
from sqlalchemy.sql.expression import and_
|
from sqlalchemy.sql.expression import and_
|
||||||
from werkzeug.security import generate_password_hash
|
from werkzeug.security import generate_password_hash
|
||||||
|
|
||||||
@ -265,9 +266,13 @@ class Shelf(Base):
|
|||||||
__tablename__ = 'shelf'
|
__tablename__ = 'shelf'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String, default=lambda : str(uuid.uuid4()))
|
||||||
name = Column(String)
|
name = Column(String)
|
||||||
is_public = Column(Integer, default=0)
|
is_public = Column(Integer, default=0)
|
||||||
user_id = Column(Integer, ForeignKey('user.id'))
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
|
books = relationship("BookShelf", backref="ub_shelf", cascade="all, delete-orphan", lazy="dynamic")
|
||||||
|
created = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
last_modified = Column(DateTime, default=datetime.datetime.utcnow, onupdate=datetime.datetime.utcnow)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Shelf %d:%r>' % (self.id, self.name)
|
return '<Shelf %d:%r>' % (self.id, self.name)
|
||||||
@ -281,11 +286,22 @@ class BookShelf(Base):
|
|||||||
book_id = Column(Integer)
|
book_id = Column(Integer)
|
||||||
order = Column(Integer)
|
order = Column(Integer)
|
||||||
shelf = Column(Integer, ForeignKey('shelf.id'))
|
shelf = Column(Integer, ForeignKey('shelf.id'))
|
||||||
|
date_added = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Book %r>' % self.id
|
return '<Book %r>' % self.id
|
||||||
|
|
||||||
|
|
||||||
|
# This table keeps track of deleted Shelves so that deletes can be propagated to any paired Kobo device.
|
||||||
|
class ShelfArchive(Base):
|
||||||
|
__tablename__ = 'shelf_archive'
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
uuid = Column(String)
|
||||||
|
user_id = Column(Integer, ForeignKey('user.id'))
|
||||||
|
last_modified = Column(DateTime, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
|
||||||
class ReadBook(Base):
|
class ReadBook(Base):
|
||||||
__tablename__ = 'book_read_link'
|
__tablename__ = 'book_read_link'
|
||||||
|
|
||||||
@ -370,6 +386,10 @@ def receive_before_flush(session, flush_context, instances):
|
|||||||
if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)):
|
if isinstance(change, (ReadBook, KoboStatistics, KoboBookmark)):
|
||||||
if change.kobo_reading_state:
|
if change.kobo_reading_state:
|
||||||
change.kobo_reading_state.last_modified = datetime.datetime.utcnow()
|
change.kobo_reading_state.last_modified = datetime.datetime.utcnow()
|
||||||
|
# Maintain the last_modified bit for the Shelf table.
|
||||||
|
for change in itertools.chain(session.new, session.deleted):
|
||||||
|
if isinstance(change, BookShelf):
|
||||||
|
change.ub_shelf.last_modified = datetime.datetime.utcnow()
|
||||||
|
|
||||||
|
|
||||||
# Baseclass representing Downloads from calibre-web in app.db
|
# Baseclass representing Downloads from calibre-web in app.db
|
||||||
@ -463,7 +483,21 @@ def migrate_Database(session):
|
|||||||
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
|
conn.execute("ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME")
|
||||||
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
|
conn.execute("ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0")
|
||||||
session.commit()
|
session.commit()
|
||||||
|
try:
|
||||||
|
session.query(exists().where(Shelf.uuid)).scalar()
|
||||||
|
except exc.OperationalError:
|
||||||
|
conn = engine.connect()
|
||||||
|
conn.execute("ALTER TABLE shelf ADD column 'uuid' STRING")
|
||||||
|
conn.execute("ALTER TABLE shelf ADD column 'created' DATETIME")
|
||||||
|
conn.execute("ALTER TABLE shelf ADD column 'last_modified' DATETIME")
|
||||||
|
conn.execute("ALTER TABLE book_shelf_link ADD column 'date_added' DATETIME")
|
||||||
|
for shelf in session.query(Shelf).all():
|
||||||
|
shelf.uuid = str(uuid.uuid4())
|
||||||
|
shelf.created = datetime.datetime.now()
|
||||||
|
shelf.last_modified = datetime.datetime.now()
|
||||||
|
for book_shelf in session.query(BookShelf).all():
|
||||||
|
book_shelf.date_added = datetime.datetime.now()
|
||||||
|
session.commit()
|
||||||
# Handle table exists, but no content
|
# Handle table exists, but no content
|
||||||
cnt = session.query(Registration).count()
|
cnt = session.query(Registration).count()
|
||||||
if not cnt:
|
if not cnt:
|
||||||
|
Loading…
Reference in New Issue
Block a user