feat: genre field in DetailPanel and ID3 TCON tag
- Added genre column to clips table (auto-migration) - Genre field in DetailPanel metadata section (between Album and Comment) - Genre saved via PATCH /api/clips/:id/metadata - Genre embedded in ID3 TCON tag on download - Placeholder text: 'e.g. Hardcore Techno, Phonk, Dark Ambient...'
This commit is contained in:
1 parent
1903d8bd2a
commit
bef707f9a4
5 files changed
+29
-12
No files matched your search
@@ -98,6 +98,11 @@ async def init_db():
|
||||
await db.execute("ALTER TABLE clips ADD COLUMN comment TEXT")
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
# Migration: add genre column if it doesn't exist
|
||||
try:
|
||||
await db.execute("ALTER TABLE clips ADD COLUMN genre TEXT")
|
||||
except Exception:
|
||||
pass # Column already exists
|
||||
await db.commit()
|
||||
|
||||
async def create_clip(prompt: str, model: str, duration: int, prompt_structure: str = None) -> str:
|
||||
@@ -176,11 +181,11 @@ async def rename_clip(clip_id: str, title: str):
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
async def update_metadata(clip_id: str, artist: str = None, album: str = None, comment: str = None):
|
||||
async def update_metadata(clip_id: str, artist: str = None, album: str = None, comment: str = None, genre: str = None):
|
||||
async with aiosqlite.connect(DB_PATH) as db:
|
||||
await db.execute(
|
||||
"UPDATE clips SET artist = ?, album = ?, comment = ? WHERE id = ?",
|
||||
(artist, album, comment, clip_id)
|
||||
"UPDATE clips SET artist = ?, album = ?, comment = ?, genre = ? WHERE id = ?",
|
||||
(artist, album, comment, genre, clip_id)
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
+6
-2
@@ -135,7 +135,7 @@ async def stream_audio(clip_id: str):
|
||||
return FileResponse(file_path, media_type="audio/mpeg", filename=clip["filename"])
|
||||
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.id3 import ID3, TIT1, TIT2, TPE1, TALB, COMM, TBPM, TKEY, APIC, PictureType
|
||||
from mutagen.id3 import ID3, TIT1, TIT2, TPE1, TALB, COMM, TBPM, TKEY, APIC, PictureType, TCON
|
||||
|
||||
@app.get("/api/clips/{clip_id}/download")
|
||||
async def download_clip(clip_id: str):
|
||||
@@ -197,6 +197,10 @@ async def download_clip(clip_id: str):
|
||||
if clip.get("key"):
|
||||
audio.tags["TKEY"] = TKEY(encoding=3, text=clip["key"])
|
||||
|
||||
# Genre
|
||||
if clip.get("genre"):
|
||||
audio.tags["TCON"] = TCON(encoding=3, text=clip["genre"])
|
||||
|
||||
# Album art
|
||||
if clip.get("artwork_filename"):
|
||||
artwork_path = ARTWORK_DIR / clip["artwork_filename"]
|
||||
@@ -496,7 +500,7 @@ async def update_clip_metadata(clip_id: str, request: MetadataRequest):
|
||||
clip = await get_clip(clip_id)
|
||||
if not clip:
|
||||
raise HTTPException(status_code=404, detail="Clip not found")
|
||||
await update_metadata(clip_id, request.artist, request.album, request.comment)
|
||||
await update_metadata(clip_id, request.artist, request.album, request.comment, request.genre)
|
||||
updated = await get_clip(clip_id)
|
||||
return updated
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ class MetadataRequest(BaseModel):
|
||||
artist: Optional[str] = None
|
||||
album: Optional[str] = None
|
||||
comment: Optional[str] = None
|
||||
genre: Optional[str] = None
|
||||
|
||||
class GenerateArtworkRequest(BaseModel):
|
||||
prompt: str
|
||||
|
||||
@@ -190,12 +190,12 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateMetadata = async (id: string, artist: string, album: string, comment: string) => {
|
||||
const handleUpdateMetadata = async (id: string, artist: string, album: string, genre: string, comment: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/clips/${id}/metadata`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ artist, album, comment }),
|
||||
body: JSON.stringify({ artist, album, genre, comment }),
|
||||
})
|
||||
if (res.ok) {
|
||||
const updated = await res.json()
|
||||
|
||||
@@ -17,6 +17,7 @@ interface Clip {
|
||||
prompt_structure?: Record<string, string> | null
|
||||
artist?: string | null
|
||||
album?: string | null
|
||||
genre?: string | null
|
||||
comment?: string | null
|
||||
created_at: string
|
||||
}
|
||||
@@ -33,7 +34,7 @@ interface Props {
|
||||
onPlay?: (clip: Clip) => void
|
||||
onTogglePlay?: () => void
|
||||
onGenerateArtwork?: (clip: Clip) => void
|
||||
onUpdateMetadata?: (id: string, artist: string, album: string, comment: string) => void
|
||||
onUpdateMetadata?: (id: string, artist: string, album: string, genre: string, comment: string) => void
|
||||
}
|
||||
|
||||
const modelName = (modelId: string) => {
|
||||
@@ -383,25 +384,31 @@ export default function DetailPanel({ clip, isPlaying, currentClipId, onClose, o
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metadata Fields (Artist, Album, Comment) */}
|
||||
{/* Metadata Fields (Artist, Album, Genre, Comment) */}
|
||||
<div className="space-y-3">
|
||||
<MetadataField
|
||||
label="Artist"
|
||||
value={clip.artist || ''}
|
||||
placeholder="Artist name..."
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, val, clip.album || '', clip.comment || '')}
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, val, clip.album || '', clip.genre || '', clip.comment || '')}
|
||||
/>
|
||||
<MetadataField
|
||||
label="Album"
|
||||
value={clip.album || ''}
|
||||
placeholder="Album name..."
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, clip.artist || '', val, clip.comment || '')}
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, clip.artist || '', val, clip.genre || '', clip.comment || '')}
|
||||
/>
|
||||
<MetadataField
|
||||
label="Genre"
|
||||
value={clip.genre || ''}
|
||||
placeholder="e.g. Hardcore Techno, Phonk, Dark Ambient..."
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, clip.artist || '', clip.album || '', val, clip.comment || '')}
|
||||
/>
|
||||
<MetadataField
|
||||
label="Comment"
|
||||
value={clip.comment || ''}
|
||||
placeholder="Notes, credits, etc..."
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, clip.artist || '', clip.album || '', val)}
|
||||
onSave={(val) => onUpdateMetadata?.(clip.id, clip.artist || '', clip.album || '', clip.genre || '', val)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user