This commit is contained in:
parent
2dbe0416de
commit
09d8e2bf2d
6
.pre-commit-config.yaml
Normal file
6
.pre-commit-config.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: stable
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3.7
|
139
backend.py
139
backend.py
|
@ -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),
|
||||
}
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
Reference in a new issue