VeuReu commited on
Commit
f3e7f3d
·
verified ·
1 Parent(s): 693ac0e

Update storage/media_routers.py

Browse files
Files changed (1) hide show
  1. storage/media_routers.py +1015 -963
storage/media_routers.py CHANGED
@@ -1,963 +1,1015 @@
1
- import os
2
- import io
3
- import shutil
4
-
5
- import sqlite3
6
-
7
- from pathlib import Path
8
-
9
- from fastapi import APIRouter, UploadFile, File, Query, HTTPException
10
- from fastapi.responses import FileResponse, JSONResponse
11
-
12
-
13
- from storage.files.file_manager import FileManager
14
- from storage.common import validate_token
15
-
16
- router = APIRouter(prefix="/media", tags=["Media Manager"])
17
- MEDIA_ROOT = Path("/data/media")
18
- file_manager = FileManager(MEDIA_ROOT)
19
- HF_TOKEN = os.getenv("HF_TOKEN")
20
- VALID_VERSIONS = ("Salamandra", "MoE")
21
- VALID_SUBTYPES = ("Original", "HITL OK", "HITL Test")
22
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
23
-
24
-
25
- @router.delete("/clear_media", tags=["Media Manager"])
26
- def clear_media(token: str = Query(..., description="Token required for authorization")):
27
- """
28
- Delete all contents of the /data/media folder.
29
-
30
- Steps:
31
- - Validate the token.
32
- - Ensure the folder exists.
33
- - Delete all files and subfolders inside /data/media.
34
- - Return a JSON response confirming the deletion.
35
-
36
- Warning: This will remove all stored videos, clips, and cast CSV files.
37
- """
38
- validate_token(token)
39
-
40
- if not MEDIA_ROOT.exists() or not MEDIA_ROOT.is_dir():
41
- raise HTTPException(status_code=404, detail="/data/media folder does not exist")
42
-
43
- # Delete contents
44
- for item in MEDIA_ROOT.iterdir():
45
- try:
46
- if item.is_dir():
47
- shutil.rmtree(item)
48
- else:
49
- item.unlink()
50
- except Exception as e:
51
- raise HTTPException(status_code=500, detail=f"Failed to delete {item}: {e}")
52
-
53
- return {"status": "ok", "message": "All media files deleted successfully"}
54
-
55
- @router.post("/upload_cast_csv", tags=["Media Manager"])
56
- async def upload_cast_csv(
57
- sha1: str,
58
- cast_file: UploadFile = File(...),
59
- token: str = Query(..., description="Token required for authorization")
60
- ):
61
- """
62
- Upload a cast CSV file for a specific video identified by its SHA-1.
63
-
64
- The CSV will be stored under:
65
- /data/media/<sha1>/cast/cast.csv
66
-
67
- Steps:
68
- - Validate the token.
69
- - Ensure /data/media/<sha1> exists.
70
- - Create /cast folder if missing.
71
- - Save the CSV file inside /cast.
72
- """
73
- validate_token(token)
74
-
75
- base_folder = MEDIA_ROOT / sha1
76
- if not base_folder.exists() or not base_folder.is_dir():
77
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
78
-
79
- cast_folder = base_folder / "cast"
80
- cast_folder.mkdir(parents=True, exist_ok=True)
81
-
82
- final_path = cast_folder / "cast.csv"
83
-
84
- file_bytes = await cast_file.read()
85
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
86
- if not save_result["operation_success"]:
87
- raise HTTPException(status_code=500, detail=save_result["error"])
88
-
89
- return JSONResponse(
90
- status_code=200,
91
- content={"status": "ok", "saved_to": str(final_path)}
92
- )
93
-
94
-
95
- @router.get("/download_cast_csv", tags=["Media Manager"])
96
- def download_cast_csv(
97
- sha1: str,
98
- token: str = Query(..., description="Token required for authorization")
99
- ):
100
- """
101
- Download the cast CSV for a specific video identified by its SHA-1.
102
-
103
- The CSV is expected under:
104
- /data/media/<sha1>/cast/cast.csv
105
-
106
- Steps:
107
- - Validate the token.
108
- - Ensure /data/media/<sha1> and /cast exist.
109
- - Return the CSV as a FileResponse.
110
- - Raise 404 if any folder or file is missing.
111
- """
112
- MEDIA_ROOT = Path("/data/media")
113
- file_manager = FileManager(MEDIA_ROOT)
114
- HF_TOKEN = os.getenv("HF_TOKEN")
115
- validate_token(token)
116
-
117
- base_folder = MEDIA_ROOT / sha1
118
- cast_folder = base_folder / "cast"
119
- csv_path = cast_folder / "cast.csv"
120
-
121
- if not base_folder.exists() or not base_folder.is_dir():
122
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
123
- if not cast_folder.exists() or not cast_folder.is_dir():
124
- raise HTTPException(status_code=404, detail="Cast folder not found")
125
- if not csv_path.exists() or not csv_path.is_file():
126
- raise HTTPException(status_code=404, detail="Cast CSV not found")
127
-
128
- # Convert to relative path for FileManager
129
- relative_path = csv_path.relative_to(MEDIA_ROOT)
130
- handler = file_manager.get_file(relative_path)
131
- if handler is None:
132
- raise HTTPException(status_code=404, detail="Cast CSV not accessible")
133
- handler.close()
134
-
135
- return FileResponse(
136
- path=csv_path,
137
- media_type="text/csv",
138
- filename="cast.csv"
139
- )
140
-
141
- @router.post("/upload_original_video", tags=["Media Manager"])
142
- async def upload_video(
143
- video: UploadFile = File(...),
144
- token: str = Query(..., description="Token required for authorization")
145
- ):
146
- """
147
- Saves an uploaded video by hashing it with SHA1 and placing it under:
148
- /data/media/<sha1>/clip/<original_filename>
149
-
150
- Behavior:
151
- - Compute SHA1 of the uploaded video.
152
- - Ensure folder structure exists.
153
- - Delete any existing .mp4 files under /clip.
154
- - Save the uploaded video in the clip folder.
155
- """
156
- MEDIA_ROOT = Path("/data/media")
157
- file_manager = FileManager(MEDIA_ROOT)
158
- HF_TOKEN = os.getenv("HF_TOKEN")
159
- validate_token(token)
160
-
161
- # Read content into memory (needed to compute hash twice)
162
- file_bytes = await video.read()
163
-
164
- # Create an in-memory file handler for hashing
165
- file_handler = io.BytesIO(file_bytes)
166
-
167
- # Compute SHA1
168
- try:
169
- sha1 = file_manager.compute_sha1(file_handler)
170
- except Exception as exc:
171
- raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
172
-
173
- # Ensure /data/media exists
174
- MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
175
-
176
- # Path: /data/media/<sha1>
177
- video_root = MEDIA_ROOT / sha1
178
- video_root.mkdir(parents=True, exist_ok=True)
179
-
180
- # Path: /data/media/<sha1>/clip
181
- clip_dir = video_root / "clip"
182
- clip_dir.mkdir(parents=True, exist_ok=True)
183
-
184
- # Delete old MP4 files
185
- try:
186
- for old_mp4 in clip_dir.glob("*.mp4"):
187
- old_mp4.unlink()
188
- except Exception as exc:
189
- raise HTTPException(status_code=500, detail=f"Failed to delete old videos: {exc}")
190
-
191
- # Save new video path
192
- final_path = clip_dir / video.filename
193
-
194
- # Save file
195
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
196
-
197
- if not save_result["operation_success"]:
198
- raise HTTPException(status_code=500, detail=save_result["error"])
199
-
200
- return JSONResponse(
201
- status_code=200,
202
- content={
203
- "status": "ok",
204
- "sha1": sha1,
205
- "saved_to": str(final_path)
206
- }
207
- )
208
-
209
-
210
- @router.get("/download_original_video", tags=["Media Manager"])
211
- def download_video(
212
- sha1: str,
213
- token: str = Query(..., description="Token required for authorization")
214
- ):
215
- """
216
- Download a stored video by its SHA-1 directory name.
217
-
218
- This endpoint looks for a video stored under the path:
219
- /data/media/<sha1>/clip/
220
- and returns the first MP4 file found in that folder.
221
-
222
- The method performs the following steps:
223
- - Checks if the SHA-1 folder exists inside the media root.
224
- - Validates that the "clip" subfolder exists.
225
- - Searches for the first .mp4 file inside the clip folder.
226
- - Uses the FileManager.get_file method to ensure the file is accessible.
227
- - Returns the video directly as a FileResponse.
228
-
229
- Parameters
230
- ----------
231
- sha1 : str
232
- The SHA-1 hash corresponding to the directory where the video is stored.
233
-
234
- Returns
235
- -------
236
- FileResponse
237
- A streaming response containing the MP4 video.
238
-
239
- Raises
240
- ------
241
- HTTPException
242
- - 404 if the SHA-1 folder does not exist.
243
- - 404 if the clip folder is missing.
244
- - 404 if no MP4 files are found.
245
- - 404 if the file cannot be retrieved using FileManager.
246
- """
247
- MEDIA_ROOT = Path("/data/media")
248
- file_manager = FileManager(MEDIA_ROOT)
249
- HF_TOKEN = os.getenv("HF_TOKEN")
250
- validate_token(token)
251
-
252
- sha1_folder = MEDIA_ROOT / sha1
253
- clip_folder = sha1_folder / "clip"
254
-
255
- if not sha1_folder.exists() or not sha1_folder.is_dir():
256
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
257
-
258
- if not clip_folder.exists() or not clip_folder.is_dir():
259
- raise HTTPException(status_code=404, detail="Clip folder not found")
260
-
261
- # Find first MP4 file
262
- mp4_files = list(clip_folder.glob("*.mp4"))
263
- if not mp4_files:
264
- raise HTTPException(status_code=404, detail="No MP4 files found")
265
-
266
- video_path = mp4_files[0]
267
-
268
- # Convert to relative path for FileManager
269
- relative_path = video_path.relative_to(MEDIA_ROOT)
270
-
271
- handler = file_manager.get_file(relative_path)
272
- if handler is None:
273
- raise HTTPException(status_code=404, detail="Video not accessible")
274
-
275
- handler.close()
276
-
277
- return FileResponse(
278
- path=video_path,
279
- media_type="video/mp4",
280
- filename=video_path.name
281
- )
282
-
283
-
284
- @router.post("/upload_video_ad", tags=["Media Manager"])
285
- async def upload_video_ad(
286
- sha1: str = Query(..., description="SHA1 associated to the media folder"),
287
- version: str = Query(..., description="Version: Salamandra or MoE"),
288
- subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"),
289
- video: UploadFile = File(...),
290
- token: str = Query(..., description="Token required for authorization")
291
- ):
292
- validate_token(token)
293
-
294
- if version not in VALID_VERSIONS:
295
- raise HTTPException(status_code=400, detail="Invalid version")
296
- if subtype not in VALID_SUBTYPES:
297
- raise HTTPException(status_code=400, detail="Invalid subtype")
298
-
299
- MEDIA_ROOT = Path("/data/media")
300
- file_manager = FileManager(MEDIA_ROOT)
301
-
302
- subtype_dir = MEDIA_ROOT / sha1 / version / subtype
303
- subtype_dir.mkdir(parents=True, exist_ok=True)
304
-
305
- for f in subtype_dir.glob("*.mp4"):
306
- f.unlink()
307
-
308
- file_bytes = await video.read()
309
- final_path = subtype_dir / video.filename
310
-
311
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
312
- if not save_result["operation_success"]:
313
- raise HTTPException(status_code=500, detail=save_result["error"])
314
-
315
- return {
316
- "status": "ok",
317
- "sha1": sha1,
318
- "version": version,
319
- "subtype": subtype,
320
- "saved_to": str(final_path)
321
- }
322
-
323
-
324
- @router.get("/download_video_ad", tags=["Media Manager"])
325
- def download_video_ad(
326
- sha1: str,
327
- version: str,
328
- subtype: str,
329
- token: str = Query(..., description="Token required for authorization")
330
- ):
331
- validate_token(token)
332
-
333
- if version not in VALID_VERSIONS:
334
- raise HTTPException(status_code=400, detail="Invalid version")
335
- if subtype not in VALID_SUBTYPES:
336
- raise HTTPException(status_code=400, detail="Invalid subtype")
337
-
338
- MEDIA_ROOT = Path("/data/media")
339
- file_manager = FileManager(MEDIA_ROOT)
340
-
341
- subtype_dir = MEDIA_ROOT / sha1 / version / subtype
342
-
343
- if not subtype_dir.exists() or not subtype_dir.is_dir():
344
- raise HTTPException(status_code=404, detail="Version/subtype folder not found")
345
-
346
- mp4_files = list(subtype_dir.glob("*.mp4"))
347
- if not mp4_files:
348
- raise HTTPException(status_code=404, detail="No MP4 files found")
349
-
350
- video_path = mp4_files[0]
351
- relative_path = video_path.relative_to(MEDIA_ROOT)
352
-
353
- handler = file_manager.get_file(relative_path)
354
- if handler is None:
355
- raise HTTPException(status_code=404, detail="Video not accessible")
356
-
357
- handler.close()
358
-
359
- return FileResponse(
360
- path=video_path,
361
- media_type="video/mp4",
362
- filename=video_path.name
363
- )
364
-
365
- @router.get("/list_original_videos", tags=["Media Manager"])
366
- def list_all_videos(
367
- token: str = Query(..., description="Token required for authorization")
368
- ):
369
- """
370
- List all videos stored under /data/media.
371
-
372
- For each SHA1 folder, the endpoint returns:
373
- - sha1: folder name
374
- - video_files: list of mp4 files inside /clip
375
- - latest_video: the most recently modified mp4
376
- - video_count: total number of mp4 files
377
-
378
- Notes:
379
- - Videos may not have a /clip folder.
380
- - SHA1 folders without mp4 files are still returned.
381
- """
382
- validate_token(token)
383
-
384
- results = []
385
-
386
- # If media root does not exist, return empty list
387
- if not MEDIA_ROOT.exists():
388
- return []
389
-
390
- for sha1_dir in MEDIA_ROOT.iterdir():
391
- if not sha1_dir.is_dir():
392
- continue # skip non-folders
393
-
394
- clip_dir = sha1_dir / "clip"
395
-
396
- videos = []
397
- latest_video = None
398
-
399
- if clip_dir.exists() and clip_dir.is_dir():
400
- mp4_files = list(clip_dir.glob("*.mp4"))
401
-
402
- # Sort by modification time (newest first)
403
- mp4_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
404
-
405
- videos = [f.name for f in mp4_files]
406
-
407
- if mp4_files:
408
- latest_video = mp4_files[0].name
409
-
410
- results.append({
411
- "sha1": sha1_dir.name,
412
- "video_name": latest_video
413
- })
414
-
415
- return results
416
-
417
- @router.post("/upload_original_audio", tags=["Media Manager"])
418
- async def upload_audio(
419
- audio: UploadFile = File(...),
420
- token: str = Query(..., description="Token required for authorization")
421
- ):
422
- """
423
- Saves an uploaded audio file by hashing it with SHA1 and placing it under:
424
- /data/media/<sha1>/audio/<original_filename>
425
-
426
- Behavior:
427
- - Compute SHA1 of the uploaded audio.
428
- - Ensure folder structure exists.
429
- - Delete any existing audio files under /audio.
430
- - Save the uploaded audio in the audio folder.
431
- """
432
- MEDIA_ROOT = Path("/data/media")
433
- file_manager = FileManager(MEDIA_ROOT)
434
- HF_TOKEN = os.getenv("HF_TOKEN")
435
- validate_token(token)
436
-
437
- # Read content into memory (needed to compute hash twice)
438
- file_bytes = await audio.read()
439
-
440
- # Create an in-memory file handler for hashing
441
- file_handler = io.BytesIO(file_bytes)
442
-
443
- # Compute SHA1
444
- try:
445
- sha1 = file_manager.compute_sha1(file_handler)
446
- except Exception as exc:
447
- raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
448
-
449
- # Ensure /data/media exists
450
- MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
451
-
452
- # Path: /data/media/<sha1>
453
- audio_root = MEDIA_ROOT / sha1
454
- audio_root.mkdir(parents=True, exist_ok=True)
455
-
456
- # Path: /data/media/<sha1>/audio
457
- audio_dir = audio_root / "audio"
458
- audio_dir.mkdir(parents=True, exist_ok=True)
459
-
460
- # Delete old audio files
461
- AUDIO_EXTENSIONS = ("*.mp3", "*.wav", "*.m4a", "*.aac", "*.ogg", "*.flac")
462
- try:
463
- for pattern in AUDIO_EXTENSIONS:
464
- for old_audio in audio_dir.glob(pattern):
465
- old_audio.unlink()
466
- except Exception as exc:
467
- raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
468
-
469
- # Final save path
470
- final_path = audio_dir / audio.filename
471
-
472
- # Save file
473
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
474
-
475
- if not save_result["operation_success"]:
476
- raise HTTPException(status_code=500, detail=save_result["error"])
477
-
478
- return JSONResponse(
479
- status_code=200,
480
- content={
481
- "status": "ok",
482
- "sha1": sha1,
483
- "saved_to": str(final_path)
484
- }
485
- )
486
-
487
- @router.get("/download_original_audio", tags=["Media Manager"])
488
- def download_audio(
489
- sha1: str,
490
- token: str = Query(..., description="Token required for authorization")
491
- ):
492
- """
493
- Download a stored audio file by its SHA-1 directory name.
494
-
495
- This endpoint looks for audio stored under the path:
496
- /data/media/<sha1>/audio/
497
- and returns the first audio file found in that folder.
498
-
499
- The method performs the following steps:
500
- - Checks if the SHA-1 folder exists inside the media root.
501
- - Validates that the "audio" subfolder exists.
502
- - Searches for the first supported audio file.
503
- - Uses FileManager.get_file to ensure the file is accessible.
504
- - Returns the audio file as a FileResponse.
505
-
506
- Parameters
507
- ----------
508
- sha1 : str
509
- The SHA-1 hash corresponding to the directory where the audio is stored.
510
-
511
- Returns
512
- -------
513
- FileResponse
514
- A streaming response containing the audio file.
515
-
516
- Raises
517
- ------
518
- HTTPException
519
- - 404 if the SHA-1 folder does not exist.
520
- - 404 if the audio folder is missing.
521
- - 404 if no audio files are found.
522
- - 404 if the file cannot be retrieved using FileManager.
523
- """
524
- MEDIA_ROOT = Path("/data/media")
525
- file_manager = FileManager(MEDIA_ROOT)
526
- HF_TOKEN = os.getenv("HF_TOKEN")
527
- validate_token(token)
528
-
529
- sha1_folder = MEDIA_ROOT / sha1
530
- audio_folder = sha1_folder / "audio"
531
-
532
- if not sha1_folder.exists() or not sha1_folder.is_dir():
533
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
534
-
535
- if not audio_folder.exists() or not audio_folder.is_dir():
536
- raise HTTPException(status_code=404, detail="Audio folder not found")
537
-
538
- # Supported audio extensions
539
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
540
-
541
- audio_files = []
542
- for pattern in AUDIO_EXTENSIONS:
543
- audio_files.extend(list(audio_folder.glob(pattern)))
544
-
545
- if not audio_files:
546
- raise HTTPException(status_code=404, detail="No audio files found")
547
-
548
- audio_path = audio_files[0]
549
-
550
- # Convert to relative path for FileManager
551
- relative_path = audio_path.relative_to(MEDIA_ROOT)
552
-
553
- handler = file_manager.get_file(relative_path)
554
- if handler is None:
555
- raise HTTPException(status_code=404, detail="Audio file not accessible")
556
-
557
- handler.close()
558
-
559
- # Guess media type based on extension (simple)
560
- media_type = "audio/" + audio_path.suffix.lstrip(".")
561
-
562
- return FileResponse(
563
- path=audio_path,
564
- media_type=media_type,
565
- filename=audio_path.name
566
- )
567
-
568
- @router.get("/list_original_audios", tags=["Media Manager"])
569
- def list_all_audios(
570
- token: str = Query(..., description="Token required for authorization")
571
- ):
572
- """
573
- List all audio files stored under /data/media.
574
-
575
- For each SHA1 folder, the endpoint returns:
576
- - sha1: folder name
577
- - audio_files: list of audio files inside /audio
578
- - latest_audio: the most recently modified audio file
579
- - audio_count: total number of audio files
580
-
581
- Notes:
582
- - Folders may not have an /audio folder.
583
- - SHA1 folders without audio files are still returned.
584
- """
585
- validate_token(token)
586
-
587
- results = []
588
-
589
- MEDIA_ROOT = Path("/data/media")
590
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
591
-
592
- # If media root does not exist, return empty list
593
- if not MEDIA_ROOT.exists():
594
- return []
595
-
596
- for sha1_dir in MEDIA_ROOT.iterdir():
597
- if not sha1_dir.is_dir():
598
- continue # skip non-folders
599
-
600
- audio_dir = sha1_dir / "audio"
601
-
602
- audio_files = []
603
- latest_audio = None
604
-
605
- if audio_dir.exists() and audio_dir.is_dir():
606
- # Collect all audio files with supported extensions
607
- files = []
608
- for pattern in AUDIO_EXTENSIONS:
609
- files.extend(list(audio_dir.glob(pattern)))
610
-
611
- # Sort by modification time (newest first)
612
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
613
-
614
- audio_files = [f.name for f in files]
615
-
616
- if files:
617
- latest_audio = files[0].name
618
-
619
- results.append({
620
- "sha1": sha1_dir.name,
621
- "audio_name": latest_audio,
622
- })
623
-
624
- return results
625
-
626
-
627
- @router.post("/upload_audio_version", tags=["Media Manager"])
628
- async def upload_audio_version(
629
- audio: UploadFile = File(...),
630
- sha1: str = Query(..., description="SHA1 of the video folder"),
631
- version: str = Query(..., description="Version: Salamandra or MoE"),
632
- token: str = Query(..., description="Token required for authorization")
633
- ):
634
- """Upload audio for a given version (Salamandra, MoE).
635
-
636
- This legacy endpoint keeps its path but now interprets the former
637
- `subtype` path component as `version`:
638
- - Target folder: /data/media/<sha1>/<version>/
639
- - Deletes any previous audio files
640
- - Saves the new audio
641
- """
642
- validate_token(token)
643
-
644
- if version not in VALID_VERSIONS:
645
- raise HTTPException(status_code=400, detail="Invalid version")
646
-
647
- MEDIA_ROOT = Path("/data/media")
648
- version_dir = MEDIA_ROOT / sha1 / version
649
- version_dir.mkdir(parents=True, exist_ok=True)
650
-
651
- # Delete old audio files
652
- try:
653
- for pattern in AUDIO_EXTENSIONS:
654
- for old_audio in version_dir.glob(pattern):
655
- old_audio.unlink()
656
- except Exception as exc:
657
- raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
658
-
659
- final_path = version_dir / audio.filename
660
-
661
- try:
662
- file_bytes = await audio.read()
663
- with open(final_path, "wb") as f:
664
- f.write(file_bytes)
665
- except Exception as exc:
666
- raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}")
667
-
668
- return JSONResponse(
669
- status_code=200,
670
- content={
671
- "status": "ok",
672
- "sha1": sha1,
673
- "version": version,
674
- "saved_to": str(final_path)
675
- }
676
- )
677
-
678
-
679
- @router.get("/download_audio_version", tags=["Media Manager"])
680
- def download_audio_version(
681
- sha1: str,
682
- version: str,
683
- token: str = Query(..., description="Token required for authorization")
684
- ):
685
- """Download the first audio file for a given version (Salamandra, MoE)."""
686
- validate_token(token)
687
-
688
- if version not in VALID_VERSIONS:
689
- raise HTTPException(status_code=400, detail="Invalid version")
690
-
691
- MEDIA_ROOT = Path("/data/media")
692
- version_dir = MEDIA_ROOT / sha1 / version
693
-
694
- if not version_dir.exists() or not version_dir.is_dir():
695
- raise HTTPException(status_code=404, detail=f"{version} folder not found")
696
-
697
- # Find audio files
698
- audio_files = []
699
- for pattern in AUDIO_EXTENSIONS:
700
- audio_files.extend(list(version_dir.glob(pattern)))
701
-
702
- if not audio_files:
703
- raise HTTPException(status_code=404, detail="No audio files found")
704
-
705
- audio_path = audio_files[0]
706
-
707
- return FileResponse(
708
- path=audio_path,
709
- media_type="audio/" + audio_path.suffix.lstrip("."),
710
- filename=audio_path.name
711
- )
712
-
713
-
714
- @router.post("/upload_audio_ad", tags=["Media Manager"])
715
- async def upload_audio_ad(
716
- audio: UploadFile = File(...),
717
- sha1: str = Query(..., description="SHA1 of the video folder"),
718
- version: str = Query(..., description="Version: Salamandra or MoE"),
719
- subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"),
720
- token: str = Query(..., description="Token required for authorization")
721
- ):
722
- validate_token(token)
723
-
724
- if version not in VALID_VERSIONS:
725
- raise HTTPException(status_code=400, detail="Invalid version")
726
- if subtype not in VALID_SUBTYPES:
727
- raise HTTPException(status_code=400, detail="Invalid subtype")
728
-
729
- MEDIA_ROOT = Path("/data/media")
730
- subtype_dir = MEDIA_ROOT / sha1 / version / subtype
731
- subtype_dir.mkdir(parents=True, exist_ok=True)
732
-
733
- try:
734
- for pattern in AUDIO_EXTENSIONS:
735
- for old_audio in subtype_dir.glob(pattern):
736
- old_audio.unlink()
737
- except Exception as exc:
738
- raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
739
-
740
- final_path = subtype_dir / audio.filename
741
-
742
- try:
743
- file_bytes = await audio.read()
744
- with open(final_path, "wb") as f:
745
- f.write(file_bytes)
746
- except Exception as exc:
747
- raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}")
748
-
749
- return JSONResponse(
750
- status_code=200,
751
- content={
752
- "status": "ok",
753
- "sha1": sha1,
754
- "version": version,
755
- "subtype": subtype,
756
- "saved_to": str(final_path)
757
- }
758
- )
759
-
760
-
761
- @router.get("/download_audio_ad", tags=["Media Manager"])
762
- def download_audio_ad(
763
- sha1: str,
764
- version: str,
765
- subtype: str,
766
- token: str = Query(..., description="Token required for authorization")
767
- ):
768
- validate_token(token)
769
-
770
- if version not in VALID_VERSIONS:
771
- raise HTTPException(status_code=400, detail="Invalid version")
772
- if subtype not in VALID_SUBTYPES:
773
- raise HTTPException(status_code=400, detail="Invalid subtype")
774
-
775
- MEDIA_ROOT = Path("/data/media")
776
- subtype_dir = MEDIA_ROOT / sha1 / version / subtype
777
-
778
- if not subtype_dir.exists() or not subtype_dir.is_dir():
779
- raise HTTPException(status_code=404, detail="Version/subtype folder not found")
780
-
781
- audio_files = []
782
- for pattern in AUDIO_EXTENSIONS:
783
- audio_files.extend(list(subtype_dir.glob(pattern)))
784
-
785
- if not audio_files:
786
- raise HTTPException(status_code=404, detail="No audio files found")
787
-
788
- audio_path = audio_files[0]
789
-
790
- return FileResponse(
791
- path=audio_path,
792
- media_type="audio/" + audio_path.suffix.lstrip("."),
793
- filename=audio_path.name
794
- )
795
-
796
-
797
- @router.get("/list_version_audios", tags=["Media Manager"])
798
- def list_version_audios(
799
- sha1: str = Query(..., description="SHA1 of the video folder"),
800
- token: str = Query(..., description="Token required for authorization")
801
- ):
802
- """List the most recent audio file for each version (Salamandra, MoE)
803
- under /data/media/<sha1>.
804
-
805
- Returns:
806
- - sha1: folder name
807
- - version: name of the version
808
- - audio_name: latest audio file or None
809
- """
810
- validate_token(token)
811
-
812
- results = []
813
-
814
- for version in VALID_VERSIONS:
815
- version_dir = MEDIA_ROOT / sha1 / version
816
-
817
- latest_audio = None
818
-
819
- if version_dir.exists() and version_dir.is_dir():
820
- files = []
821
- for pattern in AUDIO_EXTENSIONS:
822
- files.extend(list(version_dir.glob(pattern)))
823
-
824
- if files:
825
- # Sort by modification time (newest first)
826
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
827
- latest_audio = files[0].name
828
-
829
- results.append({
830
- "sha1": sha1,
831
- "version": version,
832
- "audio_name": latest_audio
833
- })
834
-
835
- return results
836
-
837
-
838
- @router.post("/upload_version_video", tags=["Media Manager"])
839
- async def upload_version_video(
840
- sha1: str = Query(..., description="SHA1 associated to the media folder"),
841
- version: str = Query(..., description="Version: Salamandra or MoE"),
842
- video: UploadFile = File(...),
843
- token: str = Query(..., description="Token required for authorization")
844
- ):
845
- """Upload a video to /data/media/<sha1>/<version>/.
846
-
847
- This legacy endpoint keeps its path but now interprets the former
848
- `subtype` path component as `version`.
849
- Steps:
850
- - Validate version.
851
- - Create version folder if missing.
852
- - Delete existing MP4 files.
853
- - Save new MP4.
854
- """
855
- validate_token(token)
856
-
857
- if version not in VALID_VERSIONS:
858
- raise HTTPException(status_code=400, detail="Invalid version")
859
-
860
- MEDIA_ROOT = Path("/data/media")
861
- file_manager = FileManager(MEDIA_ROOT)
862
-
863
- version_dir = MEDIA_ROOT / sha1 / version
864
- version_dir.mkdir(parents=True, exist_ok=True)
865
-
866
- # Remove old mp4 files
867
- for f in version_dir.glob("*.mp4"):
868
- f.unlink()
869
-
870
- file_bytes = await video.read()
871
- final_path = version_dir / video.filename
872
-
873
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
874
- if not save_result["operation_success"]:
875
- raise HTTPException(status_code=500, detail=save_result["error"])
876
-
877
- return {
878
- "status": "ok",
879
- "sha1": sha1,
880
- "version": version,
881
- "saved_to": str(final_path)
882
- }
883
-
884
-
885
- @router.get("/download_version_video", tags=["Media Manager"])
886
- def download_version_video(
887
- sha1: str,
888
- version: str,
889
- token: str = Query(..., description="Token required for authorization")
890
- ):
891
- """Download the video stored under /data/media/<sha1>/<version>.
892
- Returns the first MP4 found.
893
- """
894
- validate_token(token)
895
-
896
- if version not in VALID_VERSIONS:
897
- raise HTTPException(status_code=400, detail="Invalid version")
898
-
899
- MEDIA_ROOT = Path("/data/media")
900
- file_manager = FileManager(MEDIA_ROOT)
901
-
902
- version_dir = MEDIA_ROOT / sha1 / version
903
-
904
- if not version_dir.exists() or not version_dir.is_dir():
905
- raise HTTPException(status_code=404, detail="Version folder not found")
906
-
907
- mp4_files = list(version_dir.glob("*.mp4"))
908
- if not mp4_files:
909
- raise HTTPException(status_code=404, detail="No MP4 files found")
910
-
911
- video_path = mp4_files[0]
912
- relative_path = video_path.relative_to(MEDIA_ROOT)
913
-
914
- handler = file_manager.get_file(relative_path)
915
- if handler is None:
916
- raise HTTPException(status_code=404, detail="Video not accessible")
917
-
918
- handler.close()
919
-
920
- return FileResponse(
921
- path=video_path,
922
- media_type="video/mp4",
923
- filename=video_path.name
924
- )
925
-
926
-
927
- @router.get("/list_version_videos", tags=["Media Manager"])
928
- def list_version_videos(
929
- sha1: str,
930
- token: str = Query(..., description="Token required for authorization")
931
- ):
932
- """List the most recent .mp4 video for each version (Salamandra, MoE)
933
- inside /data/media/<sha1>.
934
-
935
- Returns:
936
- - sha1
937
- - version
938
- - video_name (latest mp4 or None)
939
- """
940
- validate_token(token)
941
-
942
- MEDIA_ROOT = Path("/data/media")
943
-
944
- results = []
945
-
946
- for version in VALID_VERSIONS:
947
- version_dir = MEDIA_ROOT / sha1 / version
948
-
949
- latest_video = None
950
-
951
- if version_dir.exists() and version_dir.is_dir():
952
- files = list(version_dir.glob("*.mp4"))
953
- if files:
954
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
955
- latest_video = files[0].name
956
-
957
- results.append({
958
- "sha1": sha1,
959
- "version": version,
960
- "video_name": latest_video
961
- })
962
-
963
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import shutil
4
+
5
+ import sqlite3
6
+
7
+ from pathlib import Path
8
+
9
+ from fastapi import APIRouter, UploadFile, File, Query, HTTPException
10
+ from fastapi.responses import FileResponse, JSONResponse
11
+
12
+
13
+ from storage.files.file_manager import FileManager
14
+ from storage.common import validate_token
15
+
16
+ router = APIRouter(prefix="/media", tags=["Media Manager"])
17
+ MEDIA_ROOT = Path("/data/media")
18
+ file_manager = FileManager(MEDIA_ROOT)
19
+ HF_TOKEN = os.getenv("HF_TOKEN")
20
+ VALID_VERSIONS = ("Salamandra", "MoE")
21
+ VALID_SUBTYPES = ("Original", "HITL OK", "HITL Test")
22
+ AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
23
+
24
+
25
+ @router.delete("/clear_media", tags=["Media Manager"])
26
+ def clear_media(token: str = Query(..., description="Token required for authorization")):
27
+ """
28
+ Delete all contents of the /data/media folder.
29
+
30
+ Steps:
31
+ - Validate the token.
32
+ - Ensure the folder exists.
33
+ - Delete all files and subfolders inside /data/media.
34
+ - Return a JSON response confirming the deletion.
35
+
36
+ Warning: This will remove all stored videos, clips, and cast CSV files.
37
+ """
38
+ validate_token(token)
39
+
40
+ if not MEDIA_ROOT.exists() or not MEDIA_ROOT.is_dir():
41
+ raise HTTPException(status_code=404, detail="/data/media folder does not exist")
42
+
43
+ # Delete contents
44
+ for item in MEDIA_ROOT.iterdir():
45
+ try:
46
+ if item.is_dir():
47
+ shutil.rmtree(item)
48
+ else:
49
+ item.unlink()
50
+ except Exception as e:
51
+ raise HTTPException(status_code=500, detail=f"Failed to delete {item}: {e}")
52
+
53
+ return {"status": "ok", "message": "All media files deleted successfully"}
54
+
55
+ @router.post("/upload_cast_csv", tags=["Media Manager"])
56
+ async def upload_cast_csv(
57
+ sha1: str,
58
+ cast_file: UploadFile = File(...),
59
+ token: str = Query(..., description="Token required for authorization")
60
+ ):
61
+ """
62
+ Upload a cast CSV file for a specific video identified by its SHA-1.
63
+
64
+ The CSV will be stored under:
65
+ /data/media/<sha1>/cast/cast.csv
66
+
67
+ Steps:
68
+ - Validate the token.
69
+ - Ensure /data/media/<sha1> exists.
70
+ - Create /cast folder if missing.
71
+ - Save the CSV file inside /cast.
72
+ """
73
+ validate_token(token)
74
+
75
+ base_folder = MEDIA_ROOT / sha1
76
+ if not base_folder.exists() or not base_folder.is_dir():
77
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
78
+
79
+ cast_folder = base_folder / "cast"
80
+ cast_folder.mkdir(parents=True, exist_ok=True)
81
+
82
+ final_path = cast_folder / "cast.csv"
83
+
84
+ file_bytes = await cast_file.read()
85
+ save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
86
+ if not save_result["operation_success"]:
87
+ raise HTTPException(status_code=500, detail=save_result["error"])
88
+
89
+ return JSONResponse(
90
+ status_code=200,
91
+ content={"status": "ok", "saved_to": str(final_path)}
92
+ )
93
+
94
+
95
+ @router.get("/download_cast_csv", tags=["Media Manager"])
96
+ def download_cast_csv(
97
+ sha1: str,
98
+ token: str = Query(..., description="Token required for authorization")
99
+ ):
100
+ """
101
+ Download the cast CSV for a specific video identified by its SHA-1.
102
+
103
+ The CSV is expected under:
104
+ /data/media/<sha1>/cast/cast.csv
105
+
106
+ Steps:
107
+ - Validate the token.
108
+ - Ensure /data/media/<sha1> and /cast exist.
109
+ - Return the CSV as a FileResponse.
110
+ - Raise 404 if any folder or file is missing.
111
+ """
112
+ MEDIA_ROOT = Path("/data/media")
113
+ file_manager = FileManager(MEDIA_ROOT)
114
+ HF_TOKEN = os.getenv("HF_TOKEN")
115
+ validate_token(token)
116
+
117
+ base_folder = MEDIA_ROOT / sha1
118
+ cast_folder = base_folder / "cast"
119
+ csv_path = cast_folder / "cast.csv"
120
+
121
+ if not base_folder.exists() or not base_folder.is_dir():
122
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
123
+ if not cast_folder.exists() or not cast_folder.is_dir():
124
+ raise HTTPException(status_code=404, detail="Cast folder not found")
125
+ if not csv_path.exists() or not csv_path.is_file():
126
+ raise HTTPException(status_code=404, detail="Cast CSV not found")
127
+
128
+ # Convert to relative path for FileManager
129
+ relative_path = csv_path.relative_to(MEDIA_ROOT)
130
+ handler = file_manager.get_file(relative_path)
131
+ if handler is None:
132
+ raise HTTPException(status_code=404, detail="Cast CSV not accessible")
133
+ handler.close()
134
+
135
+ return FileResponse(
136
+ path=csv_path,
137
+ media_type="text/csv",
138
+ filename="cast.csv"
139
+ )
140
+
141
+ @router.post("/upload_original_video", tags=["Media Manager"])
142
+ async def upload_video(
143
+ video: UploadFile = File(...),
144
+ token: str = Query(..., description="Token required for authorization")
145
+ ):
146
+ """
147
+ Saves an uploaded video by hashing it with SHA1 and placing it under:
148
+ /data/media/<sha1>/clip/<original_filename>
149
+
150
+ Behavior:
151
+ - Compute SHA1 of the uploaded video.
152
+ - Ensure folder structure exists.
153
+ - Delete any existing .mp4 files under /clip.
154
+ - Save the uploaded video in the clip folder.
155
+ """
156
+ MEDIA_ROOT = Path("/data/media")
157
+ file_manager = FileManager(MEDIA_ROOT)
158
+ HF_TOKEN = os.getenv("HF_TOKEN")
159
+ validate_token(token)
160
+
161
+ # Read content into memory (needed to compute hash twice)
162
+ file_bytes = await video.read()
163
+
164
+ # Create an in-memory file handler for hashing
165
+ file_handler = io.BytesIO(file_bytes)
166
+
167
+ # Compute SHA1
168
+ try:
169
+ sha1 = file_manager.compute_sha1(file_handler)
170
+ except Exception as exc:
171
+ raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
172
+
173
+ # Ensure /data/media exists
174
+ MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
175
+
176
+ # Path: /data/media/<sha1>
177
+ video_root = MEDIA_ROOT / sha1
178
+ video_root.mkdir(parents=True, exist_ok=True)
179
+
180
+ # Path: /data/media/<sha1>/clip
181
+ clip_dir = video_root / "clip"
182
+ clip_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ # Delete old MP4 files
185
+ try:
186
+ for old_mp4 in clip_dir.glob("*.mp4"):
187
+ old_mp4.unlink()
188
+ except Exception as exc:
189
+ raise HTTPException(status_code=500, detail=f"Failed to delete old videos: {exc}")
190
+
191
+ # Save new video path
192
+ final_path = clip_dir / video.filename
193
+
194
+ # Save file
195
+ save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
196
+
197
+ if not save_result["operation_success"]:
198
+ raise HTTPException(status_code=500, detail=save_result["error"])
199
+
200
+ return JSONResponse(
201
+ status_code=200,
202
+ content={
203
+ "status": "ok",
204
+ "sha1": sha1,
205
+ "saved_to": str(final_path)
206
+ }
207
+ )
208
+
209
+
210
+ @router.get("/download_original_video", tags=["Media Manager"])
211
+ def download_video(
212
+ sha1: str,
213
+ token: str = Query(..., description="Token required for authorization")
214
+ ):
215
+ """
216
+ Download a stored video by its SHA-1 directory name.
217
+
218
+ This endpoint looks for a video stored under the path:
219
+ /data/media/<sha1>/clip/
220
+ and returns the first MP4 file found in that folder.
221
+
222
+ The method performs the following steps:
223
+ - Checks if the SHA-1 folder exists inside the media root.
224
+ - Validates that the "clip" subfolder exists.
225
+ - Searches for the first .mp4 file inside the clip folder.
226
+ - Uses the FileManager.get_file method to ensure the file is accessible.
227
+ - Returns the video directly as a FileResponse.
228
+
229
+ Parameters
230
+ ----------
231
+ sha1 : str
232
+ The SHA-1 hash corresponding to the directory where the video is stored.
233
+
234
+ Returns
235
+ -------
236
+ FileResponse
237
+ A streaming response containing the MP4 video.
238
+
239
+ Raises
240
+ ------
241
+ HTTPException
242
+ - 404 if the SHA-1 folder does not exist.
243
+ - 404 if the clip folder is missing.
244
+ - 404 if no MP4 files are found.
245
+ - 404 if the file cannot be retrieved using FileManager.
246
+ """
247
+ MEDIA_ROOT = Path("/data/media")
248
+ file_manager = FileManager(MEDIA_ROOT)
249
+ HF_TOKEN = os.getenv("HF_TOKEN")
250
+ validate_token(token)
251
+
252
+ sha1_folder = MEDIA_ROOT / sha1
253
+ clip_folder = sha1_folder / "clip"
254
+
255
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
256
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
257
+
258
+ if not clip_folder.exists() or not clip_folder.is_dir():
259
+ raise HTTPException(status_code=404, detail="Clip folder not found")
260
+
261
+ # Find first MP4 file
262
+ mp4_files = list(clip_folder.glob("*.mp4"))
263
+ if not mp4_files:
264
+ raise HTTPException(status_code=404, detail="No MP4 files found")
265
+
266
+ video_path = mp4_files[0]
267
+
268
+ # Convert to relative path for FileManager
269
+ relative_path = video_path.relative_to(MEDIA_ROOT)
270
+
271
+ handler = file_manager.get_file(relative_path)
272
+ if handler is None:
273
+ raise HTTPException(status_code=404, detail="Video not accessible")
274
+
275
+ handler.close()
276
+
277
+ return FileResponse(
278
+ path=video_path,
279
+ media_type="video/mp4",
280
+ filename=video_path.name
281
+ )
282
+
283
+ @router.delete("/delete_video", tags=["Media Manager"])
284
+ def delete_media(
285
+ hash: str,
286
+ token: str = Query(..., description="Token required for authorization")
287
+ ):
288
+ """
289
+ Delete a stored media directory by its hash.
290
+
291
+ This endpoint removes the folder located at:
292
+ /data/media/<hash>
293
+
294
+ The method performs the following steps:
295
+ - Validates the authorization token.
296
+ - Checks if the hash folder exists inside the media root.
297
+ - Deletes the entire directory recursively.
298
+
299
+ Parameters
300
+ ----------
301
+ hash : str
302
+ The hash corresponding to the directory to be deleted.
303
+
304
+ Returns
305
+ -------
306
+ dict
307
+ A confirmation message indicating successful deletion.
308
+
309
+ Raises
310
+ ------
311
+ HTTPException
312
+ - 404 if the hash folder does not exist.
313
+ - 500 if the folder cannot be deleted.
314
+ """
315
+ MEDIA_ROOT = Path("/data/media")
316
+ validate_token(token)
317
+
318
+ hash_folder = MEDIA_ROOT / hash
319
+
320
+ if not hash_folder.exists() or not hash_folder.is_dir():
321
+ raise HTTPException(status_code=404, detail="Media folder not found")
322
+
323
+ try:
324
+ shutil.rmtree(hash_folder)
325
+ except Exception as e:
326
+ raise HTTPException(
327
+ status_code=500,
328
+ detail=f"Failed to delete media folder: {str(e)}"
329
+ )
330
+
331
+ return {
332
+ "status": "ok",
333
+ "message": f"Media folder '{hash}' deleted successfully"
334
+ }
335
+
336
+ @router.post("/upload_video_ad", tags=["Media Manager"])
337
+ async def upload_video_ad(
338
+ sha1: str = Query(..., description="SHA1 associated to the media folder"),
339
+ version: str = Query(..., description="Version: Salamandra or MoE"),
340
+ subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"),
341
+ video: UploadFile = File(...),
342
+ token: str = Query(..., description="Token required for authorization")
343
+ ):
344
+ validate_token(token)
345
+
346
+ if version not in VALID_VERSIONS:
347
+ raise HTTPException(status_code=400, detail="Invalid version")
348
+ if subtype not in VALID_SUBTYPES:
349
+ raise HTTPException(status_code=400, detail="Invalid subtype")
350
+
351
+ MEDIA_ROOT = Path("/data/media")
352
+ file_manager = FileManager(MEDIA_ROOT)
353
+
354
+ subtype_dir = MEDIA_ROOT / sha1 / version / subtype
355
+ subtype_dir.mkdir(parents=True, exist_ok=True)
356
+
357
+ for f in subtype_dir.glob("*.mp4"):
358
+ f.unlink()
359
+
360
+ file_bytes = await video.read()
361
+ final_path = subtype_dir / video.filename
362
+
363
+ save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
364
+ if not save_result["operation_success"]:
365
+ raise HTTPException(status_code=500, detail=save_result["error"])
366
+
367
+ return {
368
+ "status": "ok",
369
+ "sha1": sha1,
370
+ "version": version,
371
+ "subtype": subtype,
372
+ "saved_to": str(final_path)
373
+ }
374
+
375
+
376
+ @router.get("/download_video_ad", tags=["Media Manager"])
377
+ def download_video_ad(
378
+ sha1: str,
379
+ version: str,
380
+ subtype: str,
381
+ token: str = Query(..., description="Token required for authorization")
382
+ ):
383
+ validate_token(token)
384
+
385
+ if version not in VALID_VERSIONS:
386
+ raise HTTPException(status_code=400, detail="Invalid version")
387
+ if subtype not in VALID_SUBTYPES:
388
+ raise HTTPException(status_code=400, detail="Invalid subtype")
389
+
390
+ MEDIA_ROOT = Path("/data/media")
391
+ file_manager = FileManager(MEDIA_ROOT)
392
+
393
+ subtype_dir = MEDIA_ROOT / sha1 / version / subtype
394
+
395
+ if not subtype_dir.exists() or not subtype_dir.is_dir():
396
+ raise HTTPException(status_code=404, detail="Version/subtype folder not found")
397
+
398
+ mp4_files = list(subtype_dir.glob("*.mp4"))
399
+ if not mp4_files:
400
+ raise HTTPException(status_code=404, detail="No MP4 files found")
401
+
402
+ video_path = mp4_files[0]
403
+ relative_path = video_path.relative_to(MEDIA_ROOT)
404
+
405
+ handler = file_manager.get_file(relative_path)
406
+ if handler is None:
407
+ raise HTTPException(status_code=404, detail="Video not accessible")
408
+
409
+ handler.close()
410
+
411
+ return FileResponse(
412
+ path=video_path,
413
+ media_type="video/mp4",
414
+ filename=video_path.name
415
+ )
416
+
417
+ @router.get("/list_original_videos", tags=["Media Manager"])
418
+ def list_all_videos(
419
+ token: str = Query(..., description="Token required for authorization")
420
+ ):
421
+ """
422
+ List all videos stored under /data/media.
423
+
424
+ For each SHA1 folder, the endpoint returns:
425
+ - sha1: folder name
426
+ - video_files: list of mp4 files inside /clip
427
+ - latest_video: the most recently modified mp4
428
+ - video_count: total number of mp4 files
429
+
430
+ Notes:
431
+ - Videos may not have a /clip folder.
432
+ - SHA1 folders without mp4 files are still returned.
433
+ """
434
+ validate_token(token)
435
+
436
+ results = []
437
+
438
+ # If media root does not exist, return empty list
439
+ if not MEDIA_ROOT.exists():
440
+ return []
441
+
442
+ for sha1_dir in MEDIA_ROOT.iterdir():
443
+ if not sha1_dir.is_dir():
444
+ continue # skip non-folders
445
+
446
+ clip_dir = sha1_dir / "clip"
447
+
448
+ videos = []
449
+ latest_video = None
450
+
451
+ if clip_dir.exists() and clip_dir.is_dir():
452
+ mp4_files = list(clip_dir.glob("*.mp4"))
453
+
454
+ # Sort by modification time (newest first)
455
+ mp4_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
456
+
457
+ videos = [f.name for f in mp4_files]
458
+
459
+ if mp4_files:
460
+ latest_video = mp4_files[0].name
461
+
462
+ results.append({
463
+ "sha1": sha1_dir.name,
464
+ "video_name": latest_video
465
+ })
466
+
467
+ return results
468
+
469
+ @router.post("/upload_original_audio", tags=["Media Manager"])
470
+ async def upload_audio(
471
+ audio: UploadFile = File(...),
472
+ token: str = Query(..., description="Token required for authorization")
473
+ ):
474
+ """
475
+ Saves an uploaded audio file by hashing it with SHA1 and placing it under:
476
+ /data/media/<sha1>/audio/<original_filename>
477
+
478
+ Behavior:
479
+ - Compute SHA1 of the uploaded audio.
480
+ - Ensure folder structure exists.
481
+ - Delete any existing audio files under /audio.
482
+ - Save the uploaded audio in the audio folder.
483
+ """
484
+ MEDIA_ROOT = Path("/data/media")
485
+ file_manager = FileManager(MEDIA_ROOT)
486
+ HF_TOKEN = os.getenv("HF_TOKEN")
487
+ validate_token(token)
488
+
489
+ # Read content into memory (needed to compute hash twice)
490
+ file_bytes = await audio.read()
491
+
492
+ # Create an in-memory file handler for hashing
493
+ file_handler = io.BytesIO(file_bytes)
494
+
495
+ # Compute SHA1
496
+ try:
497
+ sha1 = file_manager.compute_sha1(file_handler)
498
+ except Exception as exc:
499
+ raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
500
+
501
+ # Ensure /data/media exists
502
+ MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
503
+
504
+ # Path: /data/media/<sha1>
505
+ audio_root = MEDIA_ROOT / sha1
506
+ audio_root.mkdir(parents=True, exist_ok=True)
507
+
508
+ # Path: /data/media/<sha1>/audio
509
+ audio_dir = audio_root / "audio"
510
+ audio_dir.mkdir(parents=True, exist_ok=True)
511
+
512
+ # Delete old audio files
513
+ AUDIO_EXTENSIONS = ("*.mp3", "*.wav", "*.m4a", "*.aac", "*.ogg", "*.flac")
514
+ try:
515
+ for pattern in AUDIO_EXTENSIONS:
516
+ for old_audio in audio_dir.glob(pattern):
517
+ old_audio.unlink()
518
+ except Exception as exc:
519
+ raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
520
+
521
+ # Final save path
522
+ final_path = audio_dir / audio.filename
523
+
524
+ # Save file
525
+ save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
526
+
527
+ if not save_result["operation_success"]:
528
+ raise HTTPException(status_code=500, detail=save_result["error"])
529
+
530
+ return JSONResponse(
531
+ status_code=200,
532
+ content={
533
+ "status": "ok",
534
+ "sha1": sha1,
535
+ "saved_to": str(final_path)
536
+ }
537
+ )
538
+
539
+ @router.get("/download_original_audio", tags=["Media Manager"])
540
+ def download_audio(
541
+ sha1: str,
542
+ token: str = Query(..., description="Token required for authorization")
543
+ ):
544
+ """
545
+ Download a stored audio file by its SHA-1 directory name.
546
+
547
+ This endpoint looks for audio stored under the path:
548
+ /data/media/<sha1>/audio/
549
+ and returns the first audio file found in that folder.
550
+
551
+ The method performs the following steps:
552
+ - Checks if the SHA-1 folder exists inside the media root.
553
+ - Validates that the "audio" subfolder exists.
554
+ - Searches for the first supported audio file.
555
+ - Uses FileManager.get_file to ensure the file is accessible.
556
+ - Returns the audio file as a FileResponse.
557
+
558
+ Parameters
559
+ ----------
560
+ sha1 : str
561
+ The SHA-1 hash corresponding to the directory where the audio is stored.
562
+
563
+ Returns
564
+ -------
565
+ FileResponse
566
+ A streaming response containing the audio file.
567
+
568
+ Raises
569
+ ------
570
+ HTTPException
571
+ - 404 if the SHA-1 folder does not exist.
572
+ - 404 if the audio folder is missing.
573
+ - 404 if no audio files are found.
574
+ - 404 if the file cannot be retrieved using FileManager.
575
+ """
576
+ MEDIA_ROOT = Path("/data/media")
577
+ file_manager = FileManager(MEDIA_ROOT)
578
+ HF_TOKEN = os.getenv("HF_TOKEN")
579
+ validate_token(token)
580
+
581
+ sha1_folder = MEDIA_ROOT / sha1
582
+ audio_folder = sha1_folder / "audio"
583
+
584
+ if not sha1_folder.exists() or not sha1_folder.is_dir():
585
+ raise HTTPException(status_code=404, detail="SHA1 folder not found")
586
+
587
+ if not audio_folder.exists() or not audio_folder.is_dir():
588
+ raise HTTPException(status_code=404, detail="Audio folder not found")
589
+
590
+ # Supported audio extensions
591
+ AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
592
+
593
+ audio_files = []
594
+ for pattern in AUDIO_EXTENSIONS:
595
+ audio_files.extend(list(audio_folder.glob(pattern)))
596
+
597
+ if not audio_files:
598
+ raise HTTPException(status_code=404, detail="No audio files found")
599
+
600
+ audio_path = audio_files[0]
601
+
602
+ # Convert to relative path for FileManager
603
+ relative_path = audio_path.relative_to(MEDIA_ROOT)
604
+
605
+ handler = file_manager.get_file(relative_path)
606
+ if handler is None:
607
+ raise HTTPException(status_code=404, detail="Audio file not accessible")
608
+
609
+ handler.close()
610
+
611
+ # Guess media type based on extension (simple)
612
+ media_type = "audio/" + audio_path.suffix.lstrip(".")
613
+
614
+ return FileResponse(
615
+ path=audio_path,
616
+ media_type=media_type,
617
+ filename=audio_path.name
618
+ )
619
+
620
+ @router.get("/list_original_audios", tags=["Media Manager"])
621
+ def list_all_audios(
622
+ token: str = Query(..., description="Token required for authorization")
623
+ ):
624
+ """
625
+ List all audio files stored under /data/media.
626
+
627
+ For each SHA1 folder, the endpoint returns:
628
+ - sha1: folder name
629
+ - audio_files: list of audio files inside /audio
630
+ - latest_audio: the most recently modified audio file
631
+ - audio_count: total number of audio files
632
+
633
+ Notes:
634
+ - Folders may not have an /audio folder.
635
+ - SHA1 folders without audio files are still returned.
636
+ """
637
+ validate_token(token)
638
+
639
+ results = []
640
+
641
+ MEDIA_ROOT = Path("/data/media")
642
+ AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
643
+
644
+ # If media root does not exist, return empty list
645
+ if not MEDIA_ROOT.exists():
646
+ return []
647
+
648
+ for sha1_dir in MEDIA_ROOT.iterdir():
649
+ if not sha1_dir.is_dir():
650
+ continue # skip non-folders
651
+
652
+ audio_dir = sha1_dir / "audio"
653
+
654
+ audio_files = []
655
+ latest_audio = None
656
+
657
+ if audio_dir.exists() and audio_dir.is_dir():
658
+ # Collect all audio files with supported extensions
659
+ files = []
660
+ for pattern in AUDIO_EXTENSIONS:
661
+ files.extend(list(audio_dir.glob(pattern)))
662
+
663
+ # Sort by modification time (newest first)
664
+ files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
665
+
666
+ audio_files = [f.name for f in files]
667
+
668
+ if files:
669
+ latest_audio = files[0].name
670
+
671
+ results.append({
672
+ "sha1": sha1_dir.name,
673
+ "audio_name": latest_audio,
674
+ })
675
+
676
+ return results
677
+
678
+
679
+ @router.post("/upload_audio_version", tags=["Media Manager"])
680
+ async def upload_audio_version(
681
+ audio: UploadFile = File(...),
682
+ sha1: str = Query(..., description="SHA1 of the video folder"),
683
+ version: str = Query(..., description="Version: Salamandra or MoE"),
684
+ token: str = Query(..., description="Token required for authorization")
685
+ ):
686
+ """Upload audio for a given version (Salamandra, MoE).
687
+
688
+ This legacy endpoint keeps its path but now interprets the former
689
+ `subtype` path component as `version`:
690
+ - Target folder: /data/media/<sha1>/<version>/
691
+ - Deletes any previous audio files
692
+ - Saves the new audio
693
+ """
694
+ validate_token(token)
695
+
696
+ if version not in VALID_VERSIONS:
697
+ raise HTTPException(status_code=400, detail="Invalid version")
698
+
699
+ MEDIA_ROOT = Path("/data/media")
700
+ version_dir = MEDIA_ROOT / sha1 / version
701
+ version_dir.mkdir(parents=True, exist_ok=True)
702
+
703
+ # Delete old audio files
704
+ try:
705
+ for pattern in AUDIO_EXTENSIONS:
706
+ for old_audio in version_dir.glob(pattern):
707
+ old_audio.unlink()
708
+ except Exception as exc:
709
+ raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
710
+
711
+ final_path = version_dir / audio.filename
712
+
713
+ try:
714
+ file_bytes = await audio.read()
715
+ with open(final_path, "wb") as f:
716
+ f.write(file_bytes)
717
+ except Exception as exc:
718
+ raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}")
719
+
720
+ return JSONResponse(
721
+ status_code=200,
722
+ content={
723
+ "status": "ok",
724
+ "sha1": sha1,
725
+ "version": version,
726
+ "saved_to": str(final_path)
727
+ }
728
+ )
729
+
730
+
731
+ @router.get("/download_audio_version", tags=["Media Manager"])
732
+ def download_audio_version(
733
+ sha1: str,
734
+ version: str,
735
+ token: str = Query(..., description="Token required for authorization")
736
+ ):
737
+ """Download the first audio file for a given version (Salamandra, MoE)."""
738
+ validate_token(token)
739
+
740
+ if version not in VALID_VERSIONS:
741
+ raise HTTPException(status_code=400, detail="Invalid version")
742
+
743
+ MEDIA_ROOT = Path("/data/media")
744
+ version_dir = MEDIA_ROOT / sha1 / version
745
+
746
+ if not version_dir.exists() or not version_dir.is_dir():
747
+ raise HTTPException(status_code=404, detail=f"{version} folder not found")
748
+
749
+ # Find audio files
750
+ audio_files = []
751
+ for pattern in AUDIO_EXTENSIONS:
752
+ audio_files.extend(list(version_dir.glob(pattern)))
753
+
754
+ if not audio_files:
755
+ raise HTTPException(status_code=404, detail="No audio files found")
756
+
757
+ audio_path = audio_files[0]
758
+
759
+ return FileResponse(
760
+ path=audio_path,
761
+ media_type="audio/" + audio_path.suffix.lstrip("."),
762
+ filename=audio_path.name
763
+ )
764
+
765
+
766
+ @router.post("/upload_audio_ad", tags=["Media Manager"])
767
+ async def upload_audio_ad(
768
+ audio: UploadFile = File(...),
769
+ sha1: str = Query(..., description="SHA1 of the video folder"),
770
+ version: str = Query(..., description="Version: Salamandra or MoE"),
771
+ subtype: str = Query(..., description="Subtype: Original, HITL OK or HITL Test"),
772
+ token: str = Query(..., description="Token required for authorization")
773
+ ):
774
+ validate_token(token)
775
+
776
+ if version not in VALID_VERSIONS:
777
+ raise HTTPException(status_code=400, detail="Invalid version")
778
+ if subtype not in VALID_SUBTYPES:
779
+ raise HTTPException(status_code=400, detail="Invalid subtype")
780
+
781
+ MEDIA_ROOT = Path("/data/media")
782
+ subtype_dir = MEDIA_ROOT / sha1 / version / subtype
783
+ subtype_dir.mkdir(parents=True, exist_ok=True)
784
+
785
+ try:
786
+ for pattern in AUDIO_EXTENSIONS:
787
+ for old_audio in subtype_dir.glob(pattern):
788
+ old_audio.unlink()
789
+ except Exception as exc:
790
+ raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
791
+
792
+ final_path = subtype_dir / audio.filename
793
+
794
+ try:
795
+ file_bytes = await audio.read()
796
+ with open(final_path, "wb") as f:
797
+ f.write(file_bytes)
798
+ except Exception as exc:
799
+ raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}")
800
+
801
+ return JSONResponse(
802
+ status_code=200,
803
+ content={
804
+ "status": "ok",
805
+ "sha1": sha1,
806
+ "version": version,
807
+ "subtype": subtype,
808
+ "saved_to": str(final_path)
809
+ }
810
+ )
811
+
812
+
813
+ @router.get("/download_audio_ad", tags=["Media Manager"])
814
+ def download_audio_ad(
815
+ sha1: str,
816
+ version: str,
817
+ subtype: str,
818
+ token: str = Query(..., description="Token required for authorization")
819
+ ):
820
+ validate_token(token)
821
+
822
+ if version not in VALID_VERSIONS:
823
+ raise HTTPException(status_code=400, detail="Invalid version")
824
+ if subtype not in VALID_SUBTYPES:
825
+ raise HTTPException(status_code=400, detail="Invalid subtype")
826
+
827
+ MEDIA_ROOT = Path("/data/media")
828
+ subtype_dir = MEDIA_ROOT / sha1 / version / subtype
829
+
830
+ if not subtype_dir.exists() or not subtype_dir.is_dir():
831
+ raise HTTPException(status_code=404, detail="Version/subtype folder not found")
832
+
833
+ audio_files = []
834
+ for pattern in AUDIO_EXTENSIONS:
835
+ audio_files.extend(list(subtype_dir.glob(pattern)))
836
+
837
+ if not audio_files:
838
+ raise HTTPException(status_code=404, detail="No audio files found")
839
+
840
+ audio_path = audio_files[0]
841
+
842
+ return FileResponse(
843
+ path=audio_path,
844
+ media_type="audio/" + audio_path.suffix.lstrip("."),
845
+ filename=audio_path.name
846
+ )
847
+
848
+
849
+ @router.get("/list_version_audios", tags=["Media Manager"])
850
+ def list_version_audios(
851
+ sha1: str = Query(..., description="SHA1 of the video folder"),
852
+ token: str = Query(..., description="Token required for authorization")
853
+ ):
854
+ """List the most recent audio file for each version (Salamandra, MoE)
855
+ under /data/media/<sha1>.
856
+
857
+ Returns:
858
+ - sha1: folder name
859
+ - version: name of the version
860
+ - audio_name: latest audio file or None
861
+ """
862
+ validate_token(token)
863
+
864
+ results = []
865
+
866
+ for version in VALID_VERSIONS:
867
+ version_dir = MEDIA_ROOT / sha1 / version
868
+
869
+ latest_audio = None
870
+
871
+ if version_dir.exists() and version_dir.is_dir():
872
+ files = []
873
+ for pattern in AUDIO_EXTENSIONS:
874
+ files.extend(list(version_dir.glob(pattern)))
875
+
876
+ if files:
877
+ # Sort by modification time (newest first)
878
+ files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
879
+ latest_audio = files[0].name
880
+
881
+ results.append({
882
+ "sha1": sha1,
883
+ "version": version,
884
+ "audio_name": latest_audio
885
+ })
886
+
887
+ return results
888
+
889
+
890
+ @router.post("/upload_version_video", tags=["Media Manager"])
891
+ async def upload_version_video(
892
+ sha1: str = Query(..., description="SHA1 associated to the media folder"),
893
+ version: str = Query(..., description="Version: Salamandra or MoE"),
894
+ video: UploadFile = File(...),
895
+ token: str = Query(..., description="Token required for authorization")
896
+ ):
897
+ """Upload a video to /data/media/<sha1>/<version>/.
898
+
899
+ This legacy endpoint keeps its path but now interprets the former
900
+ `subtype` path component as `version`.
901
+ Steps:
902
+ - Validate version.
903
+ - Create version folder if missing.
904
+ - Delete existing MP4 files.
905
+ - Save new MP4.
906
+ """
907
+ validate_token(token)
908
+
909
+ if version not in VALID_VERSIONS:
910
+ raise HTTPException(status_code=400, detail="Invalid version")
911
+
912
+ MEDIA_ROOT = Path("/data/media")
913
+ file_manager = FileManager(MEDIA_ROOT)
914
+
915
+ version_dir = MEDIA_ROOT / sha1 / version
916
+ version_dir.mkdir(parents=True, exist_ok=True)
917
+
918
+ # Remove old mp4 files
919
+ for f in version_dir.glob("*.mp4"):
920
+ f.unlink()
921
+
922
+ file_bytes = await video.read()
923
+ final_path = version_dir / video.filename
924
+
925
+ save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
926
+ if not save_result["operation_success"]:
927
+ raise HTTPException(status_code=500, detail=save_result["error"])
928
+
929
+ return {
930
+ "status": "ok",
931
+ "sha1": sha1,
932
+ "version": version,
933
+ "saved_to": str(final_path)
934
+ }
935
+
936
+
937
+ @router.get("/download_version_video", tags=["Media Manager"])
938
+ def download_version_video(
939
+ sha1: str,
940
+ version: str,
941
+ token: str = Query(..., description="Token required for authorization")
942
+ ):
943
+ """Download the video stored under /data/media/<sha1>/<version>.
944
+ Returns the first MP4 found.
945
+ """
946
+ validate_token(token)
947
+
948
+ if version not in VALID_VERSIONS:
949
+ raise HTTPException(status_code=400, detail="Invalid version")
950
+
951
+ MEDIA_ROOT = Path("/data/media")
952
+ file_manager = FileManager(MEDIA_ROOT)
953
+
954
+ version_dir = MEDIA_ROOT / sha1 / version
955
+
956
+ if not version_dir.exists() or not version_dir.is_dir():
957
+ raise HTTPException(status_code=404, detail="Version folder not found")
958
+
959
+ mp4_files = list(version_dir.glob("*.mp4"))
960
+ if not mp4_files:
961
+ raise HTTPException(status_code=404, detail="No MP4 files found")
962
+
963
+ video_path = mp4_files[0]
964
+ relative_path = video_path.relative_to(MEDIA_ROOT)
965
+
966
+ handler = file_manager.get_file(relative_path)
967
+ if handler is None:
968
+ raise HTTPException(status_code=404, detail="Video not accessible")
969
+
970
+ handler.close()
971
+
972
+ return FileResponse(
973
+ path=video_path,
974
+ media_type="video/mp4",
975
+ filename=video_path.name
976
+ )
977
+
978
+
979
+ @router.get("/list_version_videos", tags=["Media Manager"])
980
+ def list_version_videos(
981
+ sha1: str,
982
+ token: str = Query(..., description="Token required for authorization")
983
+ ):
984
+ """List the most recent .mp4 video for each version (Salamandra, MoE)
985
+ inside /data/media/<sha1>.
986
+
987
+ Returns:
988
+ - sha1
989
+ - version
990
+ - video_name (latest mp4 or None)
991
+ """
992
+ validate_token(token)
993
+
994
+ MEDIA_ROOT = Path("/data/media")
995
+
996
+ results = []
997
+
998
+ for version in VALID_VERSIONS:
999
+ version_dir = MEDIA_ROOT / sha1 / version
1000
+
1001
+ latest_video = None
1002
+
1003
+ if version_dir.exists() and version_dir.is_dir():
1004
+ files = list(version_dir.glob("*.mp4"))
1005
+ if files:
1006
+ files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
1007
+ latest_video = files[0].name
1008
+
1009
+ results.append({
1010
+ "sha1": sha1,
1011
+ "version": version,
1012
+ "video_name": latest_video
1013
+ })
1014
+
1015
+ return results