Self-hosted service
guillevc/yubal avatar
guillevc/yubal

yubal: a self-hosted YouTube Music downloader that writes album folders and M3U files

Self-hosted YouTube Music downloader. Tags, organizes, and keeps playlists in sync.

1,556 stars61 forksPythonMIT

At a glance

What is it?
yubal takes a YouTube Music link and produces a tagged library laid out by artist and album, with playlists stored as M3U files that point back at the album copies. It is a Docker-first FastAPI service with a scheduler and a browser extension, and its main constraint is that sync is a one-way pull from YouTube Music.
Who is it for?
Adopt yubal if you already run a media server, want playlists that stay in sync on a cron schedule, and are willing to keep a Docker container and its yt-dlp dependency updated. Do not adopt it if you need two-way library editing, want a tool that manages an existing collection in place, or cannot accept a downloader whose extraction layer breaks whenever YouTube changes.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The gap yubal fills between yt-dlp and a media server

Downloading audio from YouTube Music is a solved problem. Producing a folder tree that Navidrome, Jellyfin or Gonic will index without manual repair is not. yubal's README states the problem in one line: downloading music is easy, organizing it is the hard part. The tool is aimed at people running a self-hosted music server who want to paste a link and get files that appear correctly in that server, rather than a flat directory of video IDs. The layout it produces is artist, then year and album title, then numbered tracks with a cover image and a matching .lrc file per track. Playlists are not duplicated into their own folders. They live under _Playlists as an M3U file plus a cover, and the M3U entries are relative paths back into the album directories. That single decision is what makes the deduplication claim real: a track that appears in ten playlists exists once on disk and is referenced ten times.

How the album-first layout and M3U references actually work

The README shows the resulting tree directly. Under data/, each artist gets a folder, each release gets a subfolder named with year and album title, and files inside follow a two-digit track number plus title convention with .opus and .lrc side by side. The _Playlists directory holds files named like My Favorites [n2g-XhDv].m3u, where the bracketed segment is a playlist identifier. The M3U content is plain extended M3U: an #EXTM3U header, an #EXTINF line with duration and artist-title, then a relative path such as ../Pink Floyd/1973 - The Dark Side of the Moon/02 - Breathe.opus. Because the paths are relative and climb out of the playlist folder, the whole data directory can be moved or bind-mounted at a different path without rewriting the M3U files. The trade-off is that playlist files are not portable on their own. Copy an M3U out of the tree and its references break. Tagging covers ReplayGain, with track gain applied by default and album gain used when a complete album is downloaded, which is the correct distinction for anyone who cares about consistent loudness across a shuffled playlist versus across an album.

Running it: Compose file, ports and the config keys that matter

The documented path is a single service in compose.yaml. The image is ghcr.io/guillevc/yubal:latest, the container name is yubal, port 8000 is published, and two volumes are mounted: ./data at /app/data and ./config at /app/config. The README's example sets PUID and PGID to 1000, YUBAL_SCHEDULER_CRON to "0 0 * * *", YUBAL_DOWNLOAD_UGC to false, and YUBAL_TZ to UTC, with restart: unless-stopped. After docker compose up -d the web UI is at http://localhost:8000. The configuration table lists the keys worth knowing before the first run. YUBAL_AUDIO_FORMAT accepts opus, mp3 or m4a and defaults to opus; YUBAL_AUDIO_QUALITY is a transcode scale where 0 is best and 10 is worst; YUBAL_FETCH_LYRICS pulls from lrclib.net and YUBAL_YTMUSIC_LYRICS_FALLBACK decides whether to fall back to YouTube Music lyrics when lrclib has nothing. YUBAL_REPLAYGAIN is on by default. YUBAL_JOB_TIMEOUT_SECONDS defaults to 1800, which is the ceiling on a single job. YUBAL_BASE_PATH exists for reverse proxy subfolder deployments, and YUBAL_DOWNLOAD_UGC controls whether user-generated content is written to _Unofficial/ rather than the normal artist tree. A CLI is also shipped, documented in packages/yubal/src/yubal/cli/README.md, for downloading and inspecting metadata from a terminal.

