hlassiege/darylpublic Fork
⑂ e615554
Commits
⬇ Clone ▾
git clone https://git.rickub.com/hlassiege/daryl.git
git clone ssh://git@rickub.com/hlassiege/daryl.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat(api): serve a thumbnail for each rush

A frame taken just after the start of the rush, generated once with
ffmpeg and cached in the project's derush folder.
hlassiege committed 2026-10-12T10:27:16+02:00 Browse files
e615554 parent: c821b56
modified backend/studio/modules/projects/api.py +13 -0
@@ -863,6 +863,19 @@ def rush_media(pid: str, rush_id: str):
863863 return FileResponse(rush["path"])
864864
865865
866+@router.get("/{pid}/rushes/{rush_id}/thumb")
867+def rush_thumb(pid: str, rush_id: str):
868+ """La vignette du rush dans la médiathèque : une image prise au début, calculée une fois puis gardée."""
869+ _check(pid)
870+ rush = next((r for r in service.load(pid)["rushes"] if r["id"] == rush_id), None)
871+ if not rush or not Path(rush["path"]).is_file():
872+ raise HTTPException(404, "rush introuvable")
873+ try:
874+ return FileResponse(runtime.rush_thumb(pid, rush), headers={"Cache-Control": "max-age=86400"})
875+ except RuntimeError as e:
876+ raise HTTPException(500, str(e)) from e
877+
878+
866879 @router.get("/{pid}/rushes/{rush_id}/peaks")
867880 def rush_peaks(pid: str, rush_id: str):
868881 """La forme d'onde du rush (une crête 0-100 par 1/RUSH_PEAKS_RATE s), calculée une fois puis gardée."""
@@ -863,6 +863,19 @@ def rush_media(pid: str, rush_id: str):
863 return FileResponse(rush["path"])863 return FileResponse(rush["path"])
864 864
865 865
866+@router.get("/{pid}/rushes/{rush_id}/thumb")
867+def rush_thumb(pid: str, rush_id: str):
868+ """La vignette du rush dans la médiathèque : une image prise au début, calculée une fois puis gardée."""
869+ _check(pid)
870+ rush = next((r for r in service.load(pid)["rushes"] if r["id"] == rush_id), None)
871+ if not rush or not Path(rush["path"]).is_file():
872+ raise HTTPException(404, "rush introuvable")
873+ try:
874+ return FileResponse(runtime.rush_thumb(pid, rush), headers={"Cache-Control": "max-age=86400"})
875+ except RuntimeError as e:
876+ raise HTTPException(500, str(e)) from e
877+
878+
866 @router.get("/{pid}/rushes/{rush_id}/peaks")879 @router.get("/{pid}/rushes/{rush_id}/peaks")
867 def rush_peaks(pid: str, rush_id: str):880 def rush_peaks(pid: str, rush_id: str):
868 """La forme d'onde du rush (une crête 0-100 par 1/RUSH_PEAKS_RATE s), calculée une fois puis gardée."""881 """La forme d'onde du rush (une crête 0-100 par 1/RUSH_PEAKS_RATE s), calculée une fois puis gardée."""
modified backend/studio/modules/projects/runtime.py +19 -0
@@ -231,6 +231,25 @@ def rush_levels(pid: str, rush: dict) -> list[int]:
231231 return levels
232232
233233
234+def rush_thumb(pid: str, rush: dict) -> Path:
235+ """Une image du rush, un peu après le début (la première est souvent noire ou floue)."""
236+ path = Path(rush["path"])
237+ key = hashlib.sha1(f"{path}:{path.stat().st_mtime}".encode()).hexdigest()[:10]
238+ dest = project_dir(pid) / service.DERUSH_DIR / f"thumb-{rush['id']}-{key}.jpg"
239+ if dest.is_file():
240+ return dest
241+ dest.parent.mkdir(parents=True, exist_ok=True)
242+ at = min(1.0, (rush.get("duration") or 0) / 3)
243+ proc = subprocess.run([tool("ffmpeg"), "-y", "-v", "error", "-ss", f"{at:.2f}", "-i", str(path), "-frames:v", "1",
244+ "-vf", "scale=320:-2", "-q:v", "5", str(dest)], capture_output=True)
245+ if proc.returncode != 0 or not dest.is_file():
246+ raise RuntimeError(f"ffmpeg : {proc.stderr.decode(errors='replace')[-300:]}")
247+ for old in dest.parent.glob(f"thumb-{rush['id']}-*.jpg"):
248+ if old != dest:
249+ old.unlink(missing_ok=True)
250+ return dest
251+
252+
234253 def rush_peaks(pid: str, rush: dict) -> list[int]:
235254 """La forme d'onde à afficher (0-100) : rapportée au haut de la voix, pas au pic le plus fort,
236255 pour qu'un claquement isolé n'écrase pas tout le reste."""
@@ -231,6 +231,25 @@ def rush_levels(pid: str, rush: dict) -> list[int]:
231 return levels231 return levels
232 232
233 233
234+def rush_thumb(pid: str, rush: dict) -> Path:
235+ """Une image du rush, un peu après le début (la première est souvent noire ou floue)."""
236+ path = Path(rush["path"])
237+ key = hashlib.sha1(f"{path}:{path.stat().st_mtime}".encode()).hexdigest()[:10]
238+ dest = project_dir(pid) / service.DERUSH_DIR / f"thumb-{rush['id']}-{key}.jpg"
239+ if dest.is_file():
240+ return dest
241+ dest.parent.mkdir(parents=True, exist_ok=True)
242+ at = min(1.0, (rush.get("duration") or 0) / 3)
243+ proc = subprocess.run([tool("ffmpeg"), "-y", "-v", "error", "-ss", f"{at:.2f}", "-i", str(path), "-frames:v", "1",
244+ "-vf", "scale=320:-2", "-q:v", "5", str(dest)], capture_output=True)
245+ if proc.returncode != 0 or not dest.is_file():
246+ raise RuntimeError(f"ffmpeg : {proc.stderr.decode(errors='replace')[-300:]}")
247+ for old in dest.parent.glob(f"thumb-{rush['id']}-*.jpg"):
248+ if old != dest:
249+ old.unlink(missing_ok=True)
250+ return dest
251+
252+
234 def rush_peaks(pid: str, rush: dict) -> list[int]:253 def rush_peaks(pid: str, rush: dict) -> list[int]:
235 """La forme d'onde à afficher (0-100) : rapportée au haut de la voix, pas au pic le plus fort,254 """La forme d'onde à afficher (0-100) : rapportée au haut de la voix, pas au pic le plus fort,
236 pour qu'un claquement isolé n'écrase pas tout le reste."""255 pour qu'un claquement isolé n'écrase pas tout le reste."""