Self-hosted service
jmbannon/ytdl-sub avatar
jmbannon/ytdl-sub

ytdl-sub: YAML Subscriptions That Turn YouTube Into a Plex or Jellyfin Library

Lightweight tool to automate downloading and metadata generation with yt-dlp

2,961 stars109 forksPythonGPL-3.0

At a glance

What is it?
ytdl-sub wraps yt-dlp in a subscription file and a preset system, so downloads land as TV episodes, albums and music videos with the metadata your media server already expects. It is a good fit if you can write YAML and a poor one if you want a graphical interface.
Who is it for?
Adopt ytdl-sub if you run Plex, Jellyfin, Emby or Kodi and are willing to describe your subscriptions in YAML; skip it if you need a GUI, since the project ships a CLI and Docker images only. Before committing, verify that your Python is 3.10 or newer, that the yt-dlp pin in pyproject.toml matches the sites you download from, and that your player's naming expectations match the preset you pick.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 1 day 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 October 3, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The gap between yt-dlp and a media server library

yt-dlp downloads a video and names it however the site names it. Plex, Jellyfin, Emby and Kodi want something else: a show folder, a season folder, an episode file, a thumbnail and an NFO sidecar. Music players want artist and album folders with numbered tracks and embedded tags. Bridging that gap by hand is a script per channel.

ytdl-sub targets people who already run one of those servers and want a channel, a playlist or a SoundCloud profile to appear as a normal library entry. The README frames the goal as downloading media via yt-dlp and preparing it for Kodi, Jellyfin, Plex, Emby and modern music players, with no additional plugins or external scrapers. The output tree in the README shows what that means in practice: a Jake Trains show folder with Season 2021 and Season 2022 subfolders, each episode carrying a thumb.jpg and an .nfo, plus poster.jpg, fanart.jpg and tvshow.nfo at the show level. A music subscription produces artist folders with bracketed release years, numbered tracks and folder.jpg.

That is the whole pitch. The tool is not a downloader with a nicer progress bar; it is a naming and metadata layer that happens to call yt-dlp.

Subscriptions, presets and the ~ override syntax

A subscription file is YAML with a specific vocabulary. The README's example starts with a __preset__ block that sets global overrides: tv_show_directory, music_directory, music_video_directory, only_recent_date_range, only_recent_max_files, and a ytdl_options block that passes arguments straight to yt-dlp's Python API. In the example that block carries a cookiefile path.

Below that come preset names, which are strings like "Plex TV Show by Date", "YouTube Releases", "SoundCloud Discography" and "Bandcamp". Each preset contains genre groups written as = Documentaries or = Kids | = TV-Y, and inside a group you map a show name to a URL. A group value can be a list of URLs instead of one URL, which the README uses to store two Rick Beato channels under a single TV show. Modifiers attach to the group name with a pipe: = News | Only Recent pulls in the Only Recent preset, which the global overrides then bound by date range and file count.

The ~ prefix is the escape hatch. Prefixing a subscription name with ~ lets you set override variables for that entry alone. The README uses it to give one TV show two seasons with different sources: s01_name, s01_url, s02_name, s02_url. This is the mechanism that makes a channel and a playlist merge into one show without renaming files afterward.

Presets are the part worth understanding before you write anything. The tool ships prebuilt presets that do the config-building, and the README says custom configs can modify any part of the process. The documentation site has a walk-through guide for building a config from scratch, so the preset names in the README are a starting vocabulary rather than the full set.

Installing ytdl-sub and running a first subscription

The repository declares requires-python >= 3.10 in pyproject.toml, and the console entry point is ytdl-sub, mapped to ytdl_sub.main:main. The README does not spell out a pip command, so the install route to check first is the project's readthedocs page at ytdl-sub.readthedocs.io, which the README links as the documentation home. There is also a docker/ directory in the repository, and the Makefile builds images named ytdl-sub:local, ytdl-sub-ubuntu:local and a GUI variant, which tells you Docker is a supported delivery path.

Once installed, the smallest useful thing you can do is write a subscription file. The README gives this example, trimmed here to the parts that matter for a first run:

yaml
__preset__:
  overrides:
    tv_show_directory: "/tv_shows"
    music_directory: "/music"
    music_video_directory: "/music_videos"
    only_recent_date_range: "2months"
    only_recent_max_files: 30
  ytdl_options:
    cookiefile: "/config/ytdl-sub-configs/cookie.txt"

The three directory keys are the roots the tool writes into, and the ytdl_options block is passed through to yt-dlp, so cookiefile here is yt-dlp's own option, not a ytdl-sub invention. Then add a preset block with a genre group and a channel URL:

yaml
Plex TV Show by Date:
  = Documentaries:
    "NOVA PBS": "https://www.youtube.com/@novapbs"

Run it with the command the README shows:

bash
ytdl-sub sub subscriptions.yaml

After the run, expect the tree the README documents: a tv_shows folder containing a NOVA PBS show folder with season subfolders, per-episode .mp4, -thumb.jpg and .nfo files, and show-level poster.jpg, fanart.jpg and tvshow.nfo. Point your media server at the root directory, not at the show folder, and let it scan. If the show does not appear, the first thing to check is whether the preset name you used is one the installed version knows, because an unrecognized preset name is a config error rather than a download error.

Where the preset model gets in your way

The preset system is the reason the tool is usable without writing Python, and it is also the reason a nonstandard library layout is painful. If your server expects a naming scheme no shipped preset produces, you are writing a custom config, and the README points at the docs guide rather than explaining the format inline. The README itself does not document rollback, and it does not describe what happens to files already on disk when a subscription's naming changes between runs.

The yt-dlp pin is a second constraint worth naming. pyproject.toml depends on yt-dlp[default]==2026.8.19, an exact pin rather than a range. That is deliberate for reproducibility, but it means the version of yt-dlp you get is the one ytdl-sub was tested against, not the newest one. When a site changes its extraction and yt-dlp ships a fix, you wait for a ytdl-sub release that moves the pin. The recent release list shows the project does publish frequently, with 2026.08.26, 2026.08.26.post1 and 2026.07.17.post1 in the last few months, and the last push to master was on 2026-09-21. Frequent releases are not the same as fast pin bumps, so check the pin in the version you install if you depend on a recently fixed extractor.

Finally, this is a CLI. The Makefile has a target that builds a GUI-tagged Docker image, but the README describes the tool as a command-line tool and the interface shown throughout is a YAML file plus ytdl-sub sub. If a graphical interface is a requirement, this is the wrong project.

ytdl-sub against a plain yt-dlp script

The obvious alternative is yt-dlp plus your own shell script or a cron job with output templates. The difference in approach is where the logic lives. A yt-dlp script puts naming in output template strings and metadata in whatever postprocessor flags you remember to pass; ytdl-sub puts naming and metadata in declarative presets that a group of subscriptions share, and passes ytdl_options through to yt-dlp when you need the underlying flags.

That trade is real in both directions. A script is a few lines and does exactly one thing; ytdl-sub asks you to learn preset names, genre group syntax and the ~ override prefix before your first download lands in the right folder. In exchange, adding a second channel to an existing show is one line of YAML, and the season numbering, NFO generation and thumbnail placement come from the preset rather than from template strings you maintain yourself. If you have exactly one channel and one output format, the script wins on total effort. If you have a dozen channels across YouTube, SoundCloud and Bandcamp feeding three different players, the preset layer is the part doing the work.

Licence, packaging and the upgrade path

ytdl-sub is GPL-3.0, per the LICENSE file at the repository root and the licence badge in the README. For someone running it as a command-line tool against their own library, that is a familiar arrangement. The question to think about is distribution: if you wrap ytdl-sub inside something you ship to others, the GPL's terms attach to that distribution, and the project's own licence file is the text that governs. This is a description of the licence, not legal advice; read LICENSE and, if you are redistributing, get proper advice.

Upgrade cost is low if you stay on released versions. Versions are date-based, formatted YYYY.MM.DD with a .postN suffix when more than one release lands in a day, which the Makefile derives from the commit count since midnight. The practical consequence is that release numbers tell you the date, not the size of the change, so a .post1 bump can carry a yt-dlp pin move or a preset change. Because pyproject.toml pins yt-dlp exactly, upgrading ytdl-sub is also how you upgrade yt-dlp, and the two move together. Pin your own version if you have subscriptions you do not want re-evaluated, and read the release notes before moving across a date boundary.

Editorial conclusion

Adopt ytdl-sub if you run Plex, Jellyfin, Emby or Kodi and are willing to describe your subscriptions in YAML; skip it if you need a GUI, since the project ships a CLI and Docker images only. Before committing, verify that your Python is 3.10 or newer, that the yt-dlp pin in pyproject.toml matches the sites you download from, and that your player's naming expectations match the preset you pick.

Frequently asked questions

What is ytdl-sub?

It is a command-line tool that downloads media via yt-dlp and formats it for Kodi, Jellyfin, Plex, Emby and modern music players. Subscriptions are defined in YAML files that import presets, and the tool writes files, thumbnails and NFO metadata into a library layout.

How do I use ytdl-sub?

Write a subscriptions YAML file containing a __preset__ block with overrides such as tv_show_directory, then add a preset name with a genre group and a URL, and run ytdl-sub sub subscriptions.yaml. The README shows a Plex TV Show by Date preset with a NOVA PBS channel as a minimal example.

Is ytdl-sub a command-line tool only?

The README describes ytdl-sub as a command-line tool and every example uses the ytdl-sub sub command against a YAML file. The Makefile does define a build target for a GUI-tagged Docker image, but the README does not document a graphical interface.

What is a ytdl-sub alternative?

The direct alternative is yt-dlp itself with your own output templates and scripted postprocessing. The difference is that ytdl-sub keeps naming and metadata in reusable presets shared across subscriptions, while a plain yt-dlp script keeps them in template strings and flags you maintain per invocation.

Official sources

  1. jmbannon/ytdl-sub on GitHub
  2. License: GPL-3.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jmbannon-ytdl-sub.svg)](https://hysenlabs.com/projects/jmbannon-ytdl-sub)