diff --git a/cista/app.py b/cista/app.py index 9a98c61..e646fe4 100644 --- a/cista/app.py +++ b/cista/app.py @@ -11,7 +11,7 @@ from wsgiref.handlers import format_date_time import brotli import sanic.helpers from blake3 import blake3 -from sanic import Blueprint, Sanic, empty, raw +from sanic import Blueprint, Sanic, empty, raw, redirect from sanic.exceptions import Forbidden, NotFound from sanic.log import logger from stream_zip import ZIP_AUTO, stream_zip @@ -198,6 +198,12 @@ async def wwwroot(req, path=""): return raw(data, headers=headers) +@app.route("/favicon.ico", methods=["GET", "HEAD"]) +async def favicon(req): + # Browsers keep asking for it when viewing files (not HTML with icon link) + return redirect("/assets/logo-97d1d7eb.svg", status=308) + + def get_files(wanted: set) -> list[tuple[PurePosixPath, Path]]: loc = PurePosixPath() idx = 0 diff --git a/cista/preview.py b/cista/preview.py index 395b493..e456599 100644 --- a/cista/preview.py +++ b/cista/preview.py @@ -1,10 +1,13 @@ import asyncio import io +import mimetypes import urllib.parse from pathlib import PurePosixPath from urllib.parse import unquote from wsgiref.handlers import format_date_time +import av +import av.datasets import fitz # PyMuPDF from PIL import Image from sanic import Blueprint, empty, raw @@ -54,6 +57,8 @@ async def preview(req, path): def dispatch(path, quality, maxsize, maxzoom): if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"): return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom) + if mimetypes.guess_type(path.name)[0].startswith("video/"): + return process_video(path, quality=quality, maxsize=maxsize) return process_image(path, quality=quality, maxsize=maxsize) @@ -86,3 +91,24 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat) return pix.pil_tobytes(format="webp", quality=quality, method=4) + + +def process_video(path, *, maxsize, quality): + with av.open(str(path)) as container: + stream = container.streams.video[0] + rotation = ( + stream.side_data + and stream.side_data.get(av.stream.SideData.DISPLAYMATRIX) + or 0 + ) + stream.codec_context.skip_frame = "NONKEY" + container.seek(container.duration // 8) + frame = next(container.decode(stream)) + img = frame.to_image() + + img.thumbnail((maxsize, maxsize)) + imgdata = io.BytesIO() + if rotation: + img = img.rotate(rotation, expand=True) + img.save(imgdata, format="webp", quality=quality, method=4) + return imgdata.getvalue() diff --git a/frontend/src/components/MediaPreview.vue b/frontend/src/components/MediaPreview.vue index beb6ecd..ba5a2e0 100644 --- a/frontend/src/components/MediaPreview.vue +++ b/frontend/src/components/MediaPreview.vue @@ -2,7 +2,7 @@ - + diff --git a/pyproject.toml b/pyproject.toml index 3a028de..77d412c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "natsort", "pathvalidate", "pillow", + "pyav", "pyjwt", "pymupdf", "sanic",