VeuReu commited on
Commit
c820145
·
verified ·
1 Parent(s): 31d4d14

Upload media_routers.py

Browse files
Files changed (1) hide show
  1. storage/media_routers.py +963 -800
storage/media_routers.py CHANGED
@@ -1,800 +1,963 @@
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_SUBTYPES = ("HITL", "MoE", "Salamandra")
21
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
22
-
23
-
24
- @router.delete("/clear_media", tags=["Media Manager"])
25
- def clear_media(token: str = Query(..., description="Token required for authorization")):
26
- """
27
- Delete all contents of the /data/media folder.
28
-
29
- Steps:
30
- - Validate the token.
31
- - Ensure the folder exists.
32
- - Delete all files and subfolders inside /data/media.
33
- - Return a JSON response confirming the deletion.
34
-
35
- Warning: This will remove all stored videos, clips, and cast CSV files.
36
- """
37
- validate_token(token)
38
-
39
- if not MEDIA_ROOT.exists() or not MEDIA_ROOT.is_dir():
40
- raise HTTPException(status_code=404, detail="/data/media folder does not exist")
41
-
42
- # Delete contents
43
- for item in MEDIA_ROOT.iterdir():
44
- try:
45
- if item.is_dir():
46
- shutil.rmtree(item)
47
- else:
48
- item.unlink()
49
- except Exception as e:
50
- raise HTTPException(status_code=500, detail=f"Failed to delete {item}: {e}")
51
-
52
- return {"status": "ok", "message": "All media files deleted successfully"}
53
-
54
- @router.post("/upload_cast_csv", tags=["Media Manager"])
55
- async def upload_cast_csv(
56
- sha1: str,
57
- cast_file: UploadFile = File(...),
58
- token: str = Query(..., description="Token required for authorization")
59
- ):
60
- """
61
- Upload a cast CSV file for a specific video identified by its SHA-1.
62
-
63
- The CSV will be stored under:
64
- /data/media/<sha1>/cast/cast.csv
65
-
66
- Steps:
67
- - Validate the token.
68
- - Ensure /data/media/<sha1> exists.
69
- - Create /cast folder if missing.
70
- - Save the CSV file inside /cast.
71
- """
72
- validate_token(token)
73
-
74
- base_folder = MEDIA_ROOT / sha1
75
- if not base_folder.exists() or not base_folder.is_dir():
76
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
77
-
78
- cast_folder = base_folder / "cast"
79
- cast_folder.mkdir(parents=True, exist_ok=True)
80
-
81
- final_path = cast_folder / "cast.csv"
82
-
83
- file_bytes = await cast_file.read()
84
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
85
- if not save_result["operation_success"]:
86
- raise HTTPException(status_code=500, detail=save_result["error"])
87
-
88
- return JSONResponse(
89
- status_code=200,
90
- content={"status": "ok", "saved_to": str(final_path)}
91
- )
92
-
93
-
94
- @router.get("/download_cast_csv", tags=["Media Manager"])
95
- def download_cast_csv(
96
- sha1: str,
97
- token: str = Query(..., description="Token required for authorization")
98
- ):
99
- """
100
- Download the cast CSV for a specific video identified by its SHA-1.
101
-
102
- The CSV is expected under:
103
- /data/media/<sha1>/cast/cast.csv
104
-
105
- Steps:
106
- - Validate the token.
107
- - Ensure /data/media/<sha1> and /cast exist.
108
- - Return the CSV as a FileResponse.
109
- - Raise 404 if any folder or file is missing.
110
- """
111
- MEDIA_ROOT = Path("/data/media")
112
- file_manager = FileManager(MEDIA_ROOT)
113
- HF_TOKEN = os.getenv("HF_TOKEN")
114
- validate_token(token)
115
-
116
- base_folder = MEDIA_ROOT / sha1
117
- cast_folder = base_folder / "cast"
118
- csv_path = cast_folder / "cast.csv"
119
-
120
- if not base_folder.exists() or not base_folder.is_dir():
121
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
122
- if not cast_folder.exists() or not cast_folder.is_dir():
123
- raise HTTPException(status_code=404, detail="Cast folder not found")
124
- if not csv_path.exists() or not csv_path.is_file():
125
- raise HTTPException(status_code=404, detail="Cast CSV not found")
126
-
127
- # Convert to relative path for FileManager
128
- relative_path = csv_path.relative_to(MEDIA_ROOT)
129
- handler = file_manager.get_file(relative_path)
130
- if handler is None:
131
- raise HTTPException(status_code=404, detail="Cast CSV not accessible")
132
- handler.close()
133
-
134
- return FileResponse(
135
- path=csv_path,
136
- media_type="text/csv",
137
- filename="cast.csv"
138
- )
139
-
140
- @router.post("/upload_original_video", tags=["Media Manager"])
141
- async def upload_video(
142
- video: UploadFile = File(...),
143
- token: str = Query(..., description="Token required for authorization")
144
- ):
145
- """
146
- Saves an uploaded video by hashing it with SHA1 and placing it under:
147
- /data/media/<sha1>/clip/<original_filename>
148
-
149
- Behavior:
150
- - Compute SHA1 of the uploaded video.
151
- - Ensure folder structure exists.
152
- - Delete any existing .mp4 files under /clip.
153
- - Save the uploaded video in the clip folder.
154
- """
155
- MEDIA_ROOT = Path("/data/media")
156
- file_manager = FileManager(MEDIA_ROOT)
157
- HF_TOKEN = os.getenv("HF_TOKEN")
158
- validate_token(token)
159
-
160
- # Read content into memory (needed to compute hash twice)
161
- file_bytes = await video.read()
162
-
163
- # Create an in-memory file handler for hashing
164
- file_handler = io.BytesIO(file_bytes)
165
-
166
- # Compute SHA1
167
- try:
168
- sha1 = file_manager.compute_sha1(file_handler)
169
- except Exception as exc:
170
- raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
171
-
172
- # Ensure /data/media exists
173
- MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
174
-
175
- # Path: /data/media/<sha1>
176
- video_root = MEDIA_ROOT / sha1
177
- video_root.mkdir(parents=True, exist_ok=True)
178
-
179
- # Path: /data/media/<sha1>/clip
180
- clip_dir = video_root / "clip"
181
- clip_dir.mkdir(parents=True, exist_ok=True)
182
-
183
- # Delete old MP4 files
184
- try:
185
- for old_mp4 in clip_dir.glob("*.mp4"):
186
- old_mp4.unlink()
187
- except Exception as exc:
188
- raise HTTPException(status_code=500, detail=f"Failed to delete old videos: {exc}")
189
-
190
- # Save new video path
191
- final_path = clip_dir / video.filename
192
-
193
- # Save file
194
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
195
-
196
- if not save_result["operation_success"]:
197
- raise HTTPException(status_code=500, detail=save_result["error"])
198
-
199
- return JSONResponse(
200
- status_code=200,
201
- content={
202
- "status": "ok",
203
- "sha1": sha1,
204
- "saved_to": str(final_path)
205
- }
206
- )
207
-
208
-
209
- @router.get("/download_original_video", tags=["Media Manager"])
210
- def download_video(
211
- sha1: str,
212
- token: str = Query(..., description="Token required for authorization")
213
- ):
214
- """
215
- Download a stored video by its SHA-1 directory name.
216
-
217
- This endpoint looks for a video stored under the path:
218
- /data/media/<sha1>/clip/
219
- and returns the first MP4 file found in that folder.
220
-
221
- The method performs the following steps:
222
- - Checks if the SHA-1 folder exists inside the media root.
223
- - Validates that the "clip" subfolder exists.
224
- - Searches for the first .mp4 file inside the clip folder.
225
- - Uses the FileManager.get_file method to ensure the file is accessible.
226
- - Returns the video directly as a FileResponse.
227
-
228
- Parameters
229
- ----------
230
- sha1 : str
231
- The SHA-1 hash corresponding to the directory where the video is stored.
232
-
233
- Returns
234
- -------
235
- FileResponse
236
- A streaming response containing the MP4 video.
237
-
238
- Raises
239
- ------
240
- HTTPException
241
- - 404 if the SHA-1 folder does not exist.
242
- - 404 if the clip folder is missing.
243
- - 404 if no MP4 files are found.
244
- - 404 if the file cannot be retrieved using FileManager.
245
- """
246
- MEDIA_ROOT = Path("/data/media")
247
- file_manager = FileManager(MEDIA_ROOT)
248
- HF_TOKEN = os.getenv("HF_TOKEN")
249
- validate_token(token)
250
-
251
- sha1_folder = MEDIA_ROOT / sha1
252
- clip_folder = sha1_folder / "clip"
253
-
254
- if not sha1_folder.exists() or not sha1_folder.is_dir():
255
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
256
-
257
- if not clip_folder.exists() or not clip_folder.is_dir():
258
- raise HTTPException(status_code=404, detail="Clip folder not found")
259
-
260
- # Find first MP4 file
261
- mp4_files = list(clip_folder.glob("*.mp4"))
262
- if not mp4_files:
263
- raise HTTPException(status_code=404, detail="No MP4 files found")
264
-
265
- video_path = mp4_files[0]
266
-
267
- # Convert to relative path for FileManager
268
- relative_path = video_path.relative_to(MEDIA_ROOT)
269
-
270
- handler = file_manager.get_file(relative_path)
271
- if handler is None:
272
- raise HTTPException(status_code=404, detail="Video not accessible")
273
-
274
- handler.close()
275
-
276
- return FileResponse(
277
- path=video_path,
278
- media_type="video/mp4",
279
- filename=video_path.name
280
- )
281
-
282
- @router.get("/list_original_videos", tags=["Media Manager"])
283
- def list_all_videos(
284
- token: str = Query(..., description="Token required for authorization")
285
- ):
286
- """
287
- List all videos stored under /data/media.
288
-
289
- For each SHA1 folder, the endpoint returns:
290
- - sha1: folder name
291
- - video_files: list of mp4 files inside /clip
292
- - latest_video: the most recently modified mp4
293
- - video_count: total number of mp4 files
294
-
295
- Notes:
296
- - Videos may not have a /clip folder.
297
- - SHA1 folders without mp4 files are still returned.
298
- """
299
- validate_token(token)
300
-
301
- results = []
302
-
303
- # If media root does not exist, return empty list
304
- if not MEDIA_ROOT.exists():
305
- return []
306
-
307
- for sha1_dir in MEDIA_ROOT.iterdir():
308
- if not sha1_dir.is_dir():
309
- continue # skip non-folders
310
-
311
- clip_dir = sha1_dir / "clip"
312
-
313
- videos = []
314
- latest_video = None
315
-
316
- if clip_dir.exists() and clip_dir.is_dir():
317
- mp4_files = list(clip_dir.glob("*.mp4"))
318
-
319
- # Sort by modification time (newest first)
320
- mp4_files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
321
-
322
- videos = [f.name for f in mp4_files]
323
-
324
- if mp4_files:
325
- latest_video = mp4_files[0].name
326
-
327
- results.append({
328
- "sha1": sha1_dir.name,
329
- "video_name": latest_video
330
- })
331
-
332
- return results
333
-
334
- @router.post("/upload_original_audio", tags=["Media Manager"])
335
- async def upload_audio(
336
- audio: UploadFile = File(...),
337
- token: str = Query(..., description="Token required for authorization")
338
- ):
339
- """
340
- Saves an uploaded audio file by hashing it with SHA1 and placing it under:
341
- /data/media/<sha1>/audio/<original_filename>
342
-
343
- Behavior:
344
- - Compute SHA1 of the uploaded audio.
345
- - Ensure folder structure exists.
346
- - Delete any existing audio files under /audio.
347
- - Save the uploaded audio in the audio folder.
348
- """
349
- MEDIA_ROOT = Path("/data/media")
350
- file_manager = FileManager(MEDIA_ROOT)
351
- HF_TOKEN = os.getenv("HF_TOKEN")
352
- validate_token(token)
353
-
354
- # Read content into memory (needed to compute hash twice)
355
- file_bytes = await audio.read()
356
-
357
- # Create an in-memory file handler for hashing
358
- file_handler = io.BytesIO(file_bytes)
359
-
360
- # Compute SHA1
361
- try:
362
- sha1 = file_manager.compute_sha1(file_handler)
363
- except Exception as exc:
364
- raise HTTPException(status_code=500, detail=f"SHA1 computation failed: {exc}")
365
-
366
- # Ensure /data/media exists
367
- MEDIA_ROOT.mkdir(parents=True, exist_ok=True)
368
-
369
- # Path: /data/media/<sha1>
370
- audio_root = MEDIA_ROOT / sha1
371
- audio_root.mkdir(parents=True, exist_ok=True)
372
-
373
- # Path: /data/media/<sha1>/audio
374
- audio_dir = audio_root / "audio"
375
- audio_dir.mkdir(parents=True, exist_ok=True)
376
-
377
- # Delete old audio files
378
- AUDIO_EXTENSIONS = ("*.mp3", "*.wav", "*.m4a", "*.aac", "*.ogg", "*.flac")
379
- try:
380
- for pattern in AUDIO_EXTENSIONS:
381
- for old_audio in audio_dir.glob(pattern):
382
- old_audio.unlink()
383
- except Exception as exc:
384
- raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
385
-
386
- # Final save path
387
- final_path = audio_dir / audio.filename
388
-
389
- # Save file
390
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
391
-
392
- if not save_result["operation_success"]:
393
- raise HTTPException(status_code=500, detail=save_result["error"])
394
-
395
- return JSONResponse(
396
- status_code=200,
397
- content={
398
- "status": "ok",
399
- "sha1": sha1,
400
- "saved_to": str(final_path)
401
- }
402
- )
403
-
404
- @router.get("/download_original_audio", tags=["Media Manager"])
405
- def download_audio(
406
- sha1: str,
407
- token: str = Query(..., description="Token required for authorization")
408
- ):
409
- """
410
- Download a stored audio file by its SHA-1 directory name.
411
-
412
- This endpoint looks for audio stored under the path:
413
- /data/media/<sha1>/audio/
414
- and returns the first audio file found in that folder.
415
-
416
- The method performs the following steps:
417
- - Checks if the SHA-1 folder exists inside the media root.
418
- - Validates that the "audio" subfolder exists.
419
- - Searches for the first supported audio file.
420
- - Uses FileManager.get_file to ensure the file is accessible.
421
- - Returns the audio file as a FileResponse.
422
-
423
- Parameters
424
- ----------
425
- sha1 : str
426
- The SHA-1 hash corresponding to the directory where the audio is stored.
427
-
428
- Returns
429
- -------
430
- FileResponse
431
- A streaming response containing the audio file.
432
-
433
- Raises
434
- ------
435
- HTTPException
436
- - 404 if the SHA-1 folder does not exist.
437
- - 404 if the audio folder is missing.
438
- - 404 if no audio files are found.
439
- - 404 if the file cannot be retrieved using FileManager.
440
- """
441
- MEDIA_ROOT = Path("/data/media")
442
- file_manager = FileManager(MEDIA_ROOT)
443
- HF_TOKEN = os.getenv("HF_TOKEN")
444
- validate_token(token)
445
-
446
- sha1_folder = MEDIA_ROOT / sha1
447
- audio_folder = sha1_folder / "audio"
448
-
449
- if not sha1_folder.exists() or not sha1_folder.is_dir():
450
- raise HTTPException(status_code=404, detail="SHA1 folder not found")
451
-
452
- if not audio_folder.exists() or not audio_folder.is_dir():
453
- raise HTTPException(status_code=404, detail="Audio folder not found")
454
-
455
- # Supported audio extensions
456
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
457
-
458
- audio_files = []
459
- for pattern in AUDIO_EXTENSIONS:
460
- audio_files.extend(list(audio_folder.glob(pattern)))
461
-
462
- if not audio_files:
463
- raise HTTPException(status_code=404, detail="No audio files found")
464
-
465
- audio_path = audio_files[0]
466
-
467
- # Convert to relative path for FileManager
468
- relative_path = audio_path.relative_to(MEDIA_ROOT)
469
-
470
- handler = file_manager.get_file(relative_path)
471
- if handler is None:
472
- raise HTTPException(status_code=404, detail="Audio file not accessible")
473
-
474
- handler.close()
475
-
476
- # Guess media type based on extension (simple)
477
- media_type = "audio/" + audio_path.suffix.lstrip(".")
478
-
479
- return FileResponse(
480
- path=audio_path,
481
- media_type=media_type,
482
- filename=audio_path.name
483
- )
484
-
485
- @router.get("/list_original_audios", tags=["Media Manager"])
486
- def list_all_audios(
487
- token: str = Query(..., description="Token required for authorization")
488
- ):
489
- """
490
- List all audio files stored under /data/media.
491
-
492
- For each SHA1 folder, the endpoint returns:
493
- - sha1: folder name
494
- - audio_files: list of audio files inside /audio
495
- - latest_audio: the most recently modified audio file
496
- - audio_count: total number of audio files
497
-
498
- Notes:
499
- - Folders may not have an /audio folder.
500
- - SHA1 folders without audio files are still returned.
501
- """
502
- validate_token(token)
503
-
504
- results = []
505
-
506
- MEDIA_ROOT = Path("/data/media")
507
- AUDIO_EXTENSIONS = ["*.mp3", "*.wav", "*.aac", "*.m4a", "*.ogg", "*.flac"]
508
-
509
- # If media root does not exist, return empty list
510
- if not MEDIA_ROOT.exists():
511
- return []
512
-
513
- for sha1_dir in MEDIA_ROOT.iterdir():
514
- if not sha1_dir.is_dir():
515
- continue # skip non-folders
516
-
517
- audio_dir = sha1_dir / "audio"
518
-
519
- audio_files = []
520
- latest_audio = None
521
-
522
- if audio_dir.exists() and audio_dir.is_dir():
523
- # Collect all audio files with supported extensions
524
- files = []
525
- for pattern in AUDIO_EXTENSIONS:
526
- files.extend(list(audio_dir.glob(pattern)))
527
-
528
- # Sort by modification time (newest first)
529
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
530
-
531
- audio_files = [f.name for f in files]
532
-
533
- if files:
534
- latest_audio = files[0].name
535
-
536
- results.append({
537
- "sha1": sha1_dir.name,
538
- "audio_name": latest_audio,
539
- })
540
-
541
- return results
542
-
543
-
544
- @router.post("/upload_audio_subtype", tags=["Media Manager"])
545
- async def upload_audio_subtype(
546
- audio: UploadFile = File(...),
547
- sha1: str = Query(..., description="SHA1 of the video folder"),
548
- subtype: str = Query(..., description="Subtype: HITL, MoE, or Salamandra"),
549
- token: str = Query(..., description="Token required for authorization")
550
- ):
551
- """
552
- Upload audio for a given subtype (HITL, MoE, Salamandra).
553
- - Creates folder if missing: /data/media/<sha1>/<subtype>/
554
- - Deletes any previous audio files
555
- - Saves the new audio
556
- """
557
- validate_token(token)
558
-
559
- if subtype not in VALID_SUBTYPES:
560
- raise HTTPException(status_code=400, detail=f"subtype must be one of {VALID_SUBTYPES}")
561
-
562
- MEDIA_ROOT = Path("/data/media")
563
- subtype_dir = MEDIA_ROOT / sha1 / subtype
564
- subtype_dir.mkdir(parents=True, exist_ok=True)
565
-
566
- # Delete old audio files
567
- try:
568
- for pattern in AUDIO_EXTENSIONS:
569
- for old_audio in subtype_dir.glob(pattern):
570
- old_audio.unlink()
571
- except Exception as exc:
572
- raise HTTPException(status_code=500, detail=f"Failed to delete old audio files: {exc}")
573
-
574
- final_path = subtype_dir / audio.filename
575
-
576
- try:
577
- file_bytes = await audio.read()
578
- with open(final_path, "wb") as f:
579
- f.write(file_bytes)
580
- except Exception as exc:
581
- raise HTTPException(status_code=500, detail=f"Failed to save audio: {exc}")
582
-
583
- return JSONResponse(
584
- status_code=200,
585
- content={
586
- "status": "ok",
587
- "sha1": sha1,
588
- "subtype": subtype,
589
- "saved_to": str(final_path)
590
- }
591
- )
592
-
593
-
594
- @router.get("/download_audio_subtype", tags=["Media Manager"])
595
- def download_audio_subtype(
596
- sha1: str,
597
- subtype: str,
598
- token: str = Query(..., description="Token required for authorization")
599
- ):
600
- """
601
- Download the first audio file for a given subtype.
602
- """
603
- validate_token(token)
604
-
605
- if subtype not in VALID_SUBTYPES:
606
- raise HTTPException(status_code=400, detail=f"subtype must be one of {VALID_SUBTYPES}")
607
-
608
- MEDIA_ROOT = Path("/data/media")
609
- subtype_dir = MEDIA_ROOT / sha1 / subtype
610
-
611
- if not subtype_dir.exists() or not subtype_dir.is_dir():
612
- raise HTTPException(status_code=404, detail=f"{subtype} folder not found")
613
-
614
- # Find audio files
615
- audio_files = []
616
- for pattern in AUDIO_EXTENSIONS:
617
- audio_files.extend(list(subtype_dir.glob(pattern)))
618
-
619
- if not audio_files:
620
- raise HTTPException(status_code=404, detail="No audio files found")
621
-
622
- audio_path = audio_files[0]
623
-
624
- return FileResponse(
625
- path=audio_path,
626
- media_type="audio/" + audio_path.suffix.lstrip("."),
627
- filename=audio_path.name
628
- )
629
-
630
-
631
- @router.get("/list_subtype_audios", tags=["Media Manager"])
632
- def list_subtype_audios(
633
- sha1: str = Query(..., description="SHA1 of the video folder"),
634
- token: str = Query(..., description="Token required for authorization")
635
- ):
636
- """
637
- List the most recent audio file for each subtype (HITL, MoE, Salamandra)
638
- under /data/media/<sha1>.
639
-
640
- Returns:
641
- - sha1: folder name
642
- - subtype: name of the subtype
643
- - audio_name: latest audio file or None
644
- """
645
- validate_token(token)
646
-
647
- results = []
648
-
649
- for subtype in VALID_SUBTYPES:
650
- subtype_dir = MEDIA_ROOT / sha1 / subtype
651
-
652
- latest_audio = None
653
-
654
- if subtype_dir.exists() and subtype_dir.is_dir():
655
- files = []
656
- for pattern in AUDIO_EXTENSIONS:
657
- files.extend(list(subtype_dir.glob(pattern)))
658
-
659
- if files:
660
- # Sort by modification time (newest first)
661
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
662
- latest_audio = files[0].name
663
-
664
- results.append({
665
- "sha1": sha1,
666
- "subtype": subtype,
667
- "audio_name": latest_audio
668
- })
669
-
670
- return results
671
-
672
-
673
- @router.post("/upload_subtype_video", tags=["Media Manager"])
674
- async def upload_subtype_video(
675
- sha1: str = Query(..., description="SHA1 associated to the media folder"),
676
- subtype: str = Query(..., description="Subtype: HITL, MoE or Salamandra"),
677
- video: UploadFile = File(...),
678
- token: str = Query(..., description="Token required for authorization")
679
- ):
680
- """
681
- Upload a video to /data/media/<sha1>/<subtype>/.
682
- Steps:
683
- - Validate subtype.
684
- - Create subtype folder if missing.
685
- - Delete existing MP4 files.
686
- - Save new MP4.
687
- """
688
- validate_token(token)
689
-
690
- VALID_SUBTYPES = ("HITL", "MoE", "Salamandra")
691
- if subtype not in VALID_SUBTYPES:
692
- raise HTTPException(status_code=400, detail="Invalid subtype")
693
-
694
- MEDIA_ROOT = Path("/data/media")
695
- file_manager = FileManager(MEDIA_ROOT)
696
-
697
- subtype_dir = MEDIA_ROOT / sha1 / subtype
698
- subtype_dir.mkdir(parents=True, exist_ok=True)
699
-
700
- # Remove old mp4 files
701
- for f in subtype_dir.glob("*.mp4"):
702
- f.unlink()
703
-
704
- file_bytes = await video.read()
705
- final_path = subtype_dir / video.filename
706
-
707
- save_result = file_manager.upload_file(io.BytesIO(file_bytes), final_path)
708
- if not save_result["operation_success"]:
709
- raise HTTPException(status_code=500, detail=save_result["error"])
710
-
711
- return {
712
- "status": "ok",
713
- "sha1": sha1,
714
- "subtype": subtype,
715
- "saved_to": str(final_path)
716
- }
717
-
718
-
719
- @router.get("/download_subtype_video", tags=["Media Manager"])
720
- def download_subtype_video(
721
- sha1: str,
722
- subtype: str,
723
- token: str = Query(..., description="Token required for authorization")
724
- ):
725
- """
726
- Download the video stored under /data/media/<sha1>/<subtype>.
727
- Returns the first MP4 found.
728
- """
729
- validate_token(token)
730
-
731
- VALID_SUBTYPES = ("HITL", "MoE", "Salamandra")
732
- if subtype not in VALID_SUBTYPES:
733
- raise HTTPException(status_code=400, detail="Invalid subtype")
734
-
735
- MEDIA_ROOT = Path("/data/media")
736
- file_manager = FileManager(MEDIA_ROOT)
737
-
738
- subtype_dir = MEDIA_ROOT / sha1 / subtype
739
-
740
- if not subtype_dir.exists() or not subtype_dir.is_dir():
741
- raise HTTPException(status_code=404, detail="Subtype folder not found")
742
-
743
- mp4_files = list(subtype_dir.glob("*.mp4"))
744
- if not mp4_files:
745
- raise HTTPException(status_code=404, detail="No MP4 files found")
746
-
747
- video_path = mp4_files[0]
748
- relative_path = video_path.relative_to(MEDIA_ROOT)
749
-
750
- handler = file_manager.get_file(relative_path)
751
- if handler is None:
752
- raise HTTPException(status_code=404, detail="Video not accessible")
753
-
754
- handler.close()
755
-
756
- return FileResponse(
757
- path=video_path,
758
- media_type="video/mp4",
759
- filename=video_path.name
760
- )
761
-
762
-
763
- @router.get("/list_subtype_videos", tags=["Media Manager"])
764
- def list_subtype_videos(
765
- sha1: str,
766
- token: str = Query(..., description="Token required for authorization")
767
- ):
768
- """
769
- List the most recent .mp4 video for each subtype (HITL, MoE, Salamandra)
770
- inside /data/media/<sha1>.
771
- Returns:
772
- - sha1
773
- - subtype
774
- - video_name (latest mp4 or None)
775
- """
776
- validate_token(token)
777
-
778
- MEDIA_ROOT = Path("/data/media")
779
- VALID_SUBTYPES = ("HITL", "MoE", "Salamandra")
780
-
781
- results = []
782
-
783
- for subtype in VALID_SUBTYPES:
784
- subtype_dir = MEDIA_ROOT / sha1 / subtype
785
-
786
- latest_video = None
787
-
788
- if subtype_dir.exists() and subtype_dir.is_dir():
789
- files = list(subtype_dir.glob("*.mp4"))
790
- if files:
791
- files.sort(key=lambda f: f.stat().st_mtime, reverse=True)
792
- latest_video = files[0].name
793
-
794
- results.append({
795
- "sha1": sha1,
796
- "subtype": subtype,
797
- "video_name": latest_video
798
- })
799
-
800
- 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
+
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_subtype", tags=["Media Manager"])
628
+ async def upload_audio_subtype(
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_subtype", tags=["Media Manager"])
680
+ def download_audio_subtype(
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_subtype_audios", tags=["Media Manager"])
798
+ def list_subtype_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_subtype_video", tags=["Media Manager"])
839
+ async def upload_subtype_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_subtype_video", tags=["Media Manager"])
886
+ def download_subtype_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_subtype_videos", tags=["Media Manager"])
928
+ def list_subtype_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