Skip to content

yt dlp plugins

Alex Shnitman edited this page Sep 24, 2026 · 1 revision

yt-dlp can be extended with plugins: extra extractors (support for a site, or a different way of handling one) and postprocessors. MeTube runs yt-dlp inside its own process, so a plugin that yt-dlp can find is used by every download, playlist scan and subscription check without any change to MeTube.

Loading a plugin into the container

yt-dlp only looks for plugins inside a package named yt_dlp_plugins, so the folder layout matters. A plugin file placed anywhere else is silently ignored. Create a folder like this next to your compose file:

plugins/
└── yt_dlp_plugins/
    ├── extractor/
    │   └── my_extractor.py
    └── postprocessor/
        └── my_postprocessor.py

Only the subfolders you need have to exist, and no __init__.py files are required. Then mount the folder and add it to Python's module path:

services:
  metube:
    image: ghcr.io/alexta69/metube
    environment:
      - PYTHONPATH=/plugins
    volumes:
      - ./downloads:/downloads
      - ./plugins:/plugins:ro

Restart the container after adding or changing a plugin.

Example: links from a YouTube front-end (Invidious, Piped)

Contributed by @marcelmanz in #1084, based on @coletdjnz's example in yt-dlp/yt-dlp#7403.

yt-dlp recognises links from a fixed list of public Invidious instances as YouTube. A link copied from your own instance, like https://yt.example.com/watch?v=..., is treated as an ordinary web page, and fails. If the instance is behind a login, yt-dlp ends up on the login page instead of the video.

Front-ends like these use the same link paths as YouTube (/watch?v=, /playlist?list=, /channel/...), so this extractor just swaps the host for www.youtube.com before yt-dlp handles the link. Save it as plugins/yt_dlp_plugins/extractor/youtube_frontend.py, and change the pattern to match your instance's hostname:

from yt_dlp.extractor.common import InfoExtractor


class YoutubeInvidiousRedirectIE(InfoExtractor):
    INVIDIOUS_URLS = (r'(?:www\.)?yt\.example\.com',)  # your instance's hostname, as a regex
    _VALID_URL = r'https?://(?P<invidious_base>{})'.format('|'.join(INVIDIOUS_URLS))

    def _real_extract(self, url):
        invidious_base = self._match_valid_url(url).group('invidious_base')
        return self.url_result(url.replace(invidious_base, 'www.youtube.com'))

To list more than one instance, add more patterns to INVIDIOUS_URLS.

With the plugin loaded, a pasted front-end link appears in the queue as the matching www.youtube.com link, and this works for links sent from bookmarklets or other apps, and for subscriptions, too. If the add instead fails with an error starting [generic], yt-dlp did not load the plugin: check the folder layout above.

Front-ends on a private address. Before yt-dlp sees a link, MeTube checks the link's host and refuses hosts that resolve to private, loopback or other internal addresses (see ALLOW_PRIVATE_ADDRESSES in the README). That check runs before the plugin can rewrite the link, so if your front-end is only reachable on your local network or over a VPN such as Tailscale, MeTube refuses its links (Refusing to fetch internal host). Setting ALLOW_PRIVATE_ADDRESSES=true gets past this but turns the protection off entirely. The alternative is to send the YouTube link in the first place: see Sending links from a YouTube front-end for a bookmarklet that does this.

Clone this wiki locally