From e404da4192286eefba0ece168b478b2e1b7bbdb0 Mon Sep 17 00:00:00 2001 From: Michael Shavit Date: Fri, 24 Jan 2020 00:04:16 -0500 Subject: [PATCH] Add support for book 'deletion' (i.e archiving) from a Kobo device. --- cps/kobo.py | 43 +++++++++++++++++++++++++++++++++++++++---- cps/ub.py | 11 +++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/cps/kobo.py b/cps/kobo.py index 7e1cbc8e..57ef1ab5 100644 --- a/cps/kobo.py +++ b/cps/kobo.py @@ -21,6 +21,7 @@ import sys import uuid from datetime import datetime from time import gmtime, strftime + try: from urllib import unquote except ImportError: @@ -35,12 +36,12 @@ from flask import ( url_for, redirect, ) -from flask_login import login_required +from flask_login import login_required, current_user from werkzeug.datastructures import Headers from sqlalchemy import func import requests -from . import config, logger, kobo_auth, db, helper +from . import config, logger, kobo_auth, db, helper, ub from .services import SyncToken as SyncToken from .web import download_required @@ -53,6 +54,7 @@ kobo_auth.register_url_value_preprocessor(kobo) log = logger.create() + 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/") @@ -114,6 +116,14 @@ def HandleSyncRequest(): # in case of external changes (e.g: adding a book through Calibre). db.reconnect_db(config) + archived_books = ( + ub.session.query(ub.ArchivedBook) + .filter(ub.ArchivedBook.user_id == int(current_user.id)) + .filter(ub.ArchivedBook.is_archived == True) + .all() + ) + archived_book_ids = [archived_book.book_id for archived_book in archived_books] + # 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. @@ -122,6 +132,7 @@ def HandleSyncRequest(): .join(db.Data) .filter(func.datetime(db.Books.last_modified) != sync_token.books_last_modified) .filter(db.Data.format.in_(KOBO_FORMATS)) + .filter(db.Books.id.notin_(archived_book_ids)) .all() ) for book in changed_entries: @@ -342,13 +353,37 @@ def TopLevelEndpoint(): return make_response(jsonify({})) +@kobo.route("/v1/library/", methods=["DELETE"]) +@login_required +def HandleBookDeletionRequest(book_uuid): + log.info("Kobo book deletion request received for book %s" % book_uuid) + book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first() + if not book: + log.info(u"Book %s not found in database", book_uuid) + return redirect_or_proxy_request() + + book_id = book.id + archived_book = ( + ub.session.query(ub.ArchivedBook) + .filter(ub.ArchivedBook.book_id == book_id) + .first() + ) + if not archived_book: + archived_book = ub.ArchivedBook(user_id=current_user.id, book_id=book_id) + archived_book.book_id = book_id + archived_book.is_archived = True + ub.session.merge(archived_book) + ub.session.commit() + + return ("", 204) + + # TODO: Implement the following routes -@kobo.route("/v1/library/", methods=["DELETE", "GET"]) @kobo.route("/v1/library//state", methods=["PUT"]) @kobo.route("/v1/library/tags", methods=["POST"]) @kobo.route("/v1/library/tags/", methods=["POST"]) @kobo.route("/v1/library/tags/", methods=["DELETE"]) -def HandleUnimplementedRequest(dummy=None, book_uuid=None, shelf_name=None, tag_id=None): +def HandleUnimplementedRequest(book_uuid=None, shelf_name=None, tag_id=None): return redirect_or_proxy_request() diff --git a/cps/ub.py b/cps/ub.py index 8564ef21..7ebd287c 100644 --- a/cps/ub.py +++ b/cps/ub.py @@ -300,6 +300,15 @@ class Bookmark(Base): format = Column(String(collation='NOCASE')) bookmark_key = Column(String) +# Baseclass representing books that are archived on the user's Kobo device. +class ArchivedBook(Base): + __tablename__ = 'archived_book' + + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('user.id')) + book_id = Column(Integer) + is_archived = Column(Boolean, unique=False) + # Baseclass representing Downloads from calibre-web in app.db class Downloads(Base): @@ -353,6 +362,8 @@ def migrate_Database(session): ReadBook.__table__.create(bind=engine) if not engine.dialect.has_table(engine.connect(), "bookmark"): Bookmark.__table__.create(bind=engine) + if not engine.dialect.has_table(engine.connect(), "archived_book"): + ArchivedBook.__table__.create(bind=engine) if not engine.dialect.has_table(engine.connect(), "registration"): ReadBook.__table__.create(bind=engine) conn = engine.connect()