music-librarian

music-librarian

Docker app from MusicLibrarian's Repository

Overview

Self-hosted, taste-driven, resumable FLAC-preferred auto-fill for a Navidrome + Soulbeet music library. Music Librarian seeds from what your household actually plays — Navidrome and Plex/Plexamp play counts (Plex via Tautulli), plus YouTube Music Takeout history — expands that taste via Last.fm, and downloads through your existing Soulbeet API using a FLAC-preferred, high-quality-lossy fallback ladder. It keeps favorited artists' full discographies current via MusicBrainz (new releases arrive on their own) and can re-acquire lossy albums as FLAC on a schedule, verifying every staged FLAC before it replaces anything. All progress lives in SQLite, so a restart resumes exactly where it left off. Every knob is editable live in the web UI (no restart). COMPANION APP: this is not a standalone downloader. It drives an existing Soulbeet instance and reads an existing Navidrome library — you need both already running. Tautulli (Plex plays) and a Last.fm API key are optional/required as noted below. SECURITY: the web UI is LAN-only and unauthenticated by design (same posture as slskd/Soulbeet/Navidrome). It can change download targets and, with upgrades enabled, delete files — do NOT expose port 8730 through a reverse proxy or tunnel. POSTs are CSRF/Origin-guarded, but that is hardening, not auth. FIRST RUN: /config starts empty and is auto-seeded with config.env, secrets.env, exclude.txt and favorites.txt. Edit /config/secrets.env with your LASTFM_API_KEY, SOULBEET_USER and SOULBEET_PASS (Navidrome/Soulbeet login). The container waits — it does not crash — until those are filled; no restart is needed once you save.

Music Librarian

Self-hosted, taste-driven, resumable FLAC-preferred auto-fill for your Navidrome library. Runs as a Docker container on the Unraid box — no Claude Code / browser dependency. Seeds from what your household actually plays (Navidrome + Plex/Plexamp play counts, YouTube Music Takeout history), expands via Last.fm (taste-weighted + horizon), downloads through the existing Soulbeet API with a FLAC-preferred / high-quality-lossy fallback ladder, keeps favorited artists' full discographies current via MusicBrainz, and can re-acquire lossy albums as FLAC on a schedule. State is in SQLite, so restarts resume exactly where they left off.

Companion app, not a standalone downloader. It drives an existing Soulbeet instance and reads an existing Navidrome library — both must already be running. A free Last.fm API key is required; Tautulli (for Plex/Plexamp plays) is optional.

Install on Unraid (Community Applications)

Search Music Librarian in the Apps tab and install. The template pulls a prebuilt image from ghcr.io/asherflynt/music-librarian — no building on the box.

On first start the container auto-seeds an empty /config with config.env, secrets.env, exclude.txt and favorites.txt. Then:

  1. Edit /mnt/user/appdata/music-librarian/secrets.env and fill in LASTFM_API_KEY, SOULBEET_USER, SOULBEET_PASS (your Navidrome/Soulbeet login). Optionally add TAUTULLI_API_KEY. The container waits for these — it never crash-loops — and picks them up within a minute of saving, no restart needed.
  2. Set SOULBEET_URL and NAVIDROME_URL in the template to your host's LAN IP and their ports (on bridge networking, not localhost).
  3. Point the Music library / Free-space pool / Staging pool mounts at the right shares for your box.

Open the UI at http://<server>:8730. Every knob has hover help and is editable live.

Keep it on the LAN. The UI is unauthenticated by design and can change download targets and (with upgrades on) delete files. Do not put port 8730 behind a reverse proxy or tunnel. See the security note under Web UI below.

Web UI

http://<server>:8730 — progress, real library quality breakdown, download stats, favorites, new releases, the taste ranking, and every tunable setting.

LAN only, no auth, by design — same posture as slskd/Soulbeet/Navidrome on this network. Do not route it through the Cloudflared tunnel: it can change download targets and, with upgrades enabled, cause files to be deleted.

