Skip to content

Commit 8f3d9cb

Browse files
committed
remux matroska files on request
1 parent 85c1cdc commit 8f3d9cb

File tree

1 file changed

+66
-18
lines changed

1 file changed

+66
-18
lines changed

extra/szurubooru-readonly-api.py

+66-18
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import magic
1919
import eyed3
2020
import aiosqlite
21-
from quart import Quart, request, send_file as quart_send_file, redirect
21+
from quart import Quart, request, send_file as quart_send_file, redirect, make_response
2222
from quart.ctx import copy_current_app_context
2323
from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError
2424

@@ -453,18 +453,54 @@ async def fetch_file_local_path(file_id: int) -> Optional[str]:
453453
return path
454454

455455

456+
async def transcode_path(file_id, local_path, mimetype):
457+
extension = get_extension(mimetype.transcode_to)
458+
transcodes_folder = Path("/tmp") / "awtfdb-transcodes"
459+
transcodes_folder.mkdir(exist_ok=True)
460+
target_path = transcodes_folder / f"{file_id}{extension}"
461+
462+
if not target_path.exists():
463+
cmdline = f"ffmpeg -y -i {shlex.quote(local_path)} -movflags +empty_moov -movflags +frag_keyframe -c:v copy {shlex.quote(str(target_path))}"
464+
log.info("transcoding with cmdline %r", cmdline)
465+
process = await asyncio.create_subprocess_shell(
466+
cmdline,
467+
stdout=asyncio.subprocess.PIPE,
468+
stderr=asyncio.subprocess.PIPE,
469+
)
470+
471+
out, err = await process.communicate()
472+
out, err = out.decode(), err.decode()
473+
log.info("out: %s, err: %s", out, err)
474+
if process.returncode != 0:
475+
log.warn(
476+
"ffmpeg (thumbnailer) returned non-zero exit code %d",
477+
process.returncode,
478+
)
479+
raise RuntimeError("ffmpeg failed")
480+
481+
return target_path
482+
483+
456484
@app.get("/_awtfdb_content/<file_id>")
457-
async def content(file_id: int):
485+
async def content(file_id: str):
458486
file_local_path = await fetch_file_local_path(file_id)
459487
if not file_local_path:
460488
return "", 404
461489

462490
mimetype = fetch_mimetype(file_local_path)
463-
nginx_host = os.environ.get("NGINX")
464-
if nginx_host:
465-
return redirect(f"http://{nginx_host}/{file_local_path}")
491+
492+
if mimetype.transcode_to:
493+
log.info("requested transcode %r", mimetype)
494+
request.timeout = None
495+
target_path = await transcode_path(file_id, file_local_path, mimetype)
496+
log.info("sending %s", target_path)
497+
return await send_file(target_path, mimetype=mimetype.target)
466498
else:
467-
return await send_file(file_local_path, mimetype=mimetype)
499+
nginx_host = os.environ.get("NGINX")
500+
if nginx_host:
501+
return redirect(f"http://{nginx_host}/{file_local_path}")
502+
else:
503+
return await send_file(file_local_path, mimetype=mimetype.target)
468504

469505

470506
def blocking_thumbnail_image(path, thumbnail_path, size):
@@ -592,7 +628,7 @@ def get_extension(mimetype):
592628
return MIME_EXTENSION_MAPPING[mimetype]
593629

594630

595-
MIME_REMAPPING = {"video/x-matroska": "video/mkv"}
631+
TRANSCODE = {"video/x-matroska": "video/mp4"}
596632
MIME_OPTIMIZATION = {
597633
".jpg": "image/jpeg",
598634
".jpeg": "image/jpeg",
@@ -602,15 +638,26 @@ def get_extension(mimetype):
602638
}
603639

604640

641+
@dataclass
642+
class Mimetype:
643+
raw: str
644+
transcode_to: Optional[str] = None
645+
646+
@property
647+
def target(self):
648+
return self.transcode_to or self.raw
649+
650+
605651
def fetch_mimetype(file_path: str):
606652
mimetype = app.file_cache.mime_type.get(file_path)
607653
if not mimetype:
608654
path = Path(file_path)
609655
if path.suffix in MIME_OPTIMIZATION:
610-
mimetype = MIME_OPTIMIZATION[path.suffix]
656+
mimetype = Mimetype(MIME_OPTIMIZATION[path.suffix])
611657
else:
612-
mimetype = magic.from_file(file_path, mime=True)
613-
mimetype = MIME_REMAPPING.get(mimetype, mimetype)
658+
mimetype = Mimetype(magic.from_file(file_path, mime=True))
659+
660+
mimetype.transcode_to = TRANSCODE.get(mimetype.raw)
614661
app.file_cache.mime_type[file_path] = mimetype
615662
return mimetype
616663

@@ -673,7 +720,8 @@ async def _thumbnail_wrapper(semaphore, function, local_path, thumb_path):
673720
return await function(local_path, thumb_path)
674721

675722

676-
async def submit_thumbnail(file_id, mimetype, file_local_path, thumbnail_path):
723+
async def submit_thumbnail(file_id, mimetype_packed, file_local_path, thumbnail_path):
724+
mimetype = mimetype_packed.target
677725
if mimetype.startswith("image/"):
678726
thumbnailing_function = thumbnail_given_path
679727
semaphore = app.image_thumbnail_semaphore
@@ -725,8 +773,8 @@ async def thumbnail(file_id: int):
725773
return "", 404
726774

727775
mimetype = fetch_mimetype(file_local_path)
728-
extension = get_extension(mimetype)
729-
log.info("thumbnailing mime %s ext %r", mimetype, extension)
776+
extension = get_extension(mimetype.target)
777+
log.info("thumbnailing mime %s ext %r", mimetype.target, extension)
730778
assert extension is not None
731779

732780
thumbnail_path = THUMBNAIL_FOLDER / f"{file_id}{extension}"
@@ -1017,19 +1065,19 @@ async def fetch_file_entity(
10171065
return None
10181066

10191067
file_mime = fetch_mimetype(file_local_path)
1020-
returned_file["mimeType"] = file_mime
1068+
returned_file["mimeType"] = file_mime.target
10211069

10221070
if "type" in fields:
10231071
file_type = app.file_cache.file_type.get(file_id)
10241072
if not file_type:
1025-
if file_mime.startswith("image/"):
1073+
if file_mime.raw.startswith("image/"):
10261074
file_type = "image"
1027-
if file_mime == "image/gif":
1075+
if file_mime.raw == "image/gif":
10281076
file_type = "animation"
10291077

1030-
elif file_mime.startswith("video/"):
1078+
elif file_mime.raw.startswith("video/"):
10311079
file_type = "video"
1032-
elif file_mime.startswith("audio/"):
1080+
elif file_mime.raw.startswith("audio/"):
10331081
file_type = "audio"
10341082
else:
10351083
file_type = "image"

0 commit comments

Comments
 (0)