Initial haunt-fm implementation
Full music recommendation pipeline: listening history capture via webhook, Last.fm candidate discovery, iTunes preview download, CLAP audio embeddings (512-dim), pgvector cosine similarity recommendations, playlist generation with known/new track interleaving, and Music Assistant playback via HA. Includes: FastAPI app, SQLAlchemy models, Alembic migrations, Docker Compose with pgvector/pg17, status dashboard, and all API endpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
63
src/haunt_fm/api/playlists.py
Normal file
63
src/haunt_fm/api/playlists.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from haunt_fm.db import get_session
|
||||
from haunt_fm.models.track import PlaylistTrack, Track
|
||||
from haunt_fm.services.music_assistant import play_playlist_on_speaker
|
||||
from haunt_fm.services.playlist_generator import generate_playlist
|
||||
|
||||
router = APIRouter(prefix="/api/playlists")
|
||||
|
||||
|
||||
class GenerateRequest(BaseModel):
|
||||
total_tracks: int = 20
|
||||
known_pct: int = 30
|
||||
name: str | None = None
|
||||
speaker_entity: str | None = None
|
||||
auto_play: bool = False
|
||||
|
||||
|
||||
@router.post("/generate")
|
||||
async def generate(req: GenerateRequest, session: AsyncSession = Depends(get_session)):
|
||||
playlist = await generate_playlist(
|
||||
session,
|
||||
total_tracks=req.total_tracks,
|
||||
known_pct=req.known_pct,
|
||||
name=req.name,
|
||||
)
|
||||
|
||||
# Load playlist tracks with track info
|
||||
result = await session.execute(
|
||||
select(PlaylistTrack, Track)
|
||||
.join(Track, PlaylistTrack.track_id == Track.id)
|
||||
.where(PlaylistTrack.playlist_id == playlist.id)
|
||||
.order_by(PlaylistTrack.position)
|
||||
)
|
||||
rows = result.all()
|
||||
|
||||
track_list = [
|
||||
{
|
||||
"position": pt.position,
|
||||
"artist": t.artist,
|
||||
"title": t.title,
|
||||
"album": t.album,
|
||||
"is_known": pt.is_known,
|
||||
"similarity_score": pt.similarity_score,
|
||||
}
|
||||
for pt, t in rows
|
||||
]
|
||||
|
||||
# Auto-play if requested
|
||||
if req.auto_play and req.speaker_entity:
|
||||
await play_playlist_on_speaker(track_list, req.speaker_entity)
|
||||
|
||||
return {
|
||||
"playlist_id": playlist.id,
|
||||
"name": playlist.name,
|
||||
"total_tracks": playlist.total_tracks,
|
||||
"known_pct": playlist.known_pct,
|
||||
"tracks": track_list,
|
||||
"auto_played": req.auto_play and req.speaker_entity is not None,
|
||||
}
|
||||
Reference in New Issue
Block a user