Versos
Turns a DJ set's tracklist into a playlist. Started as Tracklister in 2024 and now on its fifth version, going from one Next.js app to Cloudflare, a queued monorepo, and finally Effect and Apple Music.
The first cut. One Next.js app that parsed tracklists with an LLM, matched them against a pgvector index, and wrote the result to Spotify.
what and why
the first version of the idea: paste a tracklist from a DJ set, get a Spotify playlist back. everything, from parsing to matching, lived in one Next.js app.
how it works
-
Sign in with Spotify
- Lucia handles sessions, Spotify OAuth grants playlist access
-
LLM parse
- the raw tracklist goes through the AI SDK’s
generateObjectwith a zod schema - I swapped between GPT-3.5, Claude 3 Haiku, and Llama on Groq to compare speed and quality
- the raw tracklist goes through the AI SDK’s
-
Embed
- each parsed track is embedded locally with transformers.js
- embeddings are cached in an LRU so repeat tracks skip the model
-
Vector search
- Postgres with pgvector finds the nearest tracks by cosine similarity
- each result gets a high, medium, or low confidence, and low ones are flagged for review
-
Playlist
- matches are looked up on Spotify and the accepted ones become a playlist
tech stack
- Next.js, tRPC, React Query, Tailwind
- Drizzle, libSQL for app data, Postgres + pgvector for embeddings
- Lucia and Arctic for auth
- AI SDK with OpenAI, Anthropic, and Groq
- transformers.js for embeddings
Rebuilt on Cloudflare. The whole pipeline runs in a Durable Object, streams progress over SSE, and matches tracks with Vectorize.
what and why
I listen to a lot of DJ mixes and sets and the YouTube comments will almost always have a timestamped tracklist. so I then would have to go through each song one by one and find it on Spotify, SoundCloud, or YouTube. this was annoying to do manually
now with LLMs being great at handling messy text like this when given the proper prompt we can easily transform a tracklist into searchable queries.
// Input"[47:58] Juelz & JAWNS - Enter The World"
// Output{ "artist": "Juelz", "title": "Enter The World"}
// Input"40:56 Special Request - Pull Up (Tim Reaper Remix)"
// Output{ "artist": "Special Request", "title": "Pull Up (Tim Reaper Remix)"}there are definitely still cases where the LLM performs poorly though. honestly this one is just hard to parse anyways
// Input"[31:29] Joy Orbison & Vice Selex vs. Sage The Gemini & IAMSU! - Flight FM vs. Gas Pedal (RL Grime Edit)"
// Output{ "artist": "Joy Orbison & Vice Selex vs. Sage The Gemini & IAMSU!", "title": "Flight FM vs. Gas Pedal (RL Grime Edit)"}how it works
-
User Input
- Raw tracklist is submitted by user
-
LLM Parse & Clean
- LLM processes raw input to standardize format
- Extracts artist and title information
- Cleans up common formatting issues and variations
-
Embedding Creation
- Creates vector embeddings for each parsed track
- Embeddings capture semantic meaning of track information
-
Vector Database Search
- Queries vector database using created embeddings
- Finds similar tracks based on semantic similarity
- Calculates confidence scores for potential matches
-
LLM Verification
- LLM evaluates matches from vector search
- Uses vector search results and additional context
- Makes intelligent matching decisions based on multiple factors
-
Spotify Search
- Queries Spotify API with verified matches
- Finds exact tracks in Spotify’s catalog
- Returns final track matches for playlist creation
-
User Review & Playlist Creation
- User reviews results
- Creates new Spotify playlists with accepted results
tech stack
- Frontend: React, Next.js, React Query, TypeScript
- Backend: Hono, TypeScript, Drizzle, Cloudflare D1, Workers, Durable Objects, Vectorize
after learning about all these Cloudflare services, I knew when I built this version of the app I wanted to try them all out. the whole process runs in a durable object and I use a server-sent event(SSE) to notify the frontend of the process state. so the loading bar actually means something!
I just learned about SSEs and they are very cool. it was very easy to set up with hono and a lot easier than webhooks.
this was a learning process for me so there is probably a lot of room for improvement especially in the whole llm, embedding, vector search area.
future plans
- improve LLM performance
- mobile design
- more social features
- allow user to correct parsing mistakes
Rebuilt as a monorepo. A Hono API with a BullMQ queue does the work, Gemini parses, and an eval suite keeps the parser honest.
what changed
v2 ran the whole pipeline inside one Durable Object. this version split it into a proper monorepo: a Next.js frontend and a separate Hono API, with the actual work moved onto a job queue.
how it works
-
Paste
- the tracklist goes to the API over tRPC
-
Queue
- a BullMQ worker backed by Redis picks up the job, so a long tracklist doesn’t hang a request
- progress streams back to the browser over SSE
-
Parse
- Gemini parses the text through the AI SDK
- the parser can call database tools to check what it’s reading against the catalog
-
Match and enrich
- tracks are matched against Spotify and the Postgres catalog
- album data is enriched and cached in Redis
-
Playlist
- the accepted matches become a Spotify playlist
evals
this is the first version where I measured the parser instead of eyeballing it. an Evalite suite runs the parser against known tracklists, and Langfuse traces every LLM call.
tech stack
- Frontend: Next.js 15, React Query, Motion
- Backend: Hono, tRPC, BullMQ
- Data: PostgreSQL with Drizzle, Redis
- AI: AI SDK v5 with Gemini
- Evals and tracing: Evalite, Langfuse
- Tooling: Turborepo, Bun, Biome
The big migration. Next.js out, TanStack Start in. Effect under the API. Spotify out, Apple Music in.
what changed
almost everything. over about six months the app was moved piece by piece onto a new stack while it kept working:
- frontend: Next.js to Vite with TanStack Router, then TanStack Start
- backend: the API services were rewritten as Effect services on a
ManagedRuntime, and the queue manager became an EffectPlaylistJobsservice - platform: Apple Music joined with an ISRC resolver, and then Spotify was removed entirely
- auth: Better Auth with passkeys replaced the old session code
- deploy: Railway, with the web app, API, worker, Redis, and Postgres as separate services
how it works
-
Paste
- a tracklist, sent over tRPC
-
Parse
- Gemini parses the text, and heuristics enrich the tracklist before matching
-
Match
- tracks are matched against Apple Music and the local catalog, with ISRCs used to line up the same recording across sources
-
Playlist
- the playlist is created in Apple Music through MusicKit
release radar
v4 also added the first feature that isn’t about tracklists: Release Radar, which scans a list of your artists and builds a playlist of new releases.
tech stack
- Frontend: TanStack Start, TanStack Router, TanStack Form, React Compiler, Base UI, Tailwind v4
- Backend: Hono, Effect, tRPC, BullMQ
- Data: PostgreSQL with Drizzle, Redis
- AI: AI SDK with Gemini
- Auth: Better Auth with passkeys
- Hosting: Railway
Tracklister, renamed. Paste text, a YouTube link, or a 1001Tracklists page, and get an Apple Music playlist, with a record of why each track was chosen.
what changed
Tracklister became Versos. the stack from v4 stayed, but the backend moved to Effect v4, and the product grew past pasting text.
how it works
-
Press
- paste a tracklist, a YouTube URL, or a 1001Tracklists URL. tracklists from The Lot Radio can be extracted too
-
Parse
- Jev or Gemini turns the text into artist and track pairs
- if both fail, a deterministic parser takes over so the import still finishes
-
Match
- candidates come from Apple Music and the local catalog
- Jev judges each one as the same recording, a related version, or a different song
-
Playlist
- the playlist is created in Apple Music, and can be public or private
every import stores a decision record: the input, which parser ran, the candidates for each track, and the match it chose. so when a match is wrong I can see exactly why.
radar
Release Radar became Radar. every week it scans your artists for Friday releases and builds a playlist of what came out.
tech stack
- Frontend: TanStack Start, React 19, Tailwind v4, shadcn/ui, Jotai, Motion
- Backend: Hono, Effect v4, tRPC, BullMQ
- Data: PostgreSQL with Drizzle, Redis
- AI: AI SDK with Gemini, Jev
- Auth: Better Auth with email, password, and passkeys
- Observability: OpenTelemetry, evlog, Axiom
- Hosting: Railway
- Tooling: Turborepo, Bun, Ultracite, Vitest
what’s next
moving off Railway onto Cloudflare. the plan is two Workers defined with Alchemy, D1 as the database, and Workflows in place of BullMQ and Redis, so there’s nothing always-on to pay for.