feat: MP3 download/export capability
Backend:
- New endpoint: GET /api/clips/{id}/download
- Returns MP3 with Content-Disposition: attachment header
- Filename derived from clip title (sanitized) or prompt, falls back to clip ID
- Sanitization: alphanumeric + spaces/hyphens/underscores only, max 60 chars
- Uses FastAPI FileResponse with media_type=audio/mpeg
Frontend:
- Gallery ribbons: Download button (downward arrow icon) next to delete button
- Uses window.open() to trigger browser download
- Hover: lyria-accent color
- Gallery tiles: Download button in tile footer action row
- Small icon (w-3 h-3), same hover behavior
- DetailPanel: Full-width 'Download MP3' button above Delete Track
- lyria-accent styled, with download icon
- Uses <a href> with download attribute for native browser handling
Architecture KB updated with Export/Download section.
This commit is contained in:
1 parent
57b9f9d78e
commit
119b75fbcd
3 files changed
+60
-1
No files matched your search
@@ -113,6 +113,31 @@ async def stream_audio(clip_id: str):
|
||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||
return FileResponse(file_path, media_type="audio/mpeg", filename=clip["filename"])
|
||||
|
||||
@app.get("/api/clips/{clip_id}/download")
|
||||
async def download_clip(clip_id: str):
|
||||
clip = await get_clip(clip_id)
|
||||
if not clip:
|
||||
raise HTTPException(status_code=404, detail="Clip not found")
|
||||
file_path = CLIPS_DIR / clip["filename"]
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Audio file not found")
|
||||
|
||||
# Build download filename from title or prompt
|
||||
base_name = clip.get("title") or clip["prompt"]
|
||||
# Sanitize for filesystem
|
||||
safe_name = "".join(c for c in base_name if c.isalnum() or c in " -_").strip()
|
||||
safe_name = safe_name[:60] # Limit length
|
||||
if not safe_name:
|
||||
safe_name = clip_id
|
||||
download_filename = f"{safe_name}.mp3"
|
||||
|
||||
return FileResponse(
|
||||
file_path,
|
||||
media_type="audio/mpeg",
|
||||
filename=download_filename,
|
||||
content_disposition_type="attachment"
|
||||
)
|
||||
|
||||
@app.delete("/api/clips/{clip_id}")
|
||||
async def remove_clip(clip_id: str):
|
||||
clip = await get_clip(clip_id)
|
||||
|
||||
Reference in new issue
Block a user