Back to Learnings

Learnings / Building Vynl

Your music.
Your rules.
Your AI DJ.

Why I built Vynl around my own music collection, with Beets for organisation and a listening interface I wanted to use.

The reason

I wanted to enjoy the music I already owned.

After years of digitising CDs and vinyl, I wanted a library I could browse and play wherever I was. Beets helped me organise the files, but I still needed the listening experience around them.

Vynl grew from that need. The collection stays on my own server, with a browser interface for choosing what to play.

01 / The story

Why I built Vynl

The Vynl library puts the music collection into a browsable listening interface. Select the image to enlarge it.

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.

↕ Filesystem and Beets adapters

Vynl application server

Next.js API routes for library browsing, playlists, cleanup and playback. SQLite stores application data through Drizzle.

↕ Browsing and playback controls · audio delivery

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.

Optional AI calls from the application

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.

Based on the project README. Remote browser access and local speaker access have different network requirements.

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' fetchart command. 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?

Explore the Vynl product page.

Own your music again.

Put this guide into practice with your own collection.

Run your own library

Set up Vynl.

Use the repository instructions for the version you install.