All projects
07AI APP2026
Tinnymix app icon: a chunky purple TNX wordmark on a mint green rounded square

Tinnymix

A Spotify companion for people who think in mixtapes — paste a YouTube tracklist or describe a mood, and get a real playlist with cover art on your account.

Spotify companion that turns YouTube tracklists and free-text moods into real playlists with AI-picked cover art, plus a Wrapped-style listening dashboard — all client-side.

Tinnymix wordmark: TNX set in an extruded outlined typeface, purple on a mint green pill

Overview

Tinnymix turns the two ways people actually assemble playlists into one product. The first is bulk import: paste the `00:00 Artist — Title` tracklist from under a YouTube mix, and every line is parsed, searched on Spotify, shown with swappable candidates, and shipped as a new playlist with a cover. The second is generation: pick a mood — Rainy Sunday, Late-night drive, Heartbreak, Pre-club — or describe a vibe in free text, and Gemini drafts the tracklist along with a playlist name, description and cover keywords. On top of those sit playlist management (rename, edit by describing the change, add and remove tracks, swap covers), a set of tools for duplicates, comparisons and cleanup, a Wrapped-style stats surface with top artists and tracks, a genre fingerprint, a listening timeline, a concerts view and a year-in-review, a Followed Artists Atlas that plots the artists a listener follows on a world map, and the original local-first cassette composer the project started as.

The architecturally interesting choice is that there is no backend for user data. Spotify authentication runs entirely in the browser over PKCE — no client secret, no session server, no auth library — and the resulting access and refresh tokens are written into IndexedDB through Dexie alongside tapes, playlist snapshots, saved cards, custom palettes and play history. Because the OAuth callback lands on a server route that has no access to the app's Dexie context, the exchange finishes in a small handoff page that validates the state parameter, swaps the code for tokens and passes them to the client to persist. Every Spotify call flows through one wrapper that retries once on a 401 by refreshing, marks the session disconnected when the refresh fails, and chunks writes to the limits the API enforces — 100 track URIs per add or remove, 50 ids per batch lookup. Cover uploads are re-encoded in the browser to a square JPEG with quality stepped down until the payload fits under Spotify's 256KB cap.

The keyed services sit behind thin Next.js route handlers so no API key reaches the client bundle: six Gemini endpoints (tracklist, liner notes, cover keywords, track facts, artist origins, natural-language playlist edits), plus Unsplash cover search, Brave Search artist news and Bandsintown tour dates. The Gemini routes validate the request with Zod, pin a response schema so the model returns structured JSON, and answer 502 rather than passing malformed output downstream. Every external service also has a `client.ts` / `mock.ts` / `index.ts` trio behind one environment flag, which means the whole app — import, generation, playlists, stats — runs against canned fixtures with zero credentials, and that is the documented way to develop it.

Problem

Turning a YouTube mix tracklist or a vague mood into a Spotify playlist means searching and pasting one track at a time.

Solution

A browser-only app that parses pasted tracklists or has Gemini draft one, bulk-matches every row against Spotify search, and ships the playlist with cover art — authenticating over PKCE with no backend.

Architecture & flows

How it fits together

Press play to walk through a flow — each pseudo-code line lights up the parts of the system it touches.

