Add black
continuous-integration/drone/push Build is passing Details

master
Simon Bruder 2020-02-15 14:41:11 +00:00
parent 2dbe0416de
commit 09d8e2bf2d
No known key found for this signature in database
GPG Key ID: 6F03E0000CC5B62F
5 changed files with 141 additions and 98 deletions

6
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: stable
hooks:
- id: black
language_version: python3.7

View File

@ -12,9 +12,9 @@ DEFAULT_WEBP_QUALITY = 80
DEFAULT_WEBP_METHOD = 0
DEFAULT_WEBP_SIZE = 1908 # width of FullHD monitor without scroll bar in full screen
mimetypes['.webp'] = 'image/webp'
mimetypes[".webp"] = "image/webp"
if os.environ.get('DISK_CACHE', '1') == '0':
if os.environ.get("DISK_CACHE", "1") == "0":
disk_cache = DiskCache(enabled=False)
else:
disk_cache = DiskCache()
@ -44,27 +44,29 @@ class BaseDB:
webp_lossless = False
self.webp_config = {
'quality': webp_quality,
'method': webp_method,
'lossless': webp_lossless,
'size': webp_size
"quality": webp_quality,
"method": webp_method,
"lossless": webp_lossless,
"size": webp_size,
}
def _generate_webp(self, fp, max_size=None):
if max_size is None:
max_size = tuple([self.webp_config['size']] * 2)
max_size = tuple([self.webp_config["size"]] * 2)
image = Image.open(fp)
image.thumbnail(max_size)
image_buffer = BytesIO()
image.save(
image_buffer,
format='webp',
save_all=True, #
append_images=[image], # https://github.com/python-pillow/Pillow/issues/4042
quality=self.webp_config['quality'],
method=self.webp_config['method'],
lossless=self.webp_config['lossless']
format="webp",
save_all=True,
append_images=[
image
], # https://github.com/python-pillow/Pillow/issues/4042
quality=self.webp_config["quality"],
method=self.webp_config["method"],
lossless=self.webp_config["lossless"],
)
image_buffer.seek(0)
@ -79,35 +81,39 @@ class BaseDB:
etag = str(crc32(data))
thumbnail_cache[filepath] = {
'data_raw': data,
'etag': etag,
'mimetype': 'image/webp'
"data_raw": data,
"etag": etag,
"mimetype": "image/webp",
}
thumbnail = thumbnail_cache[filepath]
thumbnail['buffer'] = BytesIO()
thumbnail['buffer'].write(thumbnail['data_raw'])
thumbnail['buffer'].seek(0)
thumbnail["buffer"] = BytesIO()
thumbnail["buffer"].write(thumbnail["data_raw"])
thumbnail["buffer"].seek(0)
return thumbnail
def _generate_page(self, page_buffer, volume, page):
page_buffer = self._generate_webp(page_buffer)
disk_cache.set(f'{volume}-{page}', page_buffer)
disk_cache.set(f"{volume}-{page}", page_buffer)
page_buffer.seek(0)
return page_buffer
class CalibreDB(BaseDB):
def __init__(self, path='metadata.db', webp_quality=DEFAULT_WEBP_QUALITY, webp_method=DEFAULT_WEBP_METHOD, webp_size=DEFAULT_WEBP_SIZE):
def __init__(
self,
path="metadata.db",
webp_quality=DEFAULT_WEBP_QUALITY,
webp_method=DEFAULT_WEBP_METHOD,
webp_size=DEFAULT_WEBP_SIZE,
):
super().__init__(
webp_quality=webp_quality,
webp_method=webp_method,
webp_size=webp_size
webp_quality=webp_quality, webp_method=webp_method, webp_size=webp_size
)
self.database_path = f'file:{path}?mode=ro'
self.database_path = f"file:{path}?mode=ro"
def create_cursor(self):
conn = sqlite3.connect(self.database_path, uri=True)
@ -116,7 +122,8 @@ class CalibreDB(BaseDB):
def get_series_list(self):
cursor = self.create_cursor()
series = cursor.execute('''
series = cursor.execute(
"""
select
series.id as id,
series.name as name,
@ -133,13 +140,15 @@ class CalibreDB(BaseDB):
data.format = \'CBZ\'
group by series.name
having min(books.series_index)
''')
"""
)
return series
def get_series_cover(self, series_id):
cursor = self.create_cursor()
first_volume = cursor.execute('''
first_volume = cursor.execute(
"""
select
books.id
from
@ -152,27 +161,33 @@ class CalibreDB(BaseDB):
series.id = ?
group by series.name
having min(books.series_index)
''', (str(series_id),)).fetchone()
""",
(str(series_id),),
).fetchone()
if first_volume is None:
raise exceptions.NotFound()
return self.get_volume_cover(first_volume['id'])
return self.get_volume_cover(first_volume["id"])
def get_series_cover_thumbnail(self, series_id):
return self._generate_thumbnail(self.get_series_cover(series_id))
def get_series_volumes(self, series_id):
cursor = self.create_cursor()
title = cursor.execute('''
title = cursor.execute(
"""
select
series.name
from
series
where
series.id = ?
''', (str(series_id),)).fetchone()['name']
volumes = cursor.execute('''
""",
(str(series_id),),
).fetchone()["name"]
volumes = cursor.execute(
"""
select
books.id,
books.title,
@ -186,16 +201,16 @@ class CalibreDB(BaseDB):
books_series_link.series = series.id and
series.id = ?
order by books.series_index
''', (str(series_id),)).fetchall()
""",
(str(series_id),),
).fetchall()
return {
'title': title,
'volumes': volumes
}
return {"title": title, "volumes": volumes}
def get_volume_cover(self, volume_id):
cursor = self.create_cursor()
volume = cursor.execute('''
volume = cursor.execute(
"""
select
books.has_cover as has_cover,
books.path as path
@ -204,10 +219,12 @@ class CalibreDB(BaseDB):
where
books.id = ?
order by books.series_index
''', (str(volume_id),)).fetchone()
""",
(str(volume_id),),
).fetchone()
if volume['has_cover']:
return volume['path'] + '/cover.jpg'
if volume["has_cover"]:
return volume["path"] + "/cover.jpg"
else:
raise exceptions.NotFound()
@ -216,7 +233,8 @@ class CalibreDB(BaseDB):
def get_volume_filepath(self, volume_id):
cursor = self.create_cursor()
location = cursor.execute('''
location = cursor.execute(
"""
select
books.path as path,
data.name as filename,
@ -227,16 +245,21 @@ class CalibreDB(BaseDB):
where
data.book = books.id and
books.id = ?
''', (str(volume_id),)).fetchone()
""",
(str(volume_id),),
).fetchone()
if location is None:
raise exceptions.NotFound()
return location['path'] + '/' + location['filename'] + '.' + location['extension']
return (
location["path"] + "/" + location["filename"] + "." + location["extension"]
)
def get_volume_info(self, volume_id):
cursor = self.create_cursor()
volume_info = cursor.execute('''
volume_info = cursor.execute(
"""
select
books.title,
books_series_link.series
@ -246,15 +269,17 @@ class CalibreDB(BaseDB):
where
books_series_link.book = books.id and
books.id = ?
''', (str(volume_id),)).fetchone()
""",
(str(volume_id),),
).fetchone()
volume_info['pages'] = self.get_volume_page_number(volume_id)
volume_info["pages"] = self.get_volume_page_number(volume_id)
return volume_info
def get_volume_page_number(self, volume_id):
path = self.get_volume_filepath(volume_id)
with ZipFile(path, 'r') as volume:
with ZipFile(path, "r") as volume:
filelist = filter_zip_filelist(volume.filelist)
return len(filelist)
@ -262,7 +287,7 @@ class CalibreDB(BaseDB):
if page_number < 1:
raise exceptions.NotFound()
path = self.get_volume_filepath(volume_id)
with ZipFile(path, 'r') as volume:
with ZipFile(path, "r") as volume:
try:
filelist = filter_zip_filelist(volume.filelist)
zip_info = filelist[page_number - 1]
@ -279,16 +304,18 @@ class CalibreDB(BaseDB):
page_buffer.write(volume.read(page_filename))
page_buffer.seek(0)
else:
mimetype = 'image/webp'
mimetype = "image/webp"
try:
page_buffer = disk_cache.get(f'{volume_id}-{page_number}')
page_buffer = disk_cache.get(f"{volume_id}-{page_number}")
except FileNotFoundError:
with volume.open(page_filename) as orig_page_buffer:
page_buffer = self._generate_page(orig_page_buffer, volume_id, page_number)
page_buffer = self._generate_page(
orig_page_buffer, volume_id, page_number
)
return {
'buffer': page_buffer,
'mimetype': mimetype,
'etag': str(zip_info.CRC)
"buffer": page_buffer,
"mimetype": mimetype,
"etag": str(zip_info.CRC),
}

View File

@ -5,7 +5,7 @@ import os
class DiskCache:
def __init__(self, path=None, enabled=True):
if path is None:
self.path = os.path.join(os.path.dirname(__file__), 'cache')
self.path = os.path.join(os.path.dirname(__file__), "cache")
else:
self.path = path
@ -19,14 +19,14 @@ class DiskCache:
if self.enabled is False:
return
with open(self._build_path(identifier), 'wb') as f:
with open(self._build_path(identifier), "wb") as f:
f.write(buf.read())
def get(self, identifier):
if self.enabled is False:
raise FileNotFoundError
with open(self._build_path(identifier), 'rb') as f:
with open(self._build_path(identifier), "rb") as f:
return BytesIO(f.read())
def _build_path(self, identifier):

View File

@ -9,33 +9,31 @@ import webbrowser
import werkzeug.exceptions as exceptions
# disable access log
log = logging.getLogger('werkzeug')
log = logging.getLogger("werkzeug")
log.setLevel(logging.ERROR)
# disable flask startup message
os.environ['WERKZEUG_RUN_MAIN'] = 'true'
os.environ["WERKZEUG_RUN_MAIN"] = "true"
mimetypes['.webp'] = 'image/webp'
archive = ZipFile(sys.argv[1], 'r')
app = Flask(__name__, static_folder='frontend/dist/static', template_folder='frontend/dist')
mimetypes[".webp"] = "image/webp"
archive = ZipFile(sys.argv[1], "r")
app = Flask(
__name__, static_folder="frontend/dist/static", template_folder="frontend/dist"
)
# kind of redundant, but avoids returning 200 if page does not exist
@app.route('/')
@app.route('/series/0')
@app.route('/volume/0')
@app.route("/")
@app.route("/series/0")
@app.route("/volume/0")
def serve_app(**kwargs):
return render_template('index.html')
return render_template("index.html")
@app.route('/api/volume/0')
@app.route("/api/volume/0")
def get_volume_info():
return jsonify({
'pages': len(archive.filelist),
'series': 0,
'title': sys.argv[1]
})
return jsonify({"pages": len(archive.filelist), "series": 0, "title": sys.argv[1]})
@app.route('/api/volume/0/page/<int:page_number>')
@app.route("/api/volume/0/page/<int:page_number>")
def get_volume_page(page_number):
try:
page = archive.filelist[page_number - 1]
@ -45,6 +43,6 @@ def get_volume_page(page_number):
return Response(archive.read(page.filename), mimetype=mimetype)
if __name__ == '__main__':
webbrowser.open('http://localhost:5051/volume/0')
app.run(host='127.0.0.1', port=5051)
if __name__ == "__main__":
webbrowser.open("http://localhost:5051/volume/0")
app.run(host="127.0.0.1", port=5051)

