18
18
import magic
19
19
import eyed3
20
20
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
22
22
from quart .ctx import copy_current_app_context
23
23
from PIL import Image , ImageDraw , ImageFont , UnidentifiedImageError
24
24
@@ -453,18 +453,54 @@ async def fetch_file_local_path(file_id: int) -> Optional[str]:
453
453
return path
454
454
455
455
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
+
456
484
@app .get ("/_awtfdb_content/<file_id>" )
457
- async def content (file_id : int ):
485
+ async def content (file_id : str ):
458
486
file_local_path = await fetch_file_local_path (file_id )
459
487
if not file_local_path :
460
488
return "" , 404
461
489
462
490
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 )
466
498
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 )
468
504
469
505
470
506
def blocking_thumbnail_image (path , thumbnail_path , size ):
@@ -592,7 +628,7 @@ def get_extension(mimetype):
592
628
return MIME_EXTENSION_MAPPING [mimetype ]
593
629
594
630
595
- MIME_REMAPPING = {"video/x-matroska" : "video/mkv " }
631
+ TRANSCODE = {"video/x-matroska" : "video/mp4 " }
596
632
MIME_OPTIMIZATION = {
597
633
".jpg" : "image/jpeg" ,
598
634
".jpeg" : "image/jpeg" ,
@@ -602,15 +638,26 @@ def get_extension(mimetype):
602
638
}
603
639
604
640
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
+
605
651
def fetch_mimetype (file_path : str ):
606
652
mimetype = app .file_cache .mime_type .get (file_path )
607
653
if not mimetype :
608
654
path = Path (file_path )
609
655
if path .suffix in MIME_OPTIMIZATION :
610
- mimetype = MIME_OPTIMIZATION [path .suffix ]
656
+ mimetype = Mimetype ( MIME_OPTIMIZATION [path .suffix ])
611
657
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 )
614
661
app .file_cache .mime_type [file_path ] = mimetype
615
662
return mimetype
616
663
@@ -673,7 +720,8 @@ async def _thumbnail_wrapper(semaphore, function, local_path, thumb_path):
673
720
return await function (local_path , thumb_path )
674
721
675
722
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
677
725
if mimetype .startswith ("image/" ):
678
726
thumbnailing_function = thumbnail_given_path
679
727
semaphore = app .image_thumbnail_semaphore
@@ -725,8 +773,8 @@ async def thumbnail(file_id: int):
725
773
return "" , 404
726
774
727
775
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 )
730
778
assert extension is not None
731
779
732
780
thumbnail_path = THUMBNAIL_FOLDER / f"{ file_id } { extension } "
@@ -1017,19 +1065,19 @@ async def fetch_file_entity(
1017
1065
return None
1018
1066
1019
1067
file_mime = fetch_mimetype (file_local_path )
1020
- returned_file ["mimeType" ] = file_mime
1068
+ returned_file ["mimeType" ] = file_mime . target
1021
1069
1022
1070
if "type" in fields :
1023
1071
file_type = app .file_cache .file_type .get (file_id )
1024
1072
if not file_type :
1025
- if file_mime .startswith ("image/" ):
1073
+ if file_mime .raw . startswith ("image/" ):
1026
1074
file_type = "image"
1027
- if file_mime == "image/gif" :
1075
+ if file_mime . raw == "image/gif" :
1028
1076
file_type = "animation"
1029
1077
1030
- elif file_mime .startswith ("video/" ):
1078
+ elif file_mime .raw . startswith ("video/" ):
1031
1079
file_type = "video"
1032
- elif file_mime .startswith ("audio/" ):
1080
+ elif file_mime .raw . startswith ("audio/" ):
1033
1081
file_type = "audio"
1034
1082
else :
1035
1083
file_type = "image"
0 commit comments