01 / The story
Why I built Vynl
I have more than 500 CDs and around a thousand vinyl records. I spent years digitising that collection. Listening to it should have been the easy part.
It was not. Problems with my NAS repeatedly set back the work I had put into organising the library. Beets was the tool I kept coming back to. It handled music management well, but I wanted a desktop listening interface around my own collection.
Spotify did not solve that for me. Its recommendations and shuffle rarely matched what I wanted to hear, and I no longer wanted to pay for a service when I already owned so much music. I wanted to choose my own albums, make my own playlists and listen wherever I happened to be.
That was the reason I started Vynl. AI helped me build it, but the starting point was a music collection I wanted to enjoy.
02 / The problem
A collection needs a listening interface
Vynl puts a browser interface over music stored on your machine or a mounted NAS. You can browse albums and artists, build playlists and play your collection without uploading the audio to a streaming service.
I use it with my Sonos systems at home. I also listen to my home collection when I am away, including on holiday and in the car. That is the part that matters most to me. The music I collected over the years is available when I want it.
The interface works on desktop and mobile browsers. Remote listening still needs a connection to the server at home.
03 / How it works
Beets remains the core of the music library
Vynl's React interface connects to Next.js API routes for library operations and playback. Its SQLite application database is separate from the Beets catalogue. The browser maintains its playback queue with Zustand; Sonos speakers fetch audio from the server themselves.
Music files
Local disk or mounted NAS. Audio files, embedded tags and artwork.
Beets library
Import, tagging and organisation. Its library database tracks the collection.
Vynl application server
Next.js API routes for library browsing, playlists, cleanup and playback. SQLite stores application data through Drizzle.
Browser player
React interface with playback and queue state managed by Zustand.
Sonos speakers
Discovery and control over the local network. Speakers fetch audio from a reachable server URL; FFmpeg converts formats where needed.
Library cleanup and playlist assistance
Configured local or cloud model providers help assess metadata and generate playlists. They are separate from the audio playback path.
Beets worked well for managing my collection, so I kept it. Vynl builds on its catalogue rather than asking me to start again. The Beets adapter reads the existing library database and carries its track metadata into Vynl's listening interface.
That includes artists, album artists, track and disc numbers, compilation flags, genres, recording identifiers and audio properties. The adapter checks that each file exists. It reads album artwork from Beets and falls back to artwork embedded in the audio file when needed.
It also handles a practical self-hosting problem. A music folder may have one path on the Mac and another inside a container. The BEETS_PATH_REMAP setting translates those paths when Vynl reads the catalogue. The collection does not need to be retagged just because its mount point changed.
For example, the adapter preserves Beets' compilation flag when preparing an album for display. This is an excerpt from the Beets adapter, not a separate script to run:
const albumArtist = row.comp
? "Various Artists"
: (row.albumartist || undefined);
const isCompilation = Boolean(row.comp);Housekeeping for a collection that has grown over years
The Housekeeping tools deal with the problems I encounter in a large library. Some use Beets directly; others repair Vynl's catalogue or work on the music files. They have different effects:
- Refresh metadata rereads tags and audio properties from the files into Vynl, including track numbers, duration, bitrate and sample rate.
- Fetch artwork calls Beets'
fetchartcommand. Rescan covers extracts embedded images and updates Vynl's cover references. - Clean missing entries removes Vynl records whose files cannot be reached. A disconnected NAS can look like missing music, so check the mount before running it.
- Split-album repair brings tracks under a chosen album artist. The merge operation can move files into one directory, update paths in both catalogues and consolidate Beets album records.
- Duplicate-format detection finds MP3/M4A pairs with matching album, track number and title. Cleanup keeps the selected format and deletes the other file. This is metadata matching, not an audio-fingerprint comparison or a judgment about which recording sounds better.
The housekeeping handler shows how directly Beets is still involved. Artwork fetching runs its existing command rather than duplicating that job:
const { stdout } = await execFileAsync("beet", ["fetchart", "-q"], {
timeout: 300000,
});BeetsAI Doctor proposes repairs, then Beets applies them
The Doctor adds model-assisted review for problems such as compilation grouping, split releases and genre metadata. Its plan-only option skips the Beets writes so the proposed changes can be inspected first. In execution mode, some decisions can be applied automatically; this is not a guarantee that every change waits for manual approval.
The apply code records the album state before and after a change. It runs beet modify, then beet write to update the file tags, and mirrors recognised changes into Vynl's tracks table. Writing the tags matters: otherwise a later metadata refresh could read the old values back from the audio files.
The apply pipeline records tag-write failures and how many Vynl rows were updated. Those steps can fail separately. Keep a backup of the audio files and both databases, inspect the plan, and check the job results before treating a repair as complete. Plan-only mode belongs to the Doctor; it does not make the other housekeeping actions non-destructive.
Sonos at home, listening away
Vynl controls Sonos through the @svrooij/sonos library inside the server process. It discovers speakers on the local network using SSDP. If multicast discovery finds nothing, a configured SONOS_SEED_IP gives it a known speaker to connect through. This helps with container or network setups where discovery packets do not reach the speakers.
The interface works with named rooms. Select a room to start or pause playback, skip tracks or adjust its volume. Separate rooms can play different music. Join rooms into a group when you want them to play together, then leave or dissolve the group to control them separately again.
These functions come directly from Vynl's Sonos integration. Each command resolves the selected speaker by name. Volume is limited to the supported 0–100 range:
export async function setVolume(speaker: string, volume: number): Promise<void> {
const device = await getDeviceByName(speaker);
const clamped = Math.max(0, Math.min(100, Math.round(volume)));
await device.SetVolume(clamped);
}
export async function groupJoin(speaker: string, target: string): Promise<void> {
const device = await getDeviceByName(speaker);
await device.JoinGroup(target);
}Grouping does not remove individual volume control. Vynl reads the playing track and transport state from the group coordinator, but reads volume from the selected speaker. Stereo pairs and home-theatre satellites appear as one addressable room rather than pretending that every physical speaker is an independent player.
How a track reaches the selected speaker
The browser sends a speaker name and file selection to Vynl's control route. That route builds an audio URL, which the speaker fetches directly from the server. For a lossless file marked for Sonos transcoding, the route requests an MP3 stream through Vynl's audio endpoint:
const sonosEncoded = isLossless
? encoded.replace(/\.(flac|wav|aiff|alac)$/i, ".mp3")
: encoded;
const sonosParam = isLossless ? "?sonos=1" : "";
const builtUri = `${base}/api/audio${sonosEncoded}${sonosParam}`;
await sonos.playUri(speaker, builtUri, false);This excerpt is from the Sonos control route. The playback helper supplies Sonos metadata before starting playback and avoids double-encoding spaces in file paths. That detail matters: an incorrectly encoded URL can make an existing music file look unavailable.
Set VYNL_HOST, or the supported NEXT_PUBLIC_VYNL_HOST fallback, to an address the speakers can reach. A URL that works through a remote connection on your phone may not work from a speaker on your home network. Vynl controls discovered, reachable Sonos rooms; it does not bypass the network between them.
Listening away from home needs a separately configured, secured route back to the server. The music still lives at home, so the server and internet connection must be available. Self-hosting does not remove those requirements.
Where AI helps
Library cleanup is one concrete use. Instead of accepting every tag suggestion, I can review the changes before they reach the files.
Vynl supports local model connections through Ollama or LM Studio, as well as cloud providers such as Anthropic and OpenRouter. The provider you select matters. A cloud-backed feature sends its model requests outside your machine. Keeping the audio files at home is not the same as keeping every AI request local.
The AI DJ and party workflow are still being developed. They are part of where I want to take Vynl, rather than a finished set of features.
Bringing playlists with you
The Spotify integration imports playlist and liked-song information, then matches it against music you already have. It uses recording identifiers and artist/title matching. Matched tracks become Vynl playlists. Missing tracks go into a wishlist.
This transfers the organisation of a playlist, not Spotify's audio files. You still need your own copy of the music.
04 / Installation
Run your own music library
Under the interface, Vynl uses Next.js and SQLite, with Beets for music management and FFmpeg for audio processing. The project repository has the installation instructions and current feature status.
Before importing a large collection, check that the server can read the music folders, that storage persists across restarts and that your playback devices can reach it. Back up the audio files, the Beets database and Vynl's database. Rebuilding an index is not the same as recovering your collection.
Vynl started as a way to listen to my own music again. That remains the test for what I add next. Does it help me find an album, organise the collection or listen where I want?

