尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證ListenBrainz playlists → downloaded, tagged library → Plex. Rust, with a daemon, CLI and Nix package.
翻譯準備中
正在載入完整 README。
檢視完整 README ↗ListenBrainz builds you a fresh Weekly Jams and Weekly Exploration every week.
tapedeck turns them into music you own. It looks every track up on MusicBrainz, downloads
it with yt-dlp, writes proper tags, files it into a clean Artist/Album/Track tree, keeps
an m3u8 per playlist and pushes the playlist to Plex. Run it once, or let the daemon do it
every few hours.
Vibe coded, built for one person. tapedeck is written with AI assistance for my own setup and comes with no guarantees. You are welcome to try it and to file issues; I will try to fix them.
ListenBrainz ──▶ MusicBrainz ──▶ yt-dlp ──▶ tags ──▶ Artist/Album (Year)/01 - Title.opus
playlists metadata audio lofty │
├──▶ playlists/*.m3u8
└──▶ Plex playlist
I used Explo for this and wanted a few things it
doesn't do: a real library layout instead of a flat folder of files, a database so a song
that shows up in three weekly playlists is downloaded once, a daemon with a CLI that talks
to it, and output that scripts (or an AI agent) can parse. tapedeck is that, in Rust. The
binary is called td.
What you get
{album_artist}/{album} ({year})/{track:02} - {title}.{ext} by default, yours if you prefer.Enter in the TUI plays a track and queues the rest through mpv, locally or
streamed from the NAS, and scrobbles to ListenBrainz so next week's playlists know.td tui on another machine browses and controls the NAS..lrc files next to the music, so Plexamp
shows them, and in the database, so Subsonic apps get them too. On by default, no account.[steam] at the Steam client's steamapps/music, or give it
a steamcmd account and the daemon downloads every soundtrack you own. They come in with
their own tags and artwork, no MusicBrainz needed.td daemon runs on an interval and listens on a Unix
socket; every other command talks to it if it's running and does the work itself if
not.Rust · clap · rusqlite · lofty · reqwest · tokio · yt-dlp · MusicBrainz · ListenBrainz
yt-dlp and ffmpeg on PATH, unless you install through Nix, which bundles them.Prebuilt binaries for Linux and macOS (x86_64 and arm64) are attached to each release. With bin:
bin install github.com/arunoruto/tapedeck
Nix, with x86_64-linux builds served from arunoruto.cachix.org (the flake advertises
the cache; accept it with --accept-flake-config or add it to your substituters):
nix run github:arunoruto/tapedeck -- --help
Cargo:
cargo install --git https://github.com/arunoruto/tapedeck
Container (amd64 and arm64; latest is always the newest release, see
the docs):
docker run -d -v /srv/music:/music -v tapedeck-data:/data -p 8687:8687 \
-e TAPEDECK_LIBRARY=/music -e TAPEDECK_LISTENBRAINZ__USER=you ghcr.io/arunoruto/tapedeck:latest
mkdir -p ~/.config/tapedeck
curl -o ~/.config/tapedeck/config.toml \
https://raw.githubusercontent.com/arunoruto/tapedeck/main/config.example.toml
$EDITOR ~/.config/tapedeck/config.toml # library path, ListenBrainz user, Plex token
td sync --no-push # download and file, skip Plex
td sync # then with Plex
A minimal config:
library = "/srv/music"
[listenbrainz]
user = "you"
playlists = ["weekly-jams", "weekly-exploration"]
[sync]
targets = ["plex"]
[sync.plex]
url = "http://plex.local:32400"
token_file = "/run/secrets/plex-token" # or token = "..."
section = "Music"
Everything else has a default. The full reference is in the docs.
| Command | What it does |
|---|---|
td sync [--playlist KIND] [--no-push] [--no-fetch] | Fetch playlists, download what's missing, tag, file, write m3u8, push; --no-fetch skips ListenBrainz and only downloads what is already wanted |
td status | Track counts by status (wanted / ready / failed) |
td search QUERY | Search the library by artist, album or title |
td playlists | Fetched playlists and where they've been synced |
td artists / td artist NAME --folder F | Album artists as MusicBrainz names them now; pin a folder name where you disagree |
td push PLAYLIST|FILE.m3u8 [--to TARGET] [--title T] | Push a fetched playlist, or any m3u8 file (archive, backup, your own); a same-titled remote copy is backed up and replaced |
td unpush NAME|FILE.m3u8 [--to TARGET] --yes | Remove a playlist from a target, backup first; the local file is never touched |
td retry [ID] | Reset failed downloads so the next sync tries again; with an id, re-download that one track |
td rm ID | Delete one track's file and forget it |
td source ID URL | Pin the download URL for one track and queue it |
td retag ID --recording MBID | Correct a wrong match: make the track that recording, retag, move if the path changes |
td refile [--dry-run] | Re-check every track against MusicBrainz, retag, move files whose path changed |
td import DIR [--apply] [--move] [--delete-duplicates] | Bring an existing folder (say, Explo's downloads) into the library; dry run by default, --apply copies and leaves the source alone, --move moves |
td import PLAYLIST-URL [--apply] [--also N,M] | A YouTube / YouTube Music playlist: each video matched to a MusicBrainz recording and downloaded as album audio, written as a playlist in YouTube's order; dry run by default, unsure entries only with --also; see Playlists |
Output is a table or plain lines; add --json to any command for newline-delimited JSON
events instead, which is what scripts and agents should parse.
Plain td opens it. Left to right: artists, albums, songs; ] switches to the playlists,
local and remote. Every action asks before it touches anything.
Colours come from a theme: Catppuccin Mocha by default, Latte built in for light terminals,
and your own as a TOML file under themes/ next to the config (see
the docs). The recording above is made with
vhs from assets/demo.tape.
After a sync the library looks like this:
/srv/music/
├── Bicep/
│ └── Isles (2021)/
│ └── 01 - Apricots.opus
├── Bonobo/
│ └── Fragments (2022)/
│ └── 06 - Polyghost.opus
└── playlists/
├── weekly-jams.m3u8
├── weekly-exploration.m3u8
└── archive/
└── weekly-jams/
└── 2026-w41.m3u8
On NixOS, import tapedeck.nixosModules.default from the flake and:
services.tapedeck = {
enable = true;
extraGroups = [ "media" ];
settings = { library = "/srv/music"; listenbrainz.user = "you"; /* ... */ };
};
That runs the daemon as its own system user from boot, shares its database and socket with
the tapedeck group so td tui works for members, and takes secrets from the system's
sops-nix. Details and the migration from a user unit are in
the docs.
With Home Manager, import tapedeck.homeModules.default from the flake and:
programs.tapedeck = {
enable = true;
settings = { library = "/srv/music"; listenbrainz.user = "you"; /* ... */ };
};
services.tapedeck.enable = true;
Without Nix, systemd/tapedeck.service is a user unit for a
cargo-installed td. Either way, td status from a shell talks to the running daemon over
its socket.
Working today: the whole pipeline above, yt-dlp as the download backend, Plex as the sync target, a Subsonic server, playback in the TUI, Steam soundtracks and lyrics.
td import <link>).lrc timings.
The heavy lifting stays in external tools (Demucs, WhisperX, stable-ts, …) behind a small
aligner interface in tapedeck, so any of them can be plugged inSync targets and download backends are each one small trait; see the architecture page. Open issues for the rest.
nix develop # rust toolchain, yt-dlp, ffmpeg
cargo test # offline suite
cargo test -- --include-ignored # also the live ListenBrainz / MusicBrainz tests
nix build # what CI and the cache build; runs the offline suite
CI runs cargo fmt --check, clippy -D warnings, the full test suite and the Nix build.
Design notes and the reasoning behind the non-obvious choices are in
https://arunoruto.github.io/tapedeck/architecture.html. Releases are cut by pushing a v* tag matching
Cargo.toml; CI builds the binaries and publishes them.
Distributed under the MIT License. See LICENSE.
td--json everywhere. One JSON object per line, for scripts and agents.token_file, and SQLite is the single source of
truth.td remote ls / td remote rm GLOB [--yes] | List or bulk-delete playlists on a sync target; rm backs up first |
td remote backup [GLOB] / td remote restore FILE | Save target playlists as m3u8 + JSON, or recreate one from such a file |
td request URL|"Artist - Title" [--pick N] [--video] | Request one song: identify, fetch, file, add to this year's Requests playlist |
td external scan / td external dedupe [--dry-run] | Index read-only libraries (Lidarr) and replace own copies of recordings they hold |
td remote relink --from DIR | Point backup entries under DIR at the library's copy of the same recording; run before deleting an imported folder |
td daemon | Run as a service: listen on the socket, sync every sync.interval |
td / td tui | Browse the library (artist › album › song) and playlists; i shows a track's tags, file quality and MusicBrainz/ListenBrainz/source URLs; sync, re-download, pin a source, fix a match, push, delete, each behind a [y/N]. Catppuccin out of the box, your own colours via a TOML theme |