View File

@ -1,6 +1,13 @@
#!/usr/bin/env python3
from backend import CalibreDB
from flask import Flask, jsonify, send_from_directory, send_file, render_template, request
from flask import (
Flask,
jsonify,
send_from_directory,
send_file,
render_template,
request,
)
from flask_cors import CORS
from werkzeug.routing import BaseConverter
import os
@ -12,7 +19,7 @@ def send_from_cwd(filename):
def send_file_with_etag(fp, etag, **kwargs):
if request.if_none_match and etag in request.if_none_match:
return '', 304
return "", 304
response = send_file(fp, **kwargs)
response.set_etag(etag)
@ -21,70 +28,75 @@ def send_file_with_etag(fp, etag, **kwargs):
def send_image(image):
return send_file_with_etag(image['buffer'], image['etag'], mimetype=image['mimetype'])
return send_file_with_etag(
image["buffer"], image["etag"], mimetype=image["mimetype"]
)
app = Flask(__name__, static_folder='frontend/dist/static', template_folder='frontend/dist')
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 604800 # 1 week
app = Flask(
__name__, static_folder="frontend/dist/static", template_folder="frontend/dist"
)
app.config["SEND_FILE_MAX_AGE_DEFAULT"] = 604800 # 1 week
CORS(app)
db = CalibreDB()
# kind of redundant, but avoids returning 200 if page does not exist
@app.route('/')
@app.route('/series/<id>')
@app.route('/volume/<id>')
@app.route("/")
@app.route("/series/<id>")
@app.route("/volume/<id>")
def serve_app(**kwargs):
return render_template('index.html')
return render_template("index.html")
@app.route('/api/series')
@app.route("/api/series")
def get_series_list():
return jsonify(list(db.get_series_list()))
@app.route('/api/series/<int:series_id>/cover')
@app.route("/api/series/<int:series_id>/cover")
def get_series_cover(series_id):
return send_from_cwd(db.get_series_cover(series_id))
@app.route('/api/series/<int:series_id>/cover/thumbnail')
@app.route("/api/series/<int:series_id>/cover/thumbnail")
def get_series_cover_thumbnail(series_id):
thumbnail = db.get_series_cover_thumbnail(series_id)
return send_image(thumbnail)
@app.route('/api/series/<int:series_id>')
@app.route("/api/series/<int:series_id>")
def get_series_info(series_id):
return jsonify(db.get_series_volumes(series_id))
@app.route('/api/volume/<int:volume_id>/cover')
@app.route("/api/volume/<int:volume_id>/cover")
def get_volume_cover(volume_id):
return send_from_cwd(db.get_volume_cover(volume_id))
@app.route('/api/volume/<int:volume_id>/cover/thumbnail')
@app.route("/api/volume/<int:volume_id>/cover/thumbnail")
def get_volume_cover_thumbnail(volume_id):
thumbnail = db.get_volume_cover_thumbnail(volume_id)
return send_image(thumbnail)
@app.route('/api/volume/<int:volume_id>')
@app.route("/api/volume/<int:volume_id>")
def get_volume_info(volume_id):
return jsonify(db.get_volume_info(volume_id))
@app.route('/api/volume/<int:volume_id>/page/<int:page_number>')
@app.route("/api/volume/<int:volume_id>/page/<int:page_number>")
def get_volume_page(volume_id, page_number):
page = db.get_volume_page(volume_id, page_number)
return send_image(page)
@app.route('/api/volume/<int:volume_id>/page/<int:page_number>/original')
@app.route("/api/volume/<int:volume_id>/page/<int:page_number>/original")
def get_volume_page_original(volume_id, page_number):
page = db.get_volume_page(volume_id, page_number, original=True)
return send_image(page)
if __name__ == '__main__':
app.run(host='0.0.0.0')
if __name__ == "__main__":
app.run(host="0.0.0.0")