metube/YTDLP_PRIVATE_BUILD.md

3.9 KiB

Private yt-dlp build

MeTube uses yt-dlp as its core downloader, pinned to the official PyPI release via pyproject.toml / uv.lock.

There is currently an extractor bug in the official release that is not yet fixed upstream. Until it is, the Docker image bakes a wheel built from a local, patched yt-dlp checkout (~/Dev/github/yt-dlp) instead of the pinned release.

This document explains how the override works, how to build/update the private wheel, and — most importantly — how to switch back to the official build once the upstream fix lands.


How the override works

  1. ./build-ytdlp.sh builds a wheel from a local yt-dlp checkout and stages it as vendor/yt_dlp-*.whl (the versioned filename is required by uv).

  2. The Dockerfile, right after uv sync, force-reinstalls that wheel over the version pinned in uv.lock:

    COPY vendor ./vendor
    RUN if [ -n "$(ls /app/vendor/yt_dlp-*.whl 2>/dev/null || true)" ]; then \
            uv pip install --python /usr/local/bin/python --force-reinstall --no-deps /app/vendor/yt_dlp-*.whl; \
        else \
            echo "No private yt-dlp wheel in vendor/ - using pinned yt-dlp from uv.lock"; \
        fi
    
  3. publish.sh runs ./build-ytdlp.sh before every build, so published images always carry the patched build (and the publish aborts loudly if the local checkout is missing).

  4. The wheel is git-ignored (/vendor/yt_dlp-*.whl). vendor/.gitkeep and vendor/.gitignore are committed so the directory always exists for COPY vendor ./vendor.

Building / updating the private wheel

./build-ytdlp.sh                          # builds from ~/Dev/github/yt-dlp
YTDLP_REPO=/path/to/yt-dlp ./build-ytdlp.sh   # custom checkout path

Rebuild whenever the local checkout changes, then rebuild the image:

docker compose up -d --build --force-recreate

The script fails with a clear error if the configured checkout is missing.


Switching back to the official build (when upstream fixes it)

1. Remove the Dockerfile override

Delete the COPY vendor / RUN uv pip install block from Dockerfile (added in the "Private yt-dlp build" section, right after uv sync).

2. Remove the build step from publish.sh

Delete the block:

# Bake the private yt-dlp build (see build-ytdlp.sh) into the image until the
# upstream fix lands. Fails loudly if the local checkout is missing.
./build-ytdlp.sh

3. Update the pinned version

Bump yt-dlp to the fixed release and re-resolve the lockfile:

uv add "yt-dlp[default,curl-cffi]@latest"  # or pin the exact fixed version
uv lock

Commit the updated pyproject.toml and uv.lock.

4. Clean up the override artifacts

rm build-ytdlp.sh            # no longer needed
rm -f vendor/yt_dlp-*.whl    # delete any staged wheel

You may also remove the vendor/ directory (.gitkeep, .gitignore) and the /vendor/yt_dlp-*.whl entry from .gitignore.

5. Update the docs

Remove the "Private yt-dlp build" section from DEPLOY.md (and delete this file if you no longer want the guide around).

6. Rebuild and verify

docker compose up -d --build --force-recreate

Confirm the installed version is the official release:

docker compose exec metube python3 -c "import yt_dlp; print(yt_dlp.version.__version__)"

7. Publish

When you next publish, ./publish.sh will no longer stage a private wheel and the image will use the official yt-dlp release.


Verifying which build is running

The /version endpoint reports METUBE_VERSION; the yt-dlp version is logged by the app at startup. To check inside a running container:

docker compose exec metube python3 -c \
  "import yt_dlp, yt_dlp.extractor.spankbang as s; import inspect; print(yt_dlp.version.__version__, 'patched:', 'spankbang' in inspect.getsource(s).lower())"