diff --git a/backend/src/database.py b/backend/src/database.py index 4dac2f1..b20bc0e 100644 --- a/backend/src/database.py +++ b/backend/src/database.py @@ -151,4 +151,9 @@ async def remove_artwork(clip_id: str): async def delete_clip(clip_id: str): async with aiosqlite.connect(DB_PATH) as db: await db.execute("DELETE FROM clips WHERE id = ?", (clip_id,)) + await db.commit() + +async def delete_all_clips(): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM clips") await db.commit() \ No newline at end of file diff --git a/backend/src/main.py b/backend/src/main.py index d9d9aa6..ca31a07 100644 --- a/backend/src/main.py +++ b/backend/src/main.py @@ -10,7 +10,7 @@ from dotenv import load_dotenv load_dotenv(Path(__file__).parent.parent / ".env") from models import GenerateRequest, GenerateResponse, ClipList, RenameRequest, LYRIA_MODELS, DEFAULT_MODEL -from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork +from database import init_db, create_clip, get_clips, get_clip, delete_clip, update_clip_status, rename_clip, set_artwork, remove_artwork, delete_all_clips from lyria_client import generate_clip, save_clip_file from waveform import generate_waveform_svg, get_waveform_path @@ -226,6 +226,42 @@ async def rename_clip_endpoint(clip_id: str, request: RenameRequest): updated = await get_clip(clip_id) return updated +# ─── Admin Endpoints ────────────────────────────────────────────── + +@app.post("/api/admin/clear-waveforms") +async def clear_waveforms(): + """Delete all cached waveform SVGs. They regenerate on next view.""" + from waveform import WAVEFORM_DIR + count = 0 + if WAVEFORM_DIR.exists(): + for f in WAVEFORM_DIR.glob("*.svg"): + f.unlink() + count += 1 + return {"cleared": count} + +@app.post("/api/admin/delete-all-clips") +async def delete_all_clips_endpoint(): + """Delete all clips, MP3s, artwork, and waveforms. Nuclear option.""" + # Get all clips to clean up files + all_clips = await get_clips() + for clip in all_clips: + # Delete MP3 + mp3_path = CLIPS_DIR / clip["filename"] + if mp3_path.exists(): + mp3_path.unlink() + # Delete artwork + if clip.get("artwork_filename"): + art_path = ARTWORK_DIR / clip["artwork_filename"] + if art_path.exists(): + art_path.unlink() + # Delete waveform + wf_path = get_waveform_path(clip["id"]) + if wf_path and wf_path.exists(): + wf_path.unlink() + # Clear DB + await delete_all_clips() + return {"deleted": len(all_clips)} + if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg b/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg deleted file mode 100644 index f4aa175..0000000 --- a/data/waveforms/7239af1a-2a3f-46bd-816a-2e349752cb83.svg +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f0dd840..3fcbaa3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import Generator from './components/Generator' import Gallery from './components/Gallery' import Player from './components/Player' import DetailPanel from './components/DetailPanel' +import SettingsModal from './components/SettingsModal' interface Clip { id: string @@ -22,6 +23,7 @@ function App() { const [currentClip, setCurrentClip] = useState(null) const [isPlaying, setIsPlaying] = useState(false) const [selectedClip, setSelectedClip] = useState(null) + const [settingsOpen, setSettingsOpen] = useState(false) const fetchClips = async () => { try { @@ -117,15 +119,48 @@ function App() { } } + const handleClearWaveforms = async () => { + try { + await fetch('/api/admin/clear-waveforms', { method: 'POST' }) + } catch (err) { + console.error('Failed to clear waveforms:', err) + } + } + + const handleDeleteAll = async () => { + try { + await fetch('/api/admin/delete-all-clips', { method: 'POST' }) + setClips([]) + setCurrentClip(null) + setIsPlaying(false) + setSelectedClip(null) + } catch (err) { + console.error('Failed to delete all clips:', err) + } + } + return (
-
-
- L +
+
+
+ L +
+

Lyria Studio

+ v0.1
-

Lyria Studio

- v0.1 +
@@ -168,6 +203,14 @@ function App() { onRemoveArtwork={handleRemoveArtwork} /> )} + + setSettingsOpen(false)} + onClearWaveforms={handleClearWaveforms} + onDeleteAll={handleDeleteAll} + clipCount={clips.length} + />
) } diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx new file mode 100644 index 0000000..dfcde65 --- /dev/null +++ b/frontend/src/components/SettingsModal.tsx @@ -0,0 +1,129 @@ +import { useState } from 'react' + +interface Props { + isOpen: boolean + onClose: () => void + onClearWaveforms: () => Promise + onDeleteAll: () => Promise + clipCount: number +} + +export default function SettingsModal({ isOpen, onClose, onClearWaveforms, onDeleteAll, clipCount }: Props) { + const [confirmDelete, setConfirmDelete] = useState(false) + const [loading, setLoading] = useState(false) + + if (!isOpen) return null + + const handleClearWaveforms = async () => { + setLoading(true) + try { + await onClearWaveforms() + } finally { + setLoading(false) + } + } + + const handleDeleteAll = async () => { + if (!confirmDelete) { + setConfirmDelete(true) + return + } + setLoading(true) + try { + await onDeleteAll() + setConfirmDelete(false) + } finally { + setLoading(false) + } + } + + return ( +
+ {/* Backdrop */} +
+ + {/* Modal */} +
+ {/* Header */} +
+

Settings

+ +
+ + {/* Content */} +
+ {/* Danger Zone */} +
+
+
+ + + + Danger Zone +
+
+ +
+ {/* Clear Waveforms */} +
+
+

Clear Waveform Cache

+

Delete all cached SVGs. They regenerate on next view.

+
+ +
+ +
+ + {/* Delete All */} +
+
+
+

Delete All Tracks

+

+ Remove {clipCount} clip{clipCount !== 1 ? 's' : ''}, MP3s, artwork, and waveforms. Cannot be undone. +

+
+ +
+ + {confirmDelete && ( +

+ Click again to confirm. This permanently deletes everything. +

+ )} +
+
+
+
+
+
+ ) +}