This repository has been archived on 2020-11-22. You can view files and clone it, but cannot push or open issues/pull-requests.
mangareader/backend.py

322 lines
9.2 KiB
Python
Raw Permalink Normal View History

2019-07-04 18:06:44 +02:00
from PIL import Image
2019-08-07 22:35:22 +02:00
from disk_cache import DiskCache
2019-07-04 18:06:44 +02:00
from io import BytesIO
from mimetypes import types_map as mimetypes
2019-07-04 18:06:44 +02:00
from zipfile import ZipFile
from zlib import crc32
2019-07-04 18:06:44 +02:00
import os.path
import sqlite3
import werkzeug.exceptions as exceptions
2019-12-31 16:17:37 +01:00
DEFAULT_WEBP_QUALITY = 80
DEFAULT_WEBP_METHOD = 0
DEFAULT_WEBP_SIZE = 1908 # width of FullHD monitor without scroll bar in full screen
2019-12-31 16:17:37 +01:00
2020-02-15 15:41:11 +01:00
mimetypes[".webp"] = "image/webp"
2020-02-15 15:41:11 +01:00
if os.environ.get("DISK_CACHE", "1") == "0":
2019-08-07 22:35:22 +02:00
disk_cache = DiskCache(enabled=False)
else:
disk_cache = DiskCache()
thumbnail_cache = {}
2019-07-04 18:06:44 +02:00
# https://docs.python.org/3.7/library/sqlite3.html#sqlite3.Connection.row_factory
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
def filter_zip_filelist(filelist):
return [file for file in filelist if not file.is_dir()]
2019-12-31 16:17:37 +01:00
class BaseDB:
def __init__(self, webp_quality, webp_method, webp_size):
2019-12-31 15:19:56 +01:00
# lossy: 0-100 (used as quality)
# lossless: 101-201 (101 subtracted and used as quality)
if webp_quality > 100:
webp_lossless = True
webp_quality = webp_quality - 101
else:
webp_lossless = False
self.webp_config = {
2020-02-15 15:41:11 +01:00
"quality": webp_quality,
"method": webp_method,
"lossless": webp_lossless,
"size": webp_size,
2019-12-31 15:19:56 +01:00
}
2019-08-07 20:15:56 +02:00
2019-12-31 16:17:37 +01:00
def _generate_webp(self, fp, max_size=None):
if max_size is None:
2020-02-15 15:41:11 +01:00
max_size = tuple([self.webp_config["size"]] * 2)
2019-12-31 16:17:37 +01:00
image = Image.open(fp)
image.thumbnail(max_size)
image_buffer = BytesIO()
image.save(
image_buffer,
2020-02-15 15:41:11 +01:00
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"],
2019-12-31 16:17:37 +01:00
)
image_buffer.seek(0)
return image_buffer
def _generate_thumbnail(self, filepath):
if filepath not in thumbnail_cache:
thumbnail_buffer = self._generate_webp(filepath, max_size=(512, 512))
data = thumbnail_buffer.read()
etag = str(crc32(data))
thumbnail_cache[filepath] = {
2020-02-15 15:41:11 +01:00
"data_raw": data,
"etag": etag,
"mimetype": "image/webp",
2019-12-31 16:17:37 +01:00
}
thumbnail = thumbnail_cache[filepath]
2020-02-15 15:41:11 +01:00
thumbnail["buffer"] = BytesIO()
thumbnail["buffer"].write(thumbnail["data_raw"])
thumbnail["buffer"].seek(0)
2019-12-31 16:17:37 +01:00
return thumbnail
def _generate_page(self, page_buffer, volume, page):
page_buffer = self._generate_webp(page_buffer)
2020-02-15 15:41:11 +01:00
disk_cache.set(f"{volume}-{page}", page_buffer)
2019-12-31 16:17:37 +01:00
page_buffer.seek(0)
return page_buffer
class CalibreDB(BaseDB):
2020-02-15 15:41:11 +01:00
def __init__(
self,
path="metadata.db",
webp_quality=DEFAULT_WEBP_QUALITY,
webp_method=DEFAULT_WEBP_METHOD,
webp_size=DEFAULT_WEBP_SIZE,
):
2019-12-31 16:17:37 +01:00
super().__init__(
2020-02-15 15:41:11 +01:00
webp_quality=webp_quality, webp_method=webp_method, webp_size=webp_size
2019-12-31 16:17:37 +01:00
)
2020-02-15 15:41:11 +01:00
self.database_path = f"file:{path}?mode=ro"
2019-12-31 16:17:37 +01:00
2019-07-04 18:06:44 +02:00
def create_cursor(self):
conn = sqlite3.connect(self.database_path, uri=True)
conn.row_factory = dict_factory
return conn.cursor()
def get_series_list(self):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
series = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
series.id as id,
series.name as name,
count(*) as volumes
from
books,
books_series_link,
data,
series
where
books_series_link.series = series.id and
books_series_link.book = books.id and
books.id = data.book and
data.format = \'CBZ\'
group by series.name
having min(books.series_index)
2020-02-15 15:41:11 +01:00
"""
)
2019-07-04 18:06:44 +02:00
return series
def get_series_cover(self, series_id):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
first_volume = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
books.id
from
books,
books_series_link,
series
where
books_series_link.book = books.id and
books_series_link.series = series.id and
series.id = ?
group by series.name
having min(books.series_index)
2020-02-15 15:41:11 +01:00
""",
(str(series_id),),
).fetchone()
2019-07-04 18:06:44 +02:00
if first_volume is None:
raise exceptions.NotFound()
2020-02-15 15:41:11 +01:00
return self.get_volume_cover(first_volume["id"])
2019-07-04 18:06:44 +02:00
def get_series_cover_thumbnail(self, series_id):
2019-12-31 16:17:37 +01:00
return self._generate_thumbnail(self.get_series_cover(series_id))
2019-07-04 18:06:44 +02:00
def get_series_volumes(self, series_id):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
title = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
series.name
from
series
where
series.id = ?
2020-02-15 15:41:11 +01:00
""",
(str(series_id),),
).fetchone()["name"]
volumes = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
books.id,
books.title,
books.series_index as "index"
from
books,
books_series_link,
series
where
books_series_link.book = books.id and
books_series_link.series = series.id and
series.id = ?
order by books.series_index
2020-02-15 15:41:11 +01:00
""",
(str(series_id),),
).fetchall()
2019-07-04 18:06:44 +02:00
2020-02-15 15:41:11 +01:00
return {"title": title, "volumes": volumes}
2019-07-04 18:06:44 +02:00
def get_volume_cover(self, volume_id):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
volume = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
books.has_cover as has_cover,
books.path as path
from
books
where
books.id = ?
order by books.series_index
2020-02-15 15:41:11 +01:00
""",
(str(volume_id),),
).fetchone()
2019-07-04 18:06:44 +02:00
2020-02-15 15:41:11 +01:00
if volume["has_cover"]:
return volume["path"] + "/cover.jpg"
2019-07-04 18:06:44 +02:00
else:
raise exceptions.NotFound()
def get_volume_cover_thumbnail(self, volume_id):
2019-12-31 16:17:37 +01:00
return self._generate_thumbnail(self.get_volume_cover(volume_id))
2019-07-04 18:06:44 +02:00
def get_volume_filepath(self, volume_id):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
location = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
books.path as path,
data.name as filename,
lower(data.format) as extension
from
books,
data
where
data.book = books.id and
books.id = ?
2020-02-15 15:41:11 +01:00
""",
(str(volume_id),),
).fetchone()
2019-07-04 18:06:44 +02:00
if location is None:
raise exceptions.NotFound()
2020-02-15 15:41:11 +01:00
return (
location["path"] + "/" + location["filename"] + "." + location["extension"]
)
2019-07-04 18:06:44 +02:00
def get_volume_info(self, volume_id):
cursor = self.create_cursor()
2020-02-15 15:41:11 +01:00
volume_info = cursor.execute(
"""
2019-07-04 18:06:44 +02:00
select
books.title,
books_series_link.series
from
books,
books_series_link
where
books_series_link.book = books.id and
books.id = ?
2020-02-15 15:41:11 +01:00
""",
(str(volume_id),),
).fetchone()
2019-07-04 18:06:44 +02:00
2020-02-15 15:41:11 +01:00
volume_info["pages"] = self.get_volume_page_number(volume_id)
2019-07-04 18:06:44 +02:00
return volume_info
def get_volume_page_number(self, volume_id):
path = self.get_volume_filepath(volume_id)
2020-02-15 15:41:11 +01:00
with ZipFile(path, "r") as volume:
filelist = filter_zip_filelist(volume.filelist)
return len(filelist)
2019-07-04 18:06:44 +02:00
2019-12-22 17:29:00 +01:00
def get_volume_page(self, volume_id, page_number, original=False):
2019-07-04 18:06:44 +02:00
if page_number < 1:
raise exceptions.NotFound()
path = self.get_volume_filepath(volume_id)
2020-02-15 15:41:11 +01:00
with ZipFile(path, "r") as volume:
2019-07-04 18:06:44 +02:00
try:
filelist = filter_zip_filelist(volume.filelist)
zip_info = filelist[page_number - 1]
2019-07-04 18:06:44 +02:00
except IndexError:
raise exceptions.NotFound()
return None
page_filename = zip_info.filename
2019-08-07 20:15:56 +02:00
2019-12-31 16:17:37 +01:00
if original is True:
mimetype = mimetypes[os.path.splitext(page_filename)[1]]
page_buffer = BytesIO()
page_buffer.write(volume.read(page_filename))
page_buffer.seek(0)
else:
2020-02-15 15:41:11 +01:00
mimetype = "image/webp"
2019-08-07 20:15:56 +02:00
2019-08-07 22:35:22 +02:00
try:
2020-02-15 15:41:11 +01:00
page_buffer = disk_cache.get(f"{volume_id}-{page_number}")
2019-08-07 22:35:22 +02:00
except FileNotFoundError:
2019-12-31 16:17:37 +01:00
with volume.open(page_filename) as orig_page_buffer:
2020-02-15 15:41:11 +01:00
page_buffer = self._generate_page(
orig_page_buffer, volume_id, page_number
)
2019-07-04 18:06:44 +02:00
return {
2020-02-15 15:41:11 +01:00
"buffer": page_buffer,
"mimetype": mimetype,
"etag": str(zip_info.CRC),
2019-07-04 18:06:44 +02:00
}