import os import io import shutil import sqlite3 from pathlib import Path from fastapi import APIRouter, UploadFile, File, Query, HTTPException from fastapi.responses import FileResponse, JSONResponse from storage.files.file_manager import FileManager from storage.common import validate_token router = APIRouter(prefix="/media", tags=["Media Manager"]) MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") VALID_VERSIONS = ("Salamandra", "MoE") VALID_SUBTYPES = ("Original", "HITL OK", "HITL Test") AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"] @router.delete("/clear_media", tags=["Media Manager"]) def clear_media(token: str = Query(..., description="Token required for authorization")): """ Delete all contents of the /data/media folder. Steps: - Validate the token. - Ensure the folder exists. - Delete all files and subfolders inside /data/media. - Return a JSON response confirming the deletion. Warning: This will remove all stored videos, clips, and cast CSV files. """ validate_token(token) if not MEDIA_ROOT.exists() or not MEDIA_ROOT.is_dir(): raise HTTPException(status_code=404, detail="/data/media folder does not exist") # Delete contents for item in MEDIA_ROOT.iterdir(): try: if item.is_dir(): shutil.rmtree(item) else: item.unlink() except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to delete {item}: {e}") return {"status": "ok", "message": "All media files deleted successfully"} @router.post("/upload_cast_csv", tags=["Media Manager"]) async def upload_cast_csv( sha1: str, cast_file: UploadFile = File(...), token: str = Query(..., description="Token required for authorization") ): """ Upload a cast CSV file for a specific video identified by its SHA-1. The CSV will be stored under: /data/media//cast/cast.csv Steps: - Validate the token. - Ensure /data/media/ exists. - Create /cast folder if missing. - Save the CSV file inside /cast. """ validate_token(token) base_folder = MEDIA_ROOT / sha1 if not base_folder.exists() or not base_folder.is_dir(): raise HTTPException(status_code=404, detail="SHA1 folder not found") cast_folder = base_folder / "cast" cast_folder.mkdir(parents=True, exist_ok=True) final_path = cast_folder / "cast.csv" file_bytes = await cast_file.read() save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path) if not save_result["operation_success"]: raise HTTPException(status_code=500, detail=save_result["error"]) return JSONResponse( status_code=200, content={"status": "ok", "saved_to": str(final_path)} ) @router.get("/download_cast_csv", tags=["Media Manager"]) def download_cast_csv( sha1: str, token: str = Query(..., description="Token required for authorization") ): """ Download the cast CSV for a specific video identified by its SHA-1. The CSV is expected under: /data/media//cast/cast.csv Steps: - Validate the token. - Ensure /data/media/ and /cast exist. - Return the CSV as a FileResponse. - Raise 404 if any folder or file is missing. """ MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") validate_token(token) base_folder = MEDIA_ROOT / sha1 cast_folder = base_folder / "cast" csv_path = cast_folder / "cast.csv" if not base_folder.exists() or not base_folder.is_dir(): raise HTTPException(status_code=404, detail="SHA1 folder not found") if not cast_folder.exists() or not cast_folder.is_dir(): raise HTTPException(status_code=404, detail="Cast folder not found") if not csv_path.exists() or not csv_path.is_file(): raise HTTPException(status_code=404, detail="Cast CSV not found") # Convert to relative path for FileManager relative_path = csv_path.relative_to(MEDIA_ROOT) handler = file_manager.get_file(relative_path) if handler is None: raise HTTPException(status_code=404, detail="Cast CSV not accessible") handler.close() return FileResponse( path=csv_path, media_type="text/csv", filename="cast.csv" ) @router.post("/upload_original_video", tags=["Media Manager"]) async def upload_video( video: UploadFile = File(...), token: str = Query(..., description="Token required for authorization") ): """ Saves an uploaded video by hashing it with SHA1 and placing it under: /data/media//clip/ Behavior: - Compute SHA1 of the uploaded video. - Ensure folder structure exists. - Delete any existing .mp4 files under /clip. - Save the uploaded video in the clip folder. """ MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") validate_token(token) # Read content into memory (needed to compute hash twice) file_bytes = await video.read() # Create an in-memory file handler for hashing file_handler = io.BytesIO(file_bytes) # Compute SHA1 try: sha1 = file_manager.compute_sha1(file_handler) except Exception as exc: raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}") # Ensure /data/media exists MEDIA_ROOT.mkdir(parents=True, exist_ok=True) # Path: /data/media/ video_root = MEDIA_ROOT / sha1 video_root.mkdir(parents=True, exist_ok=True) # Path: /data/media//clip clip_dir = video_root / "clip" clip_dir.mkdir(parents=True, exist_ok=True) # Delete old MP4 files try: for old_mp4 in clip_dir.glob("*.mp4"): old_mp4.unlink() except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to delete old videos: {exc}") # Save new video path final_path = clip_dir / video.filename # Save file save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path) if not save_result["operation_success"]: raise HTTPException(status_code=500, detail=save_result["error"]) return JSONResponse( status_code=200, content={ "status": "ok", "sha1": sha1, "saved_to": str(final_path) } ) @router.get("/download_original_video", tags=["Media Manager"]) def download_video( sha1: str, token: str = Query(..., description="Token required for authorization") ): """ Download a stored video by its SHA-1 directory name. This endpoint looks for a video stored under the path: /data/media//clip/ and returns the first MP4 file found in that folder. The method performs the following steps: - Checks if the SHA-1 folder exists inside the media root. - Validates that the "clip" subfolder exists. - Searches for the first .mp4 file inside the clip folder. - Uses the FileManager.get_file method to ensure the file is accessible. - Returns the video directly as a FileResponse. Parameters ---------- sha1 : str The SHA-1 hash corresponding to the directory where the video is stored. Returns ------- FileResponse A streaming response containing the MP4 video. Raises ------ HTTPException - 404 if the SHA-1 folder does not exist. - 404 if the clip folder is missing. - 404 if no MP4 files are found. - 404 if the file cannot be retrieved using FileManager. """ MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") validate_token(token) sha1_folder = MEDIA_ROOT / sha1 clip_folder = sha1_folder / "clip" if not sha1_folder.exists() or not sha1_folder.is_dir(): raise HTTPException(status_code=404, detail="SHA1 folder not found") if not clip_folder.exists() or not clip_folder.is_dir(): raise HTTPException(status_code=404, detail="Clip folder not found") # Find first MP4 file mp4_files = list(clip_folder.glob("*.mp4")) if not mp4_files: raise HTTPException(status_code=404, detail="No MP4 files found") video_path = mp4_files[0] # Convert to relative path for FileManager relative_path = video_path.relative_to(MEDIA_ROOT) handler = file_manager.get_file(relative_path) if handler is None: raise HTTPException(status_code=404, detail="Video not accessible") handler.close() return FileResponse( path=video_path, media_type="video/mp4", filename=video_path.name ) @router.delete("/delete_video", tags=["Media Manager"]) def delete_media( hash: str, token: str = Query(..., description="Token required for authorization") ): """ Delete a stored media directory by its hash. This endpoint removes the folder located at: /data/media/ The method performs the following steps: - Validates the authorization token. - Checks if the hash folder exists inside the media root. - Deletes the entire directory recursively. Parameters ---------- hash : str The hash corresponding to the directory to be deleted. Returns ------- dict A confirmation message indicating successful deletion. Raises ------ HTTPException - 404 if the hash folder does not exist. - 500 if the folder cannot be deleted. """ MEDIA_ROOT = Path("/data/media") validate_token(token) hash_folder = MEDIA_ROOT / hash if not hash_folder.exists() or not hash_folder.is_dir(): raise HTTPException(status_code=404, detail="Media folder not found") try: shutil.rmtree(hash_folder) except Exception as e: raise HTTPException( status_code=500, detail=f"Failed to delete media folder: {str(e)}" ) return { "status": "ok", "message": f"Media folder '{hash}' deleted successfully" } @router.post("/upload_video_ad", tags=["Media Manager"]) async def upload_video_ad( sha1: str = Query(..., description="SHA1 associated to the media folder"), version: str = Query(..., description="Version: Salamandra or MoE"), subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"), video: UploadFile = File(...), token: str = Query(..., description="Token required for authorization") ): validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") if subtype not in VALID_SUBTYPES: raise HTTPException(status_code=400, detail="Invalid subtype") MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) subtype_dir = MEDIA_ROOT / sha1 / version / subtype subtype_dir.mkdir(parents=True, exist_ok=True) for f in subtype_dir.glob("*.mp4"): f.unlink() file_bytes = await video.read() final_path = subtype_dir / video.filename save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path) if not save_result["operation_success"]: raise HTTPException(status_code=500, detail=save_result["error"]) return { "status": "ok", "sha1": sha1, "version": version, "subtype": subtype, "saved_to": str(final_path) } @router.get("/download_video_ad", tags=["Media Manager"]) def download_video_ad( sha1: str, version: str, subtype: str, token: str = Query(..., description="Token required for authorization") ): validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") if subtype not in VALID_SUBTYPES: raise HTTPException(status_code=400, detail="Invalid subtype") MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) subtype_dir = MEDIA_ROOT / sha1 / version / subtype if not subtype_dir.exists() or not subtype_dir.is_dir(): raise HTTPException(status_code=404, detail="Version/subtype folder not found") mp4_files = list(subtype_dir.glob("*.mp4")) if not mp4_files: raise HTTPException(status_code=404, detail="No MP4 files found") video_path = mp4_files[0] relative_path = video_path.relative_to(MEDIA_ROOT) handler = file_manager.get_file(relative_path) if handler is None: raise HTTPException(status_code=404, detail="Video not accessible") handler.close() return FileResponse( path=video_path, media_type="video/mp4", filename=video_path.name ) @router.get("/list_original_videos", tags=["Media Manager"]) def list_all_videos( token: str = Query(..., description="Token required for authorization") ): """ List all videos stored under /data/media. For each SHA1 folder, the endpoint returns: - sha1: folder name - video_files: list of mp4 files inside /clip - latest_video: the most recently modified mp4 - video_count: total number of mp4 files Notes: - Videos may not have a /clip folder. - SHA1 folders without mp4 files are still returned. """ validate_token(token) results = [] # If media root does not exist, return empty list if not MEDIA_ROOT.exists(): return [] for sha1_dir in MEDIA_ROOT.iterdir(): if not sha1_dir.is_dir(): continue # skip non-folders clip_dir = sha1_dir / "clip" videos = [] latest_video = None if clip_dir.exists() and clip_dir.is_dir(): mp4_files = list(clip_dir.glob("*.mp4")) # Sort by modification time (newest first) mp4_files.sort(key=lambda f: f.stat().st_mtime, reverse=True) videos = [f.name for f in mp4_files] if mp4_files: latest_video = mp4_files[0].name results.append({ "sha1": sha1_dir.name, "video_name": latest_video }) return results @router.post("/upload_original_audio", tags=["Media Manager"]) async def upload_audio( audio: UploadFile = File(...), token: str = Query(..., description="Token required for authorization") ): """ Saves an uploaded audio file by hashing it with SHA1 and placing it under: /data/media//audio/ Behavior: - Compute SHA1 of the uploaded audio. - Ensure folder structure exists. - Delete any existing audio files under /audio. - Save the uploaded audio in the audio folder. """ MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") validate_token(token) # Read content into memory (needed to compute hash twice) file_bytes = await audio.read() # Create an in-memory file handler for hashing file_handler = io.BytesIO(file_bytes) # Compute SHA1 try: sha1 = file_manager.compute_sha1(file_handler) except Exception as exc: raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}") # Ensure /data/media exists MEDIA_ROOT.mkdir(parents=True, exist_ok=True) # Path: /data/media/ audio_root = MEDIA_ROOT / sha1 audio_root.mkdir(parents=True, exist_ok=True) # Path: /data/media//audio audio_dir = audio_root / "audio" audio_dir.mkdir(parents=True, exist_ok=True) # Delete old audio files AUDIO_EXTENSIONS = ("*.mp3", "*.wav", "*.m4a", "*.aac", "*.ogg", "*.flac") try: for pattern in AUDIO_EXTENSIONS: for old_audio in audio_dir.glob(pattern): old_audio.unlink() except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}") # Final save path final_path = audio_dir / audio.filename # Save file save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path) if not save_result["operation_success"]: raise HTTPException(status_code=500, detail=save_result["error"]) return JSONResponse( status_code=200, content={ "status": "ok", "sha1": sha1, "saved_to": str(final_path) } ) @router.get("/download_original_audio", tags=["Media Manager"]) def download_audio( sha1: str, token: str = Query(..., description="Token required for authorization") ): """ Download a stored audio file by its SHA-1 directory name. This endpoint looks for audio stored under the path: /data/media//audio/ and returns the first audio file found in that folder. The method performs the following steps: - Checks if the SHA-1 folder exists inside the media root. - Validates that the "audio" subfolder exists. - Searches for the first supported audio file. - Uses FileManager.get_file to ensure the file is accessible. - Returns the audio file as a FileResponse. Parameters ---------- sha1 : str The SHA-1 hash corresponding to the directory where the audio is stored. Returns ------- FileResponse A streaming response containing the audio file. Raises ------ HTTPException - 404 if the SHA-1 folder does not exist. - 404 if the audio folder is missing. - 404 if no audio files are found. - 404 if the file cannot be retrieved using FileManager. """ MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) HF_TOKEN = os.getenv("HF_TOKEN") validate_token(token) sha1_folder = MEDIA_ROOT / sha1 audio_folder = sha1_folder / "audio" if not sha1_folder.exists() or not sha1_folder.is_dir(): raise HTTPException(status_code=404, detail="SHA1 folder not found") if not audio_folder.exists() or not audio_folder.is_dir(): raise HTTPException(status_code=404, detail="Audio folder not found") # Supported audio extensions AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"] audio_files = [] for pattern in AUDIO_EXTENSIONS: audio_files.extend(list(audio_folder.glob(pattern))) if not audio_files: raise HTTPException(status_code=404, detail="No audio files found") audio_path = audio_files[0] # Convert to relative path for FileManager relative_path = audio_path.relative_to(MEDIA_ROOT) handler = file_manager.get_file(relative_path) if handler is None: raise HTTPException(status_code=404, detail="Audio file not accessible") handler.close() # Guess media type based on extension (simple) media_type = "audio/" + audio_path.suffix.lstrip(".") return FileResponse( path=audio_path, media_type=media_type, filename=audio_path.name ) @router.get("/list_original_audios", tags=["Media Manager"]) def list_all_audios( token: str = Query(..., description="Token required for authorization") ): """ List all audio files stored under /data/media. For each SHA1 folder, the endpoint returns: - sha1: folder name - audio_files: list of audio files inside /audio - latest_audio: the most recently modified audio file - audio_count: total number of audio files Notes: - Folders may not have an /audio folder. - SHA1 folders without audio files are still returned. """ validate_token(token) results = [] MEDIA_ROOT = Path("/data/media") AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"] # If media root does not exist, return empty list if not MEDIA_ROOT.exists(): return [] for sha1_dir in MEDIA_ROOT.iterdir(): if not sha1_dir.is_dir(): continue # skip non-folders audio_dir = sha1_dir / "audio" audio_files = [] latest_audio = None if audio_dir.exists() and audio_dir.is_dir(): # Collect all audio files with supported extensions files = [] for pattern in AUDIO_EXTENSIONS: files.extend(list(audio_dir.glob(pattern))) # Sort by modification time (newest first) files.sort(key=lambda f: f.stat().st_mtime, reverse=True) audio_files = [f.name for f in files] if files: latest_audio = files[0].name results.append({ "sha1": sha1_dir.name, "audio_name": latest_audio, }) return results @router.post("/upload_audio_version", tags=["Media Manager"]) async def upload_audio_version( audio: UploadFile = File(...), sha1: str = Query(..., description="SHA1 of the video folder"), version: str = Query(..., description="Version: Salamandra or MoE"), token: str = Query(..., description="Token required for authorization") ): """Upload audio for a given version (Salamandra, MoE). This legacy endpoint keeps its path but now interprets the former `subtype` path component as `version`: - Target folder: /data/media/// - Deletes any previous audio files - Saves the new audio """ validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") MEDIA_ROOT = Path("/data/media") version_dir = MEDIA_ROOT / sha1 / version version_dir.mkdir(parents=True, exist_ok=True) # Delete old audio files try: for pattern in AUDIO_EXTENSIONS: for old_audio in version_dir.glob(pattern): old_audio.unlink() except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}") final_path = version_dir / audio.filename try: file_bytes = await audio.read() with open(final_path, "wb") as f: f.write(file_bytes) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}") return JSONResponse( status_code=200, content={ "status": "ok", "sha1": sha1, "version": version, "saved_to": str(final_path) } ) @router.get("/download_audio_version", tags=["Media Manager"]) def download_audio_version( sha1: str, version: str, token: str = Query(..., description="Token required for authorization") ): """Download the first audio file for a given version (Salamandra, MoE).""" validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") MEDIA_ROOT = Path("/data/media") version_dir = MEDIA_ROOT / sha1 / version if not version_dir.exists() or not version_dir.is_dir(): raise HTTPException(status_code=404, detail=f"{version} folder not found") # Find audio files audio_files = [] for pattern in AUDIO_EXTENSIONS: audio_files.extend(list(version_dir.glob(pattern))) if not audio_files: raise HTTPException(status_code=404, detail="No audio files found") audio_path = audio_files[0] return FileResponse( path=audio_path, media_type="audio/" + audio_path.suffix.lstrip("."), filename=audio_path.name ) @router.post("/upload_audio_ad", tags=["Media Manager"]) async def upload_audio_ad( audio: UploadFile = File(...), sha1: str = Query(..., description="SHA1 of the video folder"), version: str = Query(..., description="Version: Salamandra or MoE"), subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"), token: str = Query(..., description="Token required for authorization") ): validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") if subtype not in VALID_SUBTYPES: raise HTTPException(status_code=400, detail="Invalid subtype") MEDIA_ROOT = Path("/data/media") subtype_dir = MEDIA_ROOT / sha1 / version / subtype subtype_dir.mkdir(parents=True, exist_ok=True) try: for pattern in AUDIO_EXTENSIONS: for old_audio in subtype_dir.glob(pattern): old_audio.unlink() except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}") final_path = subtype_dir / audio.filename try: file_bytes = await audio.read() with open(final_path, "wb") as f: f.write(file_bytes) except Exception as exc: raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}") return JSONResponse( status_code=200, content={ "status": "ok", "sha1": sha1, "version": version, "subtype": subtype, "saved_to": str(final_path) } ) @router.get("/download_audio_ad", tags=["Media Manager"]) def download_audio_ad( sha1: str, version: str, subtype: str, token: str = Query(..., description="Token required for authorization") ): validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") if subtype not in VALID_SUBTYPES: raise HTTPException(status_code=400, detail="Invalid subtype") MEDIA_ROOT = Path("/data/media") subtype_dir = MEDIA_ROOT / sha1 / version / subtype if not subtype_dir.exists() or not subtype_dir.is_dir(): raise HTTPException(status_code=404, detail="Version/subtype folder not found") audio_files = [] for pattern in AUDIO_EXTENSIONS: audio_files.extend(list(subtype_dir.glob(pattern))) if not audio_files: raise HTTPException(status_code=404, detail="No audio files found") audio_path = audio_files[0] return FileResponse( path=audio_path, media_type="audio/" + audio_path.suffix.lstrip("."), filename=audio_path.name ) @router.get("/list_version_audios", tags=["Media Manager"]) def list_version_audios( sha1: str = Query(..., description="SHA1 of the video folder"), token: str = Query(..., description="Token required for authorization") ): """List the most recent audio file for each version (Salamandra, MoE) under /data/media/. Returns: - sha1: folder name - version: name of the version - audio_name: latest audio file or None """ validate_token(token) results = [] for version in VALID_VERSIONS: version_dir = MEDIA_ROOT / sha1 / version latest_audio = None if version_dir.exists() and version_dir.is_dir(): files = [] for pattern in AUDIO_EXTENSIONS: files.extend(list(version_dir.glob(pattern))) if files: # Sort by modification time (newest first) files.sort(key=lambda f: f.stat().st_mtime, reverse=True) latest_audio = files[0].name results.append({ "sha1": sha1, "version": version, "audio_name": latest_audio }) return results @router.post("/upload_version_video", tags=["Media Manager"]) async def upload_version_video( sha1: str = Query(..., description="SHA1 associated to the media folder"), version: str = Query(..., description="Version: Salamandra or MoE"), video: UploadFile = File(...), token: str = Query(..., description="Token required for authorization") ): """Upload a video to /data/media///. This legacy endpoint keeps its path but now interprets the former `subtype` path component as `version`. Steps: - Validate version. - Create version folder if missing. - Delete existing MP4 files. - Save new MP4. """ validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) version_dir = MEDIA_ROOT / sha1 / version version_dir.mkdir(parents=True, exist_ok=True) # Remove old mp4 files for f in version_dir.glob("*.mp4"): f.unlink() file_bytes = await video.read() final_path = version_dir / video.filename save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path) if not save_result["operation_success"]: raise HTTPException(status_code=500, detail=save_result["error"]) return { "status": "ok", "sha1": sha1, "version": version, "saved_to": str(final_path) } @router.get("/download_version_video", tags=["Media Manager"]) def download_version_video( sha1: str, version: str, token: str = Query(..., description="Token required for authorization") ): """Download the video stored under /data/media//. Returns the first MP4 found. """ validate_token(token) if version not in VALID_VERSIONS: raise HTTPException(status_code=400, detail="Invalid version") MEDIA_ROOT = Path("/data/media") file_manager = FileManager(MEDIA_ROOT) version_dir = MEDIA_ROOT / sha1 / version if not version_dir.exists() or not version_dir.is_dir(): raise HTTPException(status_code=404, detail="Version folder not found") mp4_files = list(version_dir.glob("*.mp4")) if not mp4_files: raise HTTPException(status_code=404, detail="No MP4 files found") video_path = mp4_files[0] relative_path = video_path.relative_to(MEDIA_ROOT) handler = file_manager.get_file(relative_path) if handler is None: raise HTTPException(status_code=404, detail="Video not accessible") handler.close() return FileResponse( path=video_path, media_type="video/mp4", filename=video_path.name ) @router.get("/list_version_videos", tags=["Media Manager"]) def list_version_videos( sha1: str, token: str = Query(..., description="Token required for authorization") ): """List the most recent .mp4 video for each version (Salamandra, MoE) inside /data/media/. Returns: - sha1 - version - video_name (latest mp4 or None) """ validate_token(token) MEDIA_ROOT = Path("/data/media") results = [] for version in VALID_VERSIONS: version_dir = MEDIA_ROOT / sha1 / version latest_video = None if version_dir.exists() and version_dir.is_dir(): files = list(version_dir.glob("*.mp4")) if files: files.sort(key=lambda f: f.stat().st_mtime, reverse=True) latest_video = files[0].name results.append({ "sha1": sha1, "version": version, "video_name": latest_video }) return results