Scheduled sync is a one-way pull, and that shapes everything

Subscribing to a playlist means yubal re-checks it on the cron schedule set by YUBAL_SCHEDULER_CRON and pulls new tracks into the library automatically. That is a pull model in one direction only. Nothing in the supplied material describes writing changes back to YouTube Music, so removing a track from your local library will not remove it from the source playlist, and it is not clear from the README whether a track deleted from the source playlist is removed locally on the next sync. Anyone expecting a two-way mirror should treat that as unverified and test it. The second constraint is the extraction layer. Releases are tied to specific yt-dlp versions: v0.10.0 is labelled with yt-dlp nightly 2026.08.18.122307 and v0.9.1 with yt-dlp 2026.6.9. That version pinning is honest about the dependency, but it also means the practical upgrade cadence is set by YouTube's changes rather than by your own schedule. A yubal release that lags behind a site change is a downloader that stops working, not one that degrades gracefully.

Where yubal is the wrong tool

yubal is a downloader with an organizer attached, not a library manager. If you already have a music collection assembled from other sources and want it cleaned, retagged, deduplicated and queried, yubal has nothing to offer: it operates on links you give it and on playlists you subscribe to, and the supplied material describes no import or scan path for existing files. It is also the wrong choice if you want to edit metadata by hand and have those edits survive. The pipeline writes tags as part of the download, so a manual correction is something you would need to reapply after a re-download or a resync. And if your library lives on a mount that refuses chown, the README explicitly flags this case: an NFS or Unraid mount must already be writable by the configured PUID and PGID, because setting those variables will not fix permissions the filesystem will not let the container change. That is a hard blocker, not a warning to skim.

Beets solves the adjacent problem with the opposite approach

Beets is the obvious comparison for anyone who wants organized music on a self-hosted server, and the difference is in where the metadata comes from. Beets is a library manager: you point it at files you already have, it matches them against MusicBrainz, and it moves and retags them according to rules you write in a config file. yubal never inspects an existing collection. It starts from a YouTube Music URL and builds the tree from what that URL returns, which means its metadata quality is bounded by what YouTube Music exposes rather than by a curated database. The practical consequence is that Beets can fix a messy library and can be re-run over the same files indefinitely, while yubal can only produce new ones. They are not substitutes. A reasonable setup runs both: yubal for acquisition from YouTube Music, Beets for anything that arrives from elsewhere or needs a MusicBrainz-grade match. Choosing one over the other comes down to whether your problem is getting music or curating it.

Licence, maintenance and what the release history implies

yubal is MIT licensed, which permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. That is a permissive licence and it does not impose copyleft obligations on your own code. It says nothing about the copyright status of the audio you download, which is a separate question and not one the licence addresses. On maintenance, the release history shows a project that ships often and ties each release to an upstream extractor version: v0.10.0 in August 2026, v0.9.1 in June 2026, v0.9.0 four days earlier. The repository is not archived and the last push is dated 2026-09-07. The cost of running it is therefore not the initial Compose file but the recurring work of updating the image when YouTube changes something the pinned yt-dlp cannot handle. There is also a browser extension, published on addons.mozilla.org for Firefox and distributed for Chrome through GitHub releases, which adds a second component to keep current. Budget for periodic image pulls rather than a set-and-forget install.

Editorial conclusion

Adopt yubal if you already run a media server, want playlists that stay in sync on a cron schedule, and are willing to keep a Docker container and its yt-dlp dependency updated. Do not adopt it if you need two-way library editing, want a tool that manages an existing collection in place, or cannot accept a downloader whose extraction layer breaks whenever YouTube changes. Before committing, verify that PUID and PGID match your host user, confirm your data volume is writable by that UID (the README calls this out for NFS and Unraid mounts that refuse chown), and test one playlist sync with YUBAL_DOWNLOAD_UGC left at false to see where the unofficial tracks land.

Official sources

  1. guillevc/yubal on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Community notes

Community notes