Commit 53578671 authored by Michael Shavit's avatar Michael Shavit

Add initial support for Kobo device Sync endpoint.

- Supports /v1/library/sync call to get list of books
- Supports /v1/library/metadata call to get metadata for a given book
  + Assumes books are stored on Backblaze for metadata call
- Changes to helper.py so that we can return no cover instead of a blank
image.
parent 0c40e40d
...@@ -32,3 +32,4 @@ gdrive_credentials ...@@ -32,3 +32,4 @@ gdrive_credentials
vendor vendor
client_secrets.json client_secrets.json
b2_secrets.json
...@@ -41,6 +41,8 @@ from cps.shelf import shelf ...@@ -41,6 +41,8 @@ from cps.shelf import shelf
from cps.admin import admi from cps.admin import admi
from cps.gdrive import gdrive from cps.gdrive import gdrive
from cps.editbooks import editbook from cps.editbooks import editbook
from cps.kobo import kobo
try: try:
from cps.oauth_bb import oauth from cps.oauth_bb import oauth
oauth_available = True oauth_available = True
...@@ -58,6 +60,7 @@ def main(): ...@@ -58,6 +60,7 @@ def main():
app.register_blueprint(admi) app.register_blueprint(admi)
app.register_blueprint(gdrive) app.register_blueprint(gdrive)
app.register_blueprint(editbook) app.register_blueprint(editbook)
app.register_blueprint(kobo)
if oauth_available: if oauth_available:
app.register_blueprint(oauth) app.register_blueprint(oauth)
success = web_server.start() success = web_server.start()
......
...@@ -295,6 +295,8 @@ def _configuration_update_helper(): ...@@ -295,6 +295,8 @@ def _configuration_update_helper():
if config.config_certfile and not os.path.isfile(config.config_certfile): if config.config_certfile and not os.path.isfile(config.config_certfile):
return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError) return _configuration_result('Certfile location is not valid, please enter correct path', gdriveError)
_config_string("config_server_url")
_config_checkbox_int("config_uploading") _config_checkbox_int("config_uploading")
_config_checkbox_int("config_anonbrowse") _config_checkbox_int("config_anonbrowse")
_config_checkbox_int("config_public_reg") _config_checkbox_int("config_public_reg")
......
...@@ -49,6 +49,7 @@ class _Settings(_Base): ...@@ -49,6 +49,7 @@ class _Settings(_Base):
config_port = Column(Integer, default=constants.DEFAULT_PORT) config_port = Column(Integer, default=constants.DEFAULT_PORT)
config_certfile = Column(String) config_certfile = Column(String)
config_keyfile = Column(String) config_keyfile = Column(String)
config_server_url = Column(String, default='')
config_calibre_web_title = Column(String, default=u'Calibre-Web') config_calibre_web_title = Column(String, default=u'Calibre-Web')
config_books_per_page = Column(Integer, default=60) config_books_per_page = Column(Integer, default=60)
......
...@@ -25,13 +25,13 @@ import ast ...@@ -25,13 +25,13 @@ import ast
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy import Table, Column, ForeignKey from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy import String, Integer, Boolean from sqlalchemy import String, Integer, Boolean, TIMESTAMP
from sqlalchemy.orm import relationship, sessionmaker, scoped_session from sqlalchemy.orm import relationship, sessionmaker, scoped_session
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
session = None session = None
cc_exceptions = ['datetime', 'comments', 'float', 'composite', 'series'] cc_exceptions = ['comments', 'float', 'composite', 'series']
cc_classes = {} cc_classes = {}
...@@ -251,10 +251,10 @@ class Books(Base): ...@@ -251,10 +251,10 @@ class Books(Base):
title = Column(String) title = Column(String)
sort = Column(String) sort = Column(String)
author_sort = Column(String) author_sort = Column(String)
timestamp = Column(String) timestamp = Column(TIMESTAMP)
pubdate = Column(String) pubdate = Column(String)
series_index = Column(String) series_index = Column(String)
last_modified = Column(String) last_modified = Column(TIMESTAMP)
path = Column(String) path = Column(String)
has_cover = Column(Integer) has_cover = Column(Integer)
uuid = Column(String) uuid = Column(String)
...@@ -353,7 +353,7 @@ def setup_db(config): ...@@ -353,7 +353,7 @@ def setup_db(config):
# conn.connection.create_function('upper', 1, ucase) # conn.connection.create_function('upper', 1, ucase)
if not cc_classes: if not cc_classes:
cc = conn.execute("SELECT id, datatype FROM custom_columns") cc = conn.execute("SELECT id, datatype, normalized FROM custom_columns")
cc_ids = [] cc_ids = []
books_custom_column_links = {} books_custom_column_links = {}
...@@ -366,7 +366,7 @@ def setup_db(config): ...@@ -366,7 +366,7 @@ def setup_db(config):
ForeignKey('custom_column_' + str(row.id) + '.id'), ForeignKey('custom_column_' + str(row.id) + '.id'),
primary_key=True) primary_key=True)
) )
cc_ids.append([row.id, row.datatype]) cc_ids.append([row.id, row.datatype, row.normalized])
if row.datatype == 'bool': if row.datatype == 'bool':
ccdict = {'__tablename__': 'custom_column_' + str(row.id), ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True), 'id': Column(Integer, primary_key=True),
...@@ -377,6 +377,11 @@ def setup_db(config): ...@@ -377,6 +377,11 @@ def setup_db(config):
'id': Column(Integer, primary_key=True), 'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')), 'book': Column(Integer, ForeignKey('books.id')),
'value': Column(Integer)} 'value': Column(Integer)}
elif not row.normalized:
ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True),
'book': Column(Integer, ForeignKey('books.id')),
'value': Column(String)}
else: else:
ccdict = {'__tablename__': 'custom_column_' + str(row.id), ccdict = {'__tablename__': 'custom_column_' + str(row.id),
'id': Column(Integer, primary_key=True), 'id': Column(Integer, primary_key=True),
...@@ -384,7 +389,8 @@ def setup_db(config): ...@@ -384,7 +389,8 @@ def setup_db(config):
cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict) cc_classes[row.id] = type(str('Custom_Column_' + str(row.id)), (Base,), ccdict)
for cc_id in cc_ids: for cc_id in cc_ids:
if (cc_id[1] == 'bool') or (cc_id[1] == 'int'): normalized = cc_id[2]
if (not normalized):
setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]], setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]],
primaryjoin=( primaryjoin=(
Books.id == cc_classes[cc_id[0]].book), Books.id == cc_classes[cc_id[0]].book),
...@@ -393,6 +399,16 @@ def setup_db(config): ...@@ -393,6 +399,16 @@ def setup_db(config):
setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]], setattr(Books, 'custom_column_' + str(cc_id[0]), relationship(cc_classes[cc_id[0]],
secondary=books_custom_column_links[cc_id[0]], secondary=books_custom_column_links[cc_id[0]],
backref='books')) backref='books'))
#for cc_id in cc_ids:
# if (cc_id[1] == 'bool') or (cc_id[1] == 'int'):
# setattr(Books, 'custom_column_' + str(cc_id[2]), relationship(cc_classes[cc_id[0]],
# primaryjoin=(
# Books.id == cc_classes[cc_id[0]].book),
# backref='books'))
# else:
# setattr(Books, 'custom_column_' + str(cc_id[2]), relationship(cc_classes[cc_id[0]],
# secondary=books_custom_column_links[cc_id[0]],
# backref='books'))
global session global session
......
...@@ -428,32 +428,46 @@ def delete_book(book, calibrepath, book_format): ...@@ -428,32 +428,46 @@ def delete_book(book, calibrepath, book_format):
return delete_book_file(book, calibrepath, book_format) return delete_book_file(book, calibrepath, book_format)
def get_cover_on_failure(use_generic_cover):
if use_generic_cover:
return send_from_directory(_STATIC_DIR, "generic_cover.jpg")
else:
return None
def get_book_cover(book_id): def get_book_cover(book_id):
book = db.session.query(db.Books).filter(db.Books.id == book_id).first() book = db.session.query(db.Books).filter(db.Books.id == book_id).first()
if book.has_cover: return get_book_cover_internal(book, False)
def get_book_cover_with_uuid(book_uuid,
use_generic_cover_on_failure=True):
book = db.session.query(db.Books).filter(db.Books.uuid == book_uuid).first()
return get_book_cover_internal(book, use_generic_cover_on_failure)
def get_book_cover_internal(book,
use_generic_cover_on_failure):
if book.has_cover:
if config.config_use_google_drive: if config.config_use_google_drive:
try: try:
if not gd.is_gdrive_ready(): if not gd.is_gdrive_ready():
return send_from_directory(_STATIC_DIR, "generic_cover.jpg") return get_cover_on_failure(use_generic_cover_on_failure)
path=gd.get_cover_via_gdrive(book.path) path=gd.get_cover_via_gdrive(book.path)
if path: if path:
return redirect(path) return redirect(path)
else: else:
log.error('%s/cover.jpg not found on Google Drive', book.path) log.error('%s/cover.jpg not found on Google Drive', book.path)
return send_from_directory(_STATIC_DIR, "generic_cover.jpg") return get_cover_on_failure(use_generic_cover_on_failure)
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
# traceback.print_exc() # traceback.print_exc()
return send_from_directory(_STATIC_DIR,"generic_cover.jpg") return get_cover_on_failure(use_generic_cover_on_failure)
else: else:
cover_file_path = os.path.join(config.config_calibre_dir, book.path) cover_file_path = os.path.join(config.config_calibre_dir, book.path)
if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")): if os.path.isfile(os.path.join(cover_file_path, "cover.jpg")):
return send_from_directory(cover_file_path, "cover.jpg") return send_from_directory(cover_file_path, "cover.jpg")
else: else:
return send_from_directory(_STATIC_DIR,"generic_cover.jpg") return get_cover_on_failure(use_generic_cover_on_failure)
else: else:
return send_from_directory(_STATIC_DIR,"generic_cover.jpg") return get_cover_on_failure(use_generic_cover_on_failure)
# saves book cover from url # saves book cover from url
......
This diff is collapsed.
...@@ -104,6 +104,10 @@ ...@@ -104,6 +104,10 @@
<!--option-- value="3" {% if config.config_updatechannel == 3 %}selected{% endif %}>{{_('Nightly (Automatic)')}}</option--> <!--option-- value="3" {% if config.config_updatechannel == 3 %}selected{% endif %}>{{_('Nightly (Automatic)')}}</option-->
</select> </select>
</div> </div>
<div class="form-group">
<label for="config_server_url">{{_('Server Url. This is only used for the (experimental) Kobo device library sync')}}</label>
<input type="text" class="form-control" name="config_server_url" id="config_server_url" value="{% if config.config_server_url != None %}{{ config.config_server_url }}{% endif %}" autocomplete="off">
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -13,3 +13,5 @@ SQLAlchemy>=1.1.0 ...@@ -13,3 +13,5 @@ SQLAlchemy>=1.1.0
tornado>=4.1 tornado>=4.1
Wand>=0.4.4 Wand>=0.4.4
unidecode>=0.04.19 unidecode>=0.04.19
b2sdk>=1.0.2,<2.0.0
jsonschema>=3.2.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment