Skip to content

Latest commit

 

History

History
193 lines (132 loc) · 8.36 KB

File metadata and controls

193 lines (132 loc) · 8.36 KB

Distill

Turn text and images into a playlist on Tidal or Spotify.

Quick start

Prerequisites: Node.js 20+, pnpm

pnpm install
pnpm setup          # creates .env with DST_SESSION_PASSWORD

Edit .env and add:

  1. DST_TIDAL_CLIENT_ID and/or DST_SPOTIFY_CLIENT_ID (see OAuth apps below)
  2. DST_OPENAI_API_KEY or DST_GEMINI_API_KEY
pnpm dev

Open http://localhost:3000. Connect a streaming platform, choose an LLM provider, paste text or upload images.

See docs/configuration.md for production deployment, Spotify dev-mode allowlist, and troubleshooting.

Who registers what

Role What they need
You (operator) OAuth developer apps, LLM API key, DST_SESSION_PASSWORD in .env
End user (clicks Connect) Their own Tidal or Spotify account — no developer app, no API keys

Distill is bring-your-own-keys: each deployment uses your developer apps and your LLM billing.

Credentials

All Distill-owned variables use the DST_ prefix. Generic names like OPENAI_API_KEY are ignored (so shell exports do not leak into the app).

Variable Required for app? Where to get it
DST_SESSION_PASSWORD Yes (auto-generated by pnpm setup) openssl rand -base64 32
DST_TIDAL_CLIENT_ID If using Tidal Tidal Developer Dashboard
DST_SPOTIFY_CLIENT_ID If using Spotify Spotify Developer Dashboard
DST_OPENAI_API_KEY or DST_GEMINI_API_KEY Yes (one) OpenAI / Google AI Studio
DST_TIDAL_CLIENT_SECRET CLI eval only Same Tidal app (client credentials)
DST_TIDAL_REFRESH_TOKEN export-fixtures only Dev endpoint while logged in
DST_LLM_PROVIDER Optional Locks UI provider choice (openai or gemini)
DST_LLM_EXTRACT_SYSTEM_PROMPT Optional Override song-extraction system prompt (see below)
DST_LLM_PLAYLIST_METADATA_SYSTEM_PROMPT Optional Override playlist name/description prompt (see below)

OAuth apps

Redirect URIs are not env vars — register these in each provider dashboard:

Provider Local redirect URI
Tidal http://localhost:3000/api/oauth/callback/tidal
Spotify https://127.0.0.1:3000/api/oauth/callback/spotify (HTTPS required; use pnpm dev:https)

Production: https://<your-domain>/api/oauth/callback/{tidal|spotify}

Distill builds redirect URIs from the request origin (X-Forwarded-Host / X-Forwarded-Proto behind a reverse proxy). Set DST_PUBLIC_URL when the public URL differs (e.g. a tunnel). Spotify maps localhost to 127.0.0.1 automatically.

Tidal: public client, PKCE. Scopes: playlists.read playlists.write search.read.

Spotify: public client, PKCE. Scopes: playlist-modify-public playlist-modify-private. Playlists are created private.

Spotify dev-mode gotcha: new apps start in Development mode. Only Spotify accounts on the app's User Management allowlist can sign in. Add your own email before testing Connect locally. End users never register their own app, but in dev mode you must add each user's email. For public hosting, request Extended Quota Mode / complete Spotify app review.

Song extraction (LLM)

Parsing uses a pluggable adapter layer in server/utils/llm/.

In the app: choose OpenAI or Gemini on the connect screen and enter an API key (unless the operator configured keys in server .env or locked the provider via DST_LLM_PROVIDER).

Provider Text Images API key
OpenAI Yes Yes Server .env or browser entry on connect
Gemini Yes Yes Server .env or browser entry on connect
Anthropic Coming soon

Browser BYOK: when the operator has not set DST_OPENAI_API_KEY / DST_GEMINI_API_KEY in .env, end users enter their own key on the connect screen. The key is stored in browser localStorage, verified with a quick test, and sent only when parsing — never written to server .env or a database.

Operator server keys: set DST_OPENAI_API_KEY and/or DST_GEMINI_API_KEY in .env to skip browser key entry for all users on that instance.

Optional env:

Variable Default Purpose
DST_LLM_PROVIDER (unset) Force openai or gemini; overrides UI
DST_OPENAI_MODEL auto OpenAI model (gpt-4o-mini for images when auto)
DST_GEMINI_MODEL gemini-2.5-flash Gemini model id

CLI scripts (debug:parse, eval:image) require DST_LLM_PROVIDER when not passed another way.

LLM prompts

Two system prompts control LLM behavior. Override via env (use \n for line breaks in a single-line value, or a multiline quoted string in .env):

Variable Used for
DST_LLM_EXTRACT_SYSTEM_PROMPT Parsing text/images into { songs: [...] } JSON
DST_LLM_PLAYLIST_METADATA_SYSTEM_PROMPT Suggesting playlist name and description after extraction

Default DST_LLM_EXTRACT_SYSTEM_PROMPT:

You extract song titles and artists from user-provided text and images.
Return ONLY valid JSON: { "songs": [{ "title", "artist", "album?", "confidence": "high"|"medium"|"low", "source": "text"|"image" }] }
Rules:
- Handle numbered lists, "Artist — Title", setlists, screenshots, social posts
- Deduplicate by artist+title (case insensitive)
- Omit non-song content (headers, dates, venue names without songs)
- If unsure, use lower confidence
- album is optional

Default DST_LLM_PLAYLIST_METADATA_SYSTEM_PROMPT:

You create concise playlist metadata from a track list.
Return ONLY valid JSON: { "name": string, "description": string }
Rules:
- name should be short, natural, and playlist-like
- description should be one sentence, no more than 140 characters
- Do not mention that you are an AI
- Do not include markdown

DST_SESSION_PASSWORD

Encrypts OAuth tokens in cookies. Must stay stable across restarts in production — changing it invalidates all sessions.

In development, if unset, Distill generates an ephemeral key (sessions won't survive restart). pnpm setup generates one for you.

If you changed DST_SESSION_PASSWORD after logging in, log out and reconnect — old cookies won't decrypt.

Flow

  1. Connect Tidal or Spotify (allow popups)
  2. Choose an LLM provider and enter an API key (or use operator-configured server keys)
  3. Paste text or upload images (max 5 per request)
  4. Review matched tracks (edit, reorder, add more)
  5. Create or update playlist

Dry run (dev only)

Toggle Dry run in the dev UI to explore the flow with fixtures — no API keys or OAuth required.

Troubleshooting

Symptom Fix
Tidal error 11102 Redirect URI mismatch — check dashboard vs actual URL (scheme, host, port, path)
401 / "Connect first" Session expired or DST_SESSION_PASSWORD changed — reconnect
Spotify OAuth fails for some users App in Development mode — add their Spotify email under User Management
Gemini rate limit Check billing; try DST_GEMINI_MODEL=gemini-2.5-flash
Tidal rate limit Tune DST_TIDAL_MAX_* or wait
Popup doesn't open Allow popups for localhost
426 Upgrade Required on localhost Custom dev port: set NUXT_HOST=:: in .env (dual-stack) — see docs/configuration.md

Match eval (golden fixtures)

See docs/testing.md. Requires DST_TIDAL_CLIENT_ID + DST_TIDAL_CLIENT_SECRET (client credentials, separate from user OAuth).

pnpm export-fixtures --playlist https://tidal.com/browse/playlist/{uuid} --parsed fixtures/parsed.json
pnpm eval:matches

DST_TIDAL_REFRESH_TOKEN: with pnpm dev running and Tidal connected, open http://localhost:3000/api/dev/tidal-refresh-token.

Debug image parse

pnpm debug:parse --image fixtures/images/setlist.jpg

With dev server: add "debug": true to /api/parse body. Press ** in the UI for the dev HUD (non-production only).

Development

pnpm test:unit
pnpm test:llm-parse

See CONTRIBUTING.md.

License

MIT — see LICENSE.