POST /api/*
Client

Tinnymix app

Next.js 16 · React 19

Database

IndexedDB

Dexie · tokens, tapes

Service

Route handlers

/api/* · Node runtime

Service

Parse & bulk match

tracklist → URIs

External

Spotify Web API

PKCE OAuth

AI

Google Gemini

structured JSON

External

Unsplash

cover search

External

Brave · Bandsintown

news · tour dates

  • Client
  • Database
  • Service
  • External
  • AI

A mood becomes a real playlist

Free text or a preset mood goes to Gemini, comes back as structured JSON, and lands on the listener's Spotify account with cover art.

Zod caps the prompt at 2000 characters and the track count between 4 and 30.

1 / 7

Decisions

Engineering decisions

Browser-side PKCE instead of a session backend

Context
Connecting to Spotify normally means a server that holds a client secret, mints sessions and stores refresh tokens per user — infrastructure a single-user companion app does not otherwise need.
Decision
Run the authorization-code-with-PKCE flow entirely in the browser with no client secret and no auth library, and persist the access and refresh tokens in IndexedDB; the OAuth callback finishes in a short handoff page because the server route cannot reach the app's Dexie context.
Consequence
There is no user database, no session store and nothing to breach server-side, at the cost of a per-device connection, a state-validating handoff step, and a refresh path that has to live inside the API wrapper.

Local-first persistence over a hosted database

Context
Tapes, playlist snapshots, saved cards, custom moods, palettes and play history all need to persist, but none of them belong to anyone but the listener.
Decision
Keep everything in IndexedDB behind Dexie — nine tables across six migrated schema versions — with JSON export/import as the escape hatch, and no server-side store at all.
Consequence
The app is genuinely free to run and leaks nothing, but state does not follow a user between browsers, so export/import and a deliberate empty state carry the burden that sync normally would.

Keyed services behind server route handlers

Context
Gemini, Unsplash and Brave Search all require secrets, and a client-only architecture would otherwise force those keys into the browser bundle.
Decision
Put each keyed service behind a thin Node route handler that validates input with Zod, pins a response schema where the provider supports one, and normalises upstream failures into a status code and message.
Consequence
No key ever reaches the client and provider responses are validated once at the boundary; the trade is that these routes are the only server-rendered surface in an otherwise fully client-side app.

One matching pipeline for both entry points

Context
Import produces artist/title pairs from pasted text and generation produces them from a language model, but both then need the same fuzzy search, candidate review and playlist write.
Decision
Convert AI-generated tracks into the identical parsed-entry shape the tracklist parser emits, and run both through one concurrency-capped matcher that yields updates per row and falls back to a looser query when the precise one comes back empty.
Consequence
Review UI, error states and playlist creation are written once and tested once; the constraint is that anything new feeding the pipeline has to produce that entry shape.

Adapter trios with a mock-mode switch

Context
A contributor cannot get far on a Spotify app without registering a developer application and collecting four different API keys first.
Decision
Give every external service a `client.ts` / `mock.ts` / `index.ts` trio chosen by one public environment flag, ship JSON fixtures for Spotify, Gemini and Unsplash, and make mock mode the default in the example environment file.
Consequence
The whole product is explorable and testable with zero credentials and the unit suites run offline, provided the fixtures are kept in step with the real response shapes.

Integrations

Notable integrations

Spotify Web API

The account side of everything — profile, playlists, search, saved tracks, top artists and tracks, recently played, now playing, followed artists and cover-art upload.

Authorization code with PKCE straight from the browser across 14 scopes, with one wrapper that attaches the bearer token, refreshes once on a 401, and chunks writes to the API limits (100 URIs per playlist mutation, 50 ids per batch lookup).

Google Gemini

Drafts mood tracklists with a title and description, writes liner notes for the cassette composer, suggests Unsplash cover keywords, produces song facts for the now-playing card, infers artist home cities for the atlas, and applies natural-language playlist edits.

Six Node route handlers under `/api/gemini/*` using the Google Generative AI SDK, each validating the request with Zod and pinning a JSON response schema so structured output can be parsed rather than scraped.

Unsplash

Supplies playlist cover art for both the import and generation flows.

A server proxy route holds the access key, returns four candidates per search, and passes photographer name and profile link through so attribution survives to the picker.

Brave Search

Pulls recent news for whichever artist is currently playing.

A `/api/brave/news` route handler queries the Brave news index server-side; the feed renders inside the now-playing card next to the AI song facts.

Bandsintown

Shows upcoming tour dates for the current artist and backs the concerts view.

A `/api/bandsintown/events` route handler proxies the public events endpoint using a configurable app id that defaults to the project name, so no key is required to run it.

Dexie (IndexedDB)

The only persistence layer — Spotify tokens and connection state, mixtapes, playlist snapshots, saved cards, custom moods, palettes, tags and play history.

A typed Dexie subclass declares nine tables across six versioned schema migrations, with `dexie-react-hooks` driving live queries in the UI and a test-only reset used by the suites against fake-indexeddb.

Stack

  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind CSS
  • Spotify Web API
  • Gemini
  • Dexie / IndexedDB
  • Vitest