Keep a music library in sync from the terminal.
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Gaurav Gosain 25252a6c41 tag tracks in go, so nothing but this binary is needed
Tagging ran as a python script over ssh, which meant a tag editor installed
on the music server alongside the music. Now the file is fetched, edited
here with the opus tag writer, and sent back, so the only things involved
are this program and the tools a music library already needs.

The cost is that a file crosses the wire twice to change a few hundred bytes
inside it. That is worth paying for a backfill that happens once, and tracks
downloaded from now on can be tagged before they are ever mirrored, so they
never pay it at all. It is sent back in place rather than as a copy beside
the original, which would want room for a second library on a disk that does
not have it.

Verified against the real library rather than a fixture: the field is
written and read back, the cover art is still there, the length is unchanged,
and the track still decodes without an error.
2026-09-16 15:11:55 +04:00
cmd/vinylpreview fix the races, leaks and swallowed failures a fast build left behind 2026-09-16 13:44:31 +04:00
docs rebuild play mode: panels, album art, mouse, and a readable analyser 2026-09-16 10:19:21 +04:00
internal tag tracks in go, so nothing but this binary is needed 2026-09-16 15:11:55 +04:00
.gitignore crate: keep a music library in sync 2026-09-15 10:56:17 +04:00
go.mod crate: keep a music library in sync 2026-09-15 10:56:17 +04:00
go.sum crate: keep a music library in sync 2026-09-15 10:56:17 +04:00
LICENSE crate: keep a music library in sync 2026-09-15 10:56:17 +04:00
README.md 345 themes, a working analyser, and album art that says why it failed 2026-09-16 11:06:41 +04:00

crate

My music library, in the terminal.


This is personalware. It does what I need and nothing else, and it assumes my setup: a music server I run, a file server I run, and a particular way of filing tracks. It is public because there is no reason for it not to be, not because it is meant to fit anyone else's arrangement. There is no support and no plan to generalise it. Fork it if it is close to something you want.

crate downloads tracks from the sources I list, files them by artist and album, mirrors them to my music server, and plays them. Repeated syncs are cheap: anything already fetched is skipped without touching the network.

It is written directly against the terminal, with no TUI framework, in the same style as youterm.

Install

go install github.com/Gaurav-Gosain/crate/cmd/crate@latest

Needs yt-dlp, ffmpeg and rsync on PATH, plus mpv for play mode.

Use

crate            open the interface
crate sync       one sync, no interface, for cron
crate playlists  rebuild one playlist per source
crate config     print the config file path

Press : for the command palette, which lists everything the interface can do.

key action
: command palette
/ search YouTube Music
s sync every source
enter sync the selected source
a add a source by URL
d remove the selected source, and its tracks
p play mode
t choose a theme
r ask the music server to reindex
q quit

In play mode: space pauses, enter plays the selected track, n and b step, h and l seek. Arrow keys work everywhere. So does the mouse: the wheel scrolls, a click plays, and the progress bar can be dragged.

How it is arranged

The remote is the only copy. Downloads are staged in a cache directory and cleared once the mirror has succeeded, so there is one source of truth and nothing local to drift out of step with it. Set keep_local if you want an offline copy as well, and accept that the two can then disagree.

Sources and albums are different groupings, and tags can only express one. A mix or an artist's channel spans many albums, so a music server that files by album tag scatters one source across dozens of entries. crate writes an .m3u per source alongside the music, which adds the missing grouping without touching the tags, so both views stay right.

Play mode streams from the file server when there is no local copy, draws the cover with the kitty graphics protocol where the terminal supports it, and spins a record where it does not.

Things that were not obvious

Collected because each one cost an evening.

--playlist-items applies to nested playlists too. Parallelising by giving worker k the stride k::n looks right and is not: on an artist's releases tab it takes every nth album and then every nth track inside each of those. About a quarter of a catalogue arrives, and since the arithmetic is identical every run, syncing again picks the same quarter. Sources whose entries are themselves playlists are split by album instead.

yt-dlp's metadata pass will not write tags to an opus file that already has cover art. It runs ffmpeg -map 0 -c copy, which tries to copy the cover as a video stream, and the ogg muxer refuses it. It only bites on a second pass, and it sustains itself: the failure keeps the download out of the archive, so the next run retries the same file forever. --postprocessor-args Metadata:-vn fixes it. The symptom is .temp.opus files piling up.

Subsonic answers every request with HTTP 200, including the ones it refused. Checking the status code alone means a scan that never ran reports itself as a success.

Deleting a source has to leave a tombstone. Shared state merged without one turns every deletion into a temporary one: whichever device still lists the source puts it back, and the music comes with it.

Terminal cells are not runes. These filenames are full of characters like |, the fullwidth bar yt-dlp substitutes for a pipe, and each takes two columns. Measured as one, a line trimmed to fit a panel still runs over the border.

Time a transfer by the bytes that landed, not by the clock. rsync.net's restricted shell rejects shell redirection and oversized sftp blocks, and both fail in milliseconds, which is indistinguishable from a very fast transfer if the test only measures elapsed time.