State-changing requests (all POSTs) are CSRF-guarded: a request carrying a cross-origin Origin header is rejected, so a random web page you happen to open can't silently drive the API (this also blunts DNS-rebinding). Requests with no Origin — curl, scripts — are still allowed, so automation is unaffected. This is hardening, not a substitute for auth: anyone who can reach port 8730 can still use the UI directly, which is why it must stay on a trusted LAN.

Layout on the server

/mnt/user/appdata/librarian/
  secrets.env      # LASTFM_API_KEY, SOULBEET_USER/PASS, TAUTULLI_API_KEY (you fill in)
  config.env       # live-tunable settings               (edit in the UI or here)
  exclude.txt      # kids-music artist blocklist         (edit in the UI or here)
  favorites.txt    # full-discography artists            (edit in the UI or here)
  takeout/         # drop Google Takeout YT Music history JSON here
  state/state.db   # SQLite progress                     (auto)
  state/status.json# live status snapshot                (auto)
  upgrade-swap.log # every verified swap + deletion      (auto)
/mnt/user/Media/Music/_upgrades/   # FLAC upgrade staging, pre-verification (auto)

Build & run from source (advanced)

Not needed for a Community Applications install — that pulls the prebuilt image. This is for running from a local clone (the update.sh workflow).

docker build -t librarian /mnt/user/appdata/librarian/build
docker run -d --name librarian --restart unless-stopped \
  -e SOULBEET_URL=http://<unraid-ip>:9765 \
  -e NAVIDROME_URL=http://<unraid-ip>:4533 \
  -e MUSIC_PATH=/music -e FREE_SPACE_PATH=/music -e TARGET_FOLDER=/music \
  -v /mnt/user/appdata/librarian:/config \
  -v /mnt/user/appdata/librarian/state:/data \
  -v /mnt/user/Media/Music:/music:ro \
  librarian

MUSIC_PATH is mounted read-only just for size/track measurement; downloads land in the library via Soulbeet/beets (which has its own writable mount).

Adjusting

Every setting is editable in the UI — hover any field for a plain-English explanation of what it does and what happens if you change it — or by editing config.env directly. The UI writes that same file and preserves its comments. Changes take effect on the next loop; no restart, no rebuild.

The common ones:

  • Want more music? Raise TARGET_TB. It resumes immediately from where it stopped.
  • Getting music you don't recognise? Lower EXPLORE_RATIO.
  • Stop temporarily: the Pause button (or PAUSED=1). Nothing is lost.
  • Wrong taste: TASTE_HALF_LIFE_DAYS controls how much old listening still counts.
  • Kids' music: add artists in the UI's Excluded panel, or to exclude.txt.
  • Want everything by an artist: star them in Navidrome, click ★ in the UI, or add them to favorites.txt.

Settings reference

Every knob, what it does, and its default. This section is generated from the same text the UI shows on hover (HELP in web.py) via tools/gen_settings_doc.py — so the docs and the UI cannot drift apart. Edit HELP, then run:

python3 tools/gen_settings_doc.py           # rewrite this section
python3 tools/gen_settings_doc.py --check   # fail if out of date

Targets & pacing

TARGET_TB

Stop once the library reaches this size · default 3 · 0.0 – 500.0

The size you want your music library to grow to, in terabytes. The librarian keeps downloading until the library hits this OR the track count target, whichever comes first, then idles.

Raise it and it picks straight back up where it left off — nothing is lost or re-downloaded. Lower it below your current size and it stops cleanly; it never deletes anything to get back under the number.

This is the one to change when you clear space and want more music.

TARGET_TRACKS

…or once it reaches this many tracks · default 100000 · 0 – 100000000

The other finish line: stop once the library holds this many tracks. Whichever target is hit first wins.

In practice size is almost always the binding limit — 100,000 FLAC tracks would be far more than 3 TB — so treat this as a safety ceiling rather than the real goal.

EXPLORE_RATIO

0 = only what you love · 1 = only new horizons · default 0.30 · 0.0 – 1.0

How adventurous the recommendations are.

At 0 the librarian only fetches more of what you already play and artists that sound like them. At 1 it only chases artists you don't own from genres you lean into. The default 0.3 spends about 70% of its effort deepening your existing taste and 30% stretching it.

