feat: Add album artwork support

- Backend: artwork_filename column on clips table (auto-migration)
- Backend: PATCH /api/clips/{id}/artwork (upload image, 5MB limit)
- Backend: GET /api/clips/{id}/artwork (serve image with correct content-type)
- Backend: DELETE /api/clips/{id}/artwork (remove artwork file + null out DB)
- Backend: DELETE /api/clips/{id} now cascade-removes artwork file
- Frontend: Clip interface includes artwork_filename
- Frontend: Gallery ribbons show artwork thumbnail with hover overlay (play + upload)
- Frontend: Gallery tiles show artwork with hover overlay (play + upload)
- Frontend: DetailPanel shows full artwork with upload/change/remove controls
- Frontend: App.tsx handleUploadArtwork + handleRemoveArtwork handlers
- Storage: /data/artwork/ directory for image files
This commit is contained in:
rin.kagamine committed 2026-05-29 18:27:28 -04:00
1 parent 23672387e5
commit 8ba7133269
10 files changed
+1030 -59

No files matched your search

+80
View File
@@ -2,13 +2,18 @@ import { useState, useEffect } from 'react'
import Generator from './components/Generator'
import Gallery from './components/Gallery'
import Player from './components/Player'
import DetailPanel from './components/DetailPanel'
interface Clip {
id: string
title?: string
prompt: string
model: string
duration_seconds: number
status: string
error_message?: string
file_size_bytes?: number
artwork_filename?: string | null
created_at: string
}
@@ -16,6 +21,7 @@ function App() {
const [clips, setClips] = useState<Clip[]>([])
const [currentClip, setCurrentClip] = useState<Clip | null>(null)
const [isPlaying, setIsPlaying] = useState(false)
const [selectedClip, setSelectedClip] = useState<Clip | null>(null)
const fetchClips = async () => {
try {
@@ -51,6 +57,66 @@ function App() {
}
}
const handleRename = async (id: string, title: string) => {
try {
await fetch(`/api/clips/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
})
setClips(prev => prev.map(c => c.id === id ? { ...c, title } : c))
if (currentClip?.id === id) {
setCurrentClip({ ...currentClip, title })
}
if (selectedClip?.id === id) {
setSelectedClip({ ...selectedClip, title })
}
} catch (err) {
console.error('Failed to rename clip:', err)
}
}
const handleUploadArtwork = async (id: string, file: File) => {
try {
const formData = new FormData()
formData.append('file', file)
const res = await fetch(`/api/clips/${id}/artwork`, {
method: 'PATCH',
body: formData,
})
if (!res.ok) {
const err = await res.json()
console.error('Artwork upload failed:', err.detail)
return
}
const updated = await res.json()
setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: updated.artwork_filename } : c))
if (currentClip?.id === id) {
setCurrentClip({ ...currentClip, artwork_filename: updated.artwork_filename })
}
if (selectedClip?.id === id) {
setSelectedClip({ ...selectedClip, artwork_filename: updated.artwork_filename })
}
} catch (err) {
console.error('Failed to upload artwork:', err)
}
}
const handleRemoveArtwork = async (id: string) => {
try {
await fetch(`/api/clips/${id}/artwork`, { method: 'DELETE' })
setClips(prev => prev.map(c => c.id === id ? { ...c, artwork_filename: null } : c))
if (currentClip?.id === id) {
setCurrentClip({ ...currentClip, artwork_filename: null })
}
if (selectedClip?.id === id) {
setSelectedClip({ ...selectedClip, artwork_filename: null })
}
} catch (err) {
console.error('Failed to remove artwork:', err)
}
}
return (
<div className="min-h-screen bg-lyria-dark">
<header className="border-b border-white/10 px-6 py-4">
@@ -72,6 +138,9 @@ function App() {
clips={clips}
onPlay={handlePlay}
onDelete={handleDelete}
onRename={handleRename}
onSelect={(clip) => setSelectedClip(clip)}
onUploadArtwork={handleUploadArtwork}
currentClipId={currentClip?.id}
/>
</div>
@@ -88,6 +157,17 @@ function App() {
}}
/>
)}
{selectedClip && (
<DetailPanel
clip={selectedClip}
onClose={() => setSelectedClip(null)}
onRename={handleRename}
onDelete={handleDelete}
onUploadArtwork={handleUploadArtwork}
onRemoveArtwork={handleRemoveArtwork}
/>
)}
</div>
)
}