Music discovery backend for Animo. Users answer a short survey (seed artists, a vibe, a niche slider, a few preferred genres) and get back a small pack of tracks that lean toward the deeper, less-crowded corners of YouTube Music. Every track is enriched with Spotify metadata and carries an AI-written "living description" that keeps evolving as people comment on it.
Beyond discovery the service also backs vouching, comments and reactions, binders (9-slot collections), friends, and song sends.
Runs as a plain FastAPI app for local development and as an Azure Functions v2
ASGI wrapper in production (function_app.py wraps the same ASGI app).
POST /open-packruns the discovery engine for the current user. Given survey inputs plus their prior liked-genre history, it walks the YouTube Music related-tracks graph, filters by a view-count window (the niche slider controls how mainstream that window is), enriches every hit through Spotify, and writes the tracks back to Supabase. Discovery and enrichment fan out across a thread pool so a five-track pack usually lands in about a second instead of five.POST /vouch/{track_id}records a per-user vouch (one per user per track, idempotent) and merges the track's genre tags into that user's favorite genres so future packs get smarter.POST /commentsaves a comment on a track. Every fifth comment triggers a background Azure OpenAI call that rewrites the track's people-voice description from the accumulated community reactions.GET /search-globalandPOST /search-global/selectlet a user pull any YouTube Music track into the archive on demand.
Auth is handled with Supabase JWTs, verified against the project's JWKS
endpoint (with a manual cache-bust on key rotation) or the legacy HS256 shared
secret depending on the token's alg.
- FastAPI, Python 3.11+
- Supabase (Postgres + auth)
- ytmusicapi + spotipy — discovery and enrichment
- Azure OpenAI — the two-voice living descriptions
- colorthief + Pillow — album-art palette extraction
Requires a Supabase project with the migrations in supabase/migrations/
applied (in numeric order), a Spotify API app, an Azure OpenAI deployment, and
— only if you want to run the Functions host locally — Azure Functions Core
Tools v4.
python3.11 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt -r requirements-dev.txt
cp .env.template .env # then fill it inNames only — see .env.template. Never commit real values.
| Variable | Purpose |
|---|---|
SUPABASE_URL |
Project URL |
SUPABASE_KEY |
Anon key |
SUPABASE_SERVICE_ROLE_KEY |
Service-role key (server-side only) |
SUPABASE_JWT_SECRET |
JWT secret, for symmetric token verification |
SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET |
Spotify enrichment |
AZURE_OPENAI_API_KEY / AZURE_OPENAI_ENDPOINT / AZURE_OPENAI_DEPLOYMENT_NAME |
Living descriptions |
GOOGLE_CLIENT_ID |
Google OAuth |
ANIMO_ALLOWED_ORIGINS |
Comma-separated CORS origins. Defaults to localhost dev ports; set this in production |
ANIMO_LOG_LEVEL |
Log level, defaults to INFO |
uvicorn main:app --reload # http://localhost:8000, docs at /docs
func start # Azure Functions host, http://localhost:7071pytest179 unit tests over the pure helpers (streaks, binder slot validation,
taste-twin scoring, voice normalization, reaction aggregation, friendship
status, song sends, first-finder credit, palette). Tests marked integration
hit the live network and are excluded by default — see pytest.ini.
Auth required unless marked public.
| Method | Path | Description |
|---|---|---|
| GET | / |
Root ping (public) |
| GET | /health |
Health check with database reachability; 503 when the database is down (public) |
| GET | /me |
Profile, streak, survey state, unopened send count |
| Method | Path | Description |
|---|---|---|
| POST | /open-pack |
Run the discovery engine and return today's enriched pack |
| GET | /my-tracks |
The user's library, newest first, with first-finder credit and perceptions |
| GET | /tracks/{track_id} |
One track (public) |
| GET | /tracks/{track_id}/history |
People-voice description history, newest first |
| GET | /search-global |
Global YouTube Music search, top 5 matches (public) |
| POST | /search-global/select |
Resolve a search result into a full Animo card |
| Method | Path | Description |
|---|---|---|
| GET | /comments/{track_id} |
50 most recent comments with reaction counts (public) |
| POST | /comment |
Add a comment; every 5th regenerates the people-voice |
| POST | /comments/{comment_id}/react |
Toggle one of: fire, heart, laugh, skull, eyes |
| GET | /vouches |
Track IDs the user has vouched for |
| POST | /vouch/{track_id} |
Idempotent vouch, one per user per track |
| Method | Path | Description |
|---|---|---|
| GET | /friends |
Accepted friends with profile info and taste-twin flag |
| GET | /friends/requests |
Incoming pending requests |
| POST | /friends/request/{target_user_id} |
Send a request |
| POST | /friends/accept/{friendship_id} |
Accept |
| POST | /friends/decline/{friendship_id} |
Decline |
| DELETE | /friends/{friendship_id} |
Unfriend; either party may delete |
| GET | /users/search |
Search users by email prefix |
| GET | /users/{target_user_id}/profile |
Public profile |
| Method | Path | Description |
|---|---|---|
| POST | /sends |
Send a track to a friend, with an optional note |
| GET | /conversations |
Per-partner conversation summaries, most recent first |
| GET | /conversations/{other_user_id} |
One conversation thread |
| POST | /sends/{send_id}/open |
Mark opened; recipient only, idempotent |
| POST | /sends/{send_id}/add |
Copy the track into the recipient's library |
| POST | /sends/{send_id}/pass |
Mark passed; recipient only |
Binders are 9-slot collections.
| Method | Path | Description |
|---|---|---|
| POST | /binders |
Create |
| GET | /binders/{binder_id} |
One binder with slot tracks (public) |
| PATCH | /binders/{binder_id} |
Update title/description/cover mode; owner only |
| DELETE | /binders/{binder_id} |
Delete, cascading to slots and vouches; owner only |
| PUT | /binders/{binder_id}/slots |
Set/move tracks between slots atomically; owner only |
| GET | /users/{target_user_id}/binders |
A user's binders |
| POST | /binders/{binder_id}/vouch |
Idempotent binder vouch |
main.pyFastAPI app, auth middleware, routesdiscovery.pyYouTube Music niche crawlermetadata.pySpotify enrichment (spotipy)agent.pyAzure OpenAI two-voice description generatordatabase.pySupabase data layerpalette.pyalbum-art palette extractionstreaks.pypure streak calculationfunction_app.pyAzure Functions v2 ASGI wrapperapi/routes/route modules (health check, etc.)supabase/migrations/schema migrations, applied in numeric ordertests/unit tests