Turn it down if you're getting music you don't recognise; turn it up if the library feels like an echo chamber.

MIN_FREE_GB

Hard floor — never fill the pool below this · default 500 · 0.0 – 100000.0

A safety net for your storage pool. If free space on the cluster pool drops below this many gigabytes, the librarian stops adding music entirely, no matter what the targets say.

This exists so a runaway fill can't fill the pool that also holds your movies, TV, and everything else. Don't set it near zero — a full ZFS pool performs badly and is unpleasant to dig out of.

STAGING_MIN_FREE_GB

Pause if the NVMe staging pool gets this low · default 80 · 0.0 – 100000.0

Downloads land on the fast NVMe cache first, then beets imports them into the library. If that cache drops below this many gigabytes free, the librarian pauses new downloads for a couple of minutes to let imports drain it.

It protects the cache SSD from filling up mid-transfer, which would stall every download at once.

CONCURRENCY

How many albums to download at once · default 3 · 1 – 16

How many albums the librarian works on simultaneously.

Higher isn't better: Soulseek peers are ordinary people's computers, and hammering several at once gets you queued, throttled, or banned. 3–4 is the sweet spot. Raise it only if downloads are clearly idling.

TASTE_REFRESH_MIN

How often to re-read your play counts (minutes) · default 360 · 5 – 100000

How often the librarian re-reads what you've been playing from Navidrome, Plex/Plexamp, and your YouTube Music history, and re-ranks artists.

The default of 360 (6 hours) is plenty — your taste doesn't change by the minute, and each refresh costs a round of API calls. Use the 'Refresh taste' button if you want it to happen right now.

MEASURE_EVERY_SEC

How often to re-scan the library (seconds) · default 300 · 30 – 86400

How often the librarian walks the library to recount size, tracks, and audio quality.

The scan is incremental — files that haven't changed are not re-read — so this is cheap. Lower it if you want the progress numbers to feel more live; raise it if the disk is busy.

SCAN_MAX_PROBES_PER_PASS

How many new files to analyse per scan (safety limit) · default 250 · 10 – 100000

How many new or changed files the librarian opens per scan pass to read their real audio format.

This is a safety limit rather than a speed dial. Your library is reached through Unraid's /mnt/user layer, which is a single shared chokepoint that Plex, slskd, beets and everything else also go through. Counting files there is cheap; opening thousands of them in a tight loop is not — that is what locked the server up hard enough to need a power cycle on 17 Jul 2026.

Files that don't fit in a pass simply wait for the next one, so the scan still finishes — it just spreads the load. At the default it works through roughly 3,000 files an hour. Only raise it if you know the server is idle.

PAUSED

Stop queueing downloads, keep everything else running · default off · on / off

Pauses new downloads without stopping the container or losing any progress.

In-flight transfers finish, imports keep happening, and the UI stays live. Flip it back off and it resumes exactly where it was. Same as the Pause button up top.

Taste weighting

TASTE_HALF_LIFE_DAYS

A play this old counts half as much · default 365 · 1.0 – 36500.0

Makes recent listening matter more than old listening.

At the default 365, a song you played a year ago counts half as much as one you played today; two years ago, a quarter. Without this, a 2019 obsession would weigh exactly as much as last week and the library would grow toward who you used to be.

Lower it (e.g. 180) to chase your current mood harder. Raise it (e.g. 1095) to give long-time favourites more say.

WEIGHT_NAVIDROME

How much Navidrome plays count · default 3.0 · 0.0 – 100.0

How much weight to give plays from Navidrome — this library, in the web player or a Subsonic app.

This is direct evidence of what you like, so it's weighted high by default. Note Navidrome only reports an album's LAST played date, so the recency decay here is coarser than for Plex.

WEIGHT_PLEX

How much Plex / Plexamp plays count · default 3.0 · 0.0 – 100.0

How much weight to give plays from Plex and Plexamp, read via Tautulli.

Plexamp is a Plex client, so its plays are recorded by Plex and never reach Navidrome — without Tautulli they'd be invisible. Requires TAUTULLI_API_KEY in secrets.env; without it this signal is simply skipped.

