backend: cache etag for thumbnails; deduplicate code for sending image
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
9800befe37
commit
6eabe96a70
26
backend.py
26
backend.py
|
@ -2,6 +2,7 @@ from PIL import Image
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from mimetypes import types_map as mimetypes
|
from mimetypes import types_map as mimetypes
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
from zlib import crc32
|
||||||
import os.path
|
import os.path
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import werkzeug.exceptions as exceptions
|
import werkzeug.exceptions as exceptions
|
||||||
|
@ -20,23 +21,28 @@ def dict_factory(cursor, row):
|
||||||
|
|
||||||
|
|
||||||
def generate_thumbnail(filepath):
|
def generate_thumbnail(filepath):
|
||||||
if filepath in thumbnail_cache:
|
if filepath not in thumbnail_cache:
|
||||||
thumbnail = BytesIO()
|
|
||||||
thumbnail.write(thumbnail_cache[filepath])
|
|
||||||
else:
|
|
||||||
image = Image.open(filepath)
|
image = Image.open(filepath)
|
||||||
image.thumbnail((512, 512))
|
image.thumbnail((512, 512))
|
||||||
thumbnail = BytesIO()
|
thumbnail = BytesIO()
|
||||||
image.save(thumbnail, 'webp')
|
image.save(thumbnail, 'webp')
|
||||||
thumbnail.seek(0)
|
thumbnail.seek(0)
|
||||||
thumbnail_cache[filepath] = thumbnail.read()
|
|
||||||
|
|
||||||
thumbnail.seek(0)
|
data = thumbnail.read()
|
||||||
|
etag = str(crc32(data))
|
||||||
|
|
||||||
return {
|
thumbnail_cache[filepath] = {
|
||||||
'data': thumbnail,
|
'data_raw': data,
|
||||||
'mimetype': 'image/webp'
|
'etag': etag,
|
||||||
}
|
'mimetype': 'image/webp'
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbnail = thumbnail_cache[filepath]
|
||||||
|
thumbnail['data'] = BytesIO()
|
||||||
|
thumbnail['data'].write(thumbnail['data_raw'])
|
||||||
|
thumbnail['data'].seek(0)
|
||||||
|
|
||||||
|
return thumbnail
|
||||||
|
|
||||||
|
|
||||||
class CalibreDB:
|
class CalibreDB:
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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 flask_cors import CORS
|
||||||
from werkzeug.routing import BaseConverter
|
from werkzeug.routing import BaseConverter
|
||||||
from zlib import crc32
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,10 +10,7 @@ def send_from_cwd(filename):
|
||||||
return send_from_directory(os.getcwd(), filename)
|
return send_from_directory(os.getcwd(), filename)
|
||||||
|
|
||||||
|
|
||||||
def send_file_with_etag(fp, etag=None, **kwargs):
|
def send_file_with_etag(fp, etag, **kwargs):
|
||||||
if etag is None:
|
|
||||||
etag = str(crc32(fp.read()))
|
|
||||||
|
|
||||||
if request.if_none_match and etag in request.if_none_match:
|
if request.if_none_match and etag in request.if_none_match:
|
||||||
return '', 304
|
return '', 304
|
||||||
|
|
||||||
|
@ -24,6 +20,10 @@ def send_file_with_etag(fp, etag=None, **kwargs):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def send_image(image):
|
||||||
|
return send_file_with_etag(image['data'], image['etag'], mimetype=image['mimetype'])
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='frontend/dist/static', template_folder='frontend/dist')
|
app = Flask(__name__, static_folder='frontend/dist/static', template_folder='frontend/dist')
|
||||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 604800 # 1 week
|
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 604800 # 1 week
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
@ -51,7 +51,7 @@ def 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):
|
def get_series_cover_thumbnail(series_id):
|
||||||
thumbnail = db.get_series_cover_thumbnail(series_id)
|
thumbnail = db.get_series_cover_thumbnail(series_id)
|
||||||
return send_file_with_etag(thumbnail['data'], mimetype=thumbnail['mimetype'])
|
return send_image(thumbnail)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/series/<int:series_id>')
|
@app.route('/api/series/<int:series_id>')
|
||||||
|
@ -67,7 +67,7 @@ def 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):
|
def get_volume_cover_thumbnail(volume_id):
|
||||||
thumbnail = db.get_volume_cover_thumbnail(volume_id)
|
thumbnail = db.get_volume_cover_thumbnail(volume_id)
|
||||||
return send_file_with_etag(thumbnail['data'], mimetype=thumbnail['mimetype'])
|
return send_image(thumbnail)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/volume/<int:volume_id>')
|
@app.route('/api/volume/<int:volume_id>')
|
||||||
|
@ -78,8 +78,7 @@ def 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):
|
def get_volume_page(volume_id, page_number):
|
||||||
page = db.get_volume_page(volume_id, page_number)
|
page = db.get_volume_page(volume_id, page_number)
|
||||||
response = send_file_with_etag(page['data'], etag=page['etag'], mimetype=page['mimetype'])
|
return send_image(page)
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Reference in a new issue