There's no double-counting: a play is recorded by whichever server actually served the audio, never both.

WEIGHT_YTMUSIC

How much YouTube Music history counts · default 1.0 · 0.0 – 100.0

How much weight to give your YouTube Music listening history from Google Takeout.

Weighted lower than Navidrome/Plex by default because it's noisier — it includes a lot of incidental, background, and one-off listening rather than music you deliberately chose. It's most valuable as the cold-start signal before you've played much in this library.

Favorites (full discography + new releases)

FAVORITES_ENABLED

Fetch full discographies for favourited artists · default on · on / off

Turns the favourites system on or off.

When on, any artist you favourite gets their ENTIRE discography downloaded, and is re-checked forever so new releases arrive automatically. Favourite an artist by starring them in Navidrome, clicking ★ in the Taste list, or adding them to favorites.txt.

Turning this off leaves existing downloads alone; it just stops new discography syncing.

FAVORITE_SYNC_HOURS

How often to check favourites for new releases · default 24 · 1 – 8760

How often to ask MusicBrainz whether your favourited artists have released anything new.

This is what makes new albums show up on their own. Daily (24) is plenty — artists don't release albums hourly, and MusicBrainz allows only one request per second. Use 'Sync favorites' to check right now.

FAVORITE_PRIORITY

Queue position vs ordinary discovery (higher = sooner) · default 100 · 0 – 10000

How far ahead of the normal discovery queue favourites jump.

This matters more than it sounds: the 100k-track fill generates thousands of candidates, so without a priority boost a favourite's discography would sit behind all of them and effectively never download. Anything above 0 puts favourites first; the exact number only matters if you want tiers.

FAVORITE_INCLUDE_EP

Include EPs in a favourite's discography · default on · on / off

Whether EPs count as part of 'the discography'.

On by default — EPs are usually real, wanted releases. For Glass Animals this is the difference between 4 albums and 7 releases.

FAVORITE_INCLUDE_SINGLES

Include singles — warning, there are a LOT · default off · on / off

Whether to also download every single.

Off for a good reason. Glass Animals alone has 29 singles and 26 remix singles against just 4 studio albums — turning this on takes them from 7 releases to 36, and almost all of it is duplicate versions of tracks you already have from the albums.

Only turn this on for an artist whose singles genuinely aren't on any album.

FAVORITE_INCLUDE_LIVE

Include live albums and broadcasts · default off · on / off

Whether live recordings count as part of the discography.

Off by default. These are often radio sessions and broadcast recordings of songs you already own, and their audio quality is inconsistent.

FAVORITE_INCLUDE_COMPILATIONS

Include compilations and greatest-hits · default off · on / off

Whether compilations, best-ofs, and greatest-hits count.

Off by default, because if you already have the studio albums a compilation is almost entirely duplicate tracks.

FAVORITE_INCLUDE_REMIX

Include remix albums and remix singles · default off · on / off

Whether remix releases count.

Off by default. Remixes are the single biggest source of near-duplicates — for Glass Animals there are 26 remix singles alone.

Lossy → FLAC upgrades

UPGRADE_ENABLED

Re-download lossy albums as FLAC when one appears · default off · on / off

Periodically re-checks albums you own as MP3 or AAC to see whether a FLAC version has shown up on Soulseek since you got them.

Off by default. When a FLAC is found it's downloaded to a staging area, NOT straight into your library. A separate script then verifies it — every file must be FLAC, the track count can't drop, every file must fully decode, and the running time must match within 5% — before it replaces the old copy.

Your old lossy copy is DELETED once the replacement is verified and confirmed in place. If any check fails, nothing is touched.

UPGRADE_HOUR

Hour of day to run the upgrade check (0–23) · default 3 · 0 – 23

What time of day the nightly upgrade pass runs, in 24-hour server local time (3 = 3am).

It runs once a day at this hour. Pick a time when nobody's streaming and the server is otherwise quiet.

UPGRADE_MAX_PER_RUN

Albums checked per nightly pass · default 10 · 1 – 1000

The most albums the upgrade pass will check in one night.

This is the main 'don't overwhelm anything' dial. Each check is a full Soulseek search against real peers, so checking hundreds at once is both slow and a good way to get banned. At 10/night it works through a backlog steadily without anyone noticing.

UPGRADE_RECHECK_DAYS

Don't re-check the same album within this many days · default 30 · 1 – 3650

How long to wait before asking about an album again after finding no FLAC.

If nobody on Soulseek had a FLAC of an album yesterday, they almost certainly won't today either. Waiting 30 days stops the pass from burning its whole nightly budget re-asking the same hopeless albums instead of trying new ones.

UPGRADE_ONLY_WHEN_IDLE

Wait for the main download queue to go quiet first · default on · on / off

Makes upgrades yield to the main fill.

On by default. Upgrades and normal downloads compete for the same Soulseek peers and the same bandwidth; upgrades are a nice-to-have, so they wait rather than slow down music you don't have yet.

Play tracking

Three signals, each recency-weighted by exponential decay (TASTE_HALF_LIFE_DAYS, default 365 — a play a year old counts half as much as one today), then combined by the WEIGHT_* knobs:

Source How Decay granularity
Navidrome Subsonic getAlbumList2?type=frequent per album — Subsonic exposes no per-play timestamps, only playCount + last-played
Plex / Plexamp Tautulli get_history per play (each row has a unix timestamp)
YouTube Music Takeout watch-history.json per listen (each entry has an ISO-8601 time)

Plexamp needs Tautulli. Plexamp is a Plex client, so its plays are recorded by Plex Media Server and never reach Navidrome — polling Navidrome alone misses them entirely. Set TAUTULLI_API_KEY in secrets.env to count them. It's optional: absent, the Plex signal is simply skipped, never an error.

There is no double-counting between Navidrome and Plex: a play is recorded by whichever server actually served the audio, never both.

Favorites — full discography + new releases (MusicBrainz)

A favorited artist gets their entire discography queued (at FAVORITE_PRIORITY, so ahead of the exploration crawl) and is re-checked every FAVORITE_SYNC_HOURS forever, so new releases are picked up automatically.

Why MusicBrainz and not Last.fm. They answer different questions, and each is useless at the other's:

  • Last.fm → discovery. getSimilar / tag.getTopAlbums answer "who else might I like". MusicBrainz has no similarity data at all, so Last.fm is not replaceable here and remains the recommendation engine.
  • MusicBrainz → discography. Canonical release-groups with release dates and release types. Last.fm's getTopAlbums returns neither, so it cannot detect a new release or exclude a live bootleg — it is structurally unable to do this job.

Release-type filtering matters more than it sounds. Glass Animals has 75 release-groups; only 7 are real albums/EPs:

FAVORITE_INCLUDE_EP=1            ->  4 albums + 3 EPs        = 7   (default)
FAVORITE_INCLUDE_SINGLES=1       -> +29 singles              = 36
FAVORITE_INCLUDE_REMIX=1         -> +26 remix singles
FAVORITE_INCLUDE_LIVE=1          -> +5  live broadcasts
FAVORITE_INCLUDE_COMPILATIONS=1  -> +3  compilations

MusicBrainz enforces 1 request/sec, and throttling returns 200 with an empty body rather than an error — so the client rate-limits at 1.1 s and treats a missing expected key as a retryable failure, never as "this artist has no releases" (which would permanently mark a discography complete).

exclude.txt wins over favorites.txt. Both are deliberate acts, but the blocklist is the safety-oriented one. An artist in both is skipped and the conflict is logged.

Scheduled lossy → FLAC upgrades

Off by default (UPGRADE_ENABLED=0). When on, one bounded pass runs daily at UPGRADE_HOUR: it takes up to UPGRADE_MAX_PER_RUN albums that mutagen classified as lossy, searches Soulseek, and stages a FLAC only if a true FLAC exists (never a lossy→lossy sidegrade). UPGRADE_ONLY_WHEN_IDLE=1 keeps it from competing with the main fill for Soulseek slots, and UPGRADE_RECHECK_DAYS stops it re-asking about the same album.

Staging goes to <music>/_upgradesinside Soulbeet's existing /music mount. That's not arbitrary: /api/downloads/queue does create_dir_all(target_folder) inside the Soulbeet container, so a path outside its mounts (a bare /upgrades) would be created in its writable layer — invisible to the host and discarded on recreate, with no error. The staging dir is excluded from the library scan and carries a .ndignore so Navidrome doesn't index half-verified albums.

The swap, and why it deletes

upgrade-swap.sh (User Script, every 30 min) verifies each staged album against four gates, and touches nothing unless all pass:

  1. every audio file is .flac
  2. FLAC track count the existing album's
  3. every file decodes cleanly (ffmpeg -v error -i … -f null -) — catches the truncated/corrupt transfers Soulseek routinely produces
  4. total duration within 5% of the existing album — catches wrong-edition rips

Only then does it beet remove --delete the old album and beet move the FLAC in, and only after confirming the replacement landed does it drop the staged copy. Configured to delete the old lossy copy; set ARCHIVE_DIR in the script to move it aside instead. Every swap and deletion is logged to upgrade-swap.log.

Gate 3 is the one that matters: without a real decode check, a truncated FLAC passes a header check happily, and you'd trade a good MP3 for an unplayable file.

flac -t isn't used because no flac binary exists on the host or in the soulbeet container — and the soulbeet image has no shell at all, so everything is exec'd as a direct binary (ffmpeg, ffprobe, beet) with file enumeration done host-side.

Quality ladder (FLAC preferred, never required)

For each album/track, best available wins:

Tier Format Notes
1 FLAC lossless
2 ALAC (.m4aALAC_MIN_KBPS, default 500) also lossless — ranked above any lossy source, even a higher-scoring one
3 High-quality lossy (mp3/aac/.m4a-as-AAC) at quality_score ≥ 0.55 (~320 kbps / V0) 320 and V0 preferred
skipped low_quality_only (only sub-threshold lossy) or no_source

The .m4a trap: .m4a is a container used by both ALAC (lossless) and AAC (lossy), and the backend only reports the file extension, not the codec — so "prefer m4a" would silently pull lossy AAC most of the time. Bitrate is the real discriminator: ALAC runs ~600–900 kbps, AAC caps ~320. The librarian computes effective bitrate from size/duration and only calls an .m4a lossless if it clears ALAC_MIN_KBPS. Tune via the ALAC_MIN_KBPS env var.

For files already on disk the guess isn't needed: mutagen reports the real codec (alac vs mp4a.40.2), which is what the library quality stats and the upgrade scanner use. Verified against this library — all 16 .m4a files are AAC at 256–320 kbps, correctly classified as lossy rather than mistaken for lossless ALAC.

Every fallback is documented. When no FLAC exists, the log records what it landed on and why:

FALLBACK [Artist - Album]: no FLAC source -> ALAC (.m4a @ ~874 kbps, lossless)
FALLBACK [Artist - Album]: no lossless source -> mp3 @ ~336 kbps (lossy)
SKIPPED  [Artist - Album]: only sub-threshold lossy sources

and status.json carries per-item detail plus a running lossless ratio:

"queued_flac": 412, "queued_alac": 9, "queued_lossy": 63,
"lossless_total": 421, "lossless_pct": 87.0,
"recent_fallbacks": [
  {"artist": "...", "album": "...", "format": "mp3", "kbps": 336, "lossless": false}
]

The SQLite candidates table keeps fmt + kbps for every acquisition, so you can always audit exactly what is lossless vs. fallback.

Status

cat /mnt/user/appdata/librarian/state/status.json or docker logs librarian — shows library TB, track count, % to target, free space, and FLAC-vs-lossy counts.

The rescue sweep (deliberately NOT in this container)

Soulbeet's DownloadMonitor doesn't reliably trigger a beets import for every completed download under sustained concurrent load — fully-downloaded albums can sit in slskd's staging dir with no import ever attempted. A periodic sweep re-runs beet import against that staging dir to catch them.

That sweep intentionally lives outside this container, as an Unraid User Script (librarian-rescue-sweep, every 20 min) that calls docker exec soulbeet beet import ....

Why not run beets in this container? Because it would mean a second beets install writing the same .beets_library.db as Soulbeet's. Pinning versions to match only papers over that — if Soulbeet's image ever bumps beets, two different versions would share one library db, which beets does not support. Calling Soulbeet's own beets via docker exec uses the canonical version and config, with zero drift.

Why not have this container docker exec? That needs /var/run/docker.sock mounted in, granting root-equivalent host control to a container that parses external data (Takeout JSON) and talks to third-party APIs. Not worth it for a convenience feature.

The host-side User Script avoids both problems, and runs on its own schedule independent of this container's auth/paused state.

Maintainer: releasing & Community Applications

The image is published to GHCR by .github/workflows/docker-publish.yml on every push to main and every v* tag (multi-arch amd64/arm64). Cut a release with:

git tag v2.1.0 && git push origin v2.1.0

Then make the package public once: GitHub → your profile → Packagesmusic-librarianPackage settingsChange visibilityPublic. CA can only pull a public image.

Community Applications reads two files from the repo root/templates/:

To submit or update the listing, go to https://ca.unraid.net/submit, point it at this repository, and run Validate then Scan. Re-run after any XML change.

Install music-librarian on Unraid in a few clicks.

Find music-librarian in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.

Open the Apps tab on your Unraid server Search Community Apps for music-librarian Review the template variables and paths Click Install

Requirements

A running Soulbeet instance and a Navidrome library. A free Last.fm API key. Optional: Tautulli for Plex/Plexamp play history.

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/asherflynt/music-librarian:latest
Last Updated2026-07-28
First Seen2026-07-28

Runtime arguments

Web UI
http://[IP]:[PORT:8730]
Network
bridge
Shell
sh
Privileged
false

Template configuration

WebUIPorttcp

Web UI / API port. LAN only — do not expose it through a reverse proxy or tunnel.

Target
8730
Default
8730
Value
8730
ConfigPathrw

Settings, secrets and Google Takeout drop-folder. Auto-seeded on first run; your edits are preserved across updates.

Target
/config
Default
/mnt/user/appdata/music-librarian
Value
/mnt/user/appdata/music-librarian
StatePathrw

SQLite progress database and live status snapshot. Keep this to resume exactly where you left off.

Target
/data
Default
/mnt/user/appdata/music-librarian/state
Value
/mnt/user/appdata/music-librarian/state
Music library (read-only)Pathro

Your existing music library, mounted read-only for size / track-count / quality measurement only. Downloads land via Soulbeet, not through this mount.

Target
/music
Default
/mnt/user/Media/Music
Value
/mnt/user/Media/Music
Free-space pool (read-only)Pathro

The pool that holds the library. Its free space is measured against MIN_FREE_GB — the librarian stops adding music if this pool drops below that floor. Point it at your bulk pool (e.g. /mnt/cluster) or /mnt/user.

Target
/cluster
Default
/mnt/user
Value
/mnt/user
Staging pool (read-only)Pathro

The fast pool Soulbeet downloads land on before beets imports them. Measured against STAGING_MIN_FREE_GB to protect the cache SSD. Point it at the same pool Soulbeet/slskd stage into.

Target
/cache
Default
/mnt/cache
Value
/mnt/cache
SOULBEET_URLVariable

Where Soulbeet's API is reachable from inside this container. On bridge networking use the Unraid host's LAN IP and Soulbeet's host port, NOT localhost or a container name.

Default
http://192.168.1.10:9765
Value
http://192.168.1.10:9765
NAVIDROME_URLVariable

Where your Navidrome server is reachable from inside this container. On bridge networking use the host's LAN IP, not localhost.

Default
http://192.168.1.10:4533
Value
http://192.168.1.10:4533
TAUTULLI_URLVariable

OPTIONAL. Tautulli base URL, to count Plex/Plexamp plays toward your taste. Leave blank to skip the Plex signal (never an error). The API key itself goes in /config/secrets.env.

TARGET_FOLDERVariable

Advanced. The library path as SOULBEET sees it (passed to Soulbeet's download queue), not as this container sees it. Only change it if Soulbeet mounts your library somewhere other than /music.

Default
/music
Value
/music