Clone
2
Building
Jaroslav Beneš edited this page 2026-08-09 01:33:07 +02:00

Building & running SageTube

Requirements

  • Python >= 3.10
  • libmpv (system library; python-mpv binds to it at runtime)
    • Arch: sudo pacman -S mpv
    • Debian/Ubuntu: sudo apt install libmpv2
    • Fedora: sudo dnf install mpv-libs
    • macOS: brew install mpv
    • Windows: place libmpv-2.dll next to the executable (builds from https://github.com/shinchiro/mpv-winbuild-cmake/releases), or install mpv and add it to PATH
  • ffmpeg (for download merging; the app can auto-install it)

Without libmpv the app still runs — the Watch tab shows an install hint and feed/search/browse/download work normally.

QtSvg is used for the interface icons. It ships with PySide6-Essentials, so there is nothing extra to install.

Run from source

python -m venv .venv && source .venv/bin/activate
pip install -e .
sagetube

On first start the app downloads its own SHA256-verified yt-dlp binary and the Deno runtime (yt-dlp's JS challenge solver) into the SageTube data dir:

  • Linux: ~/.local/share/SageTube/bin/
  • macOS: ~/Library/Application Support/SageTube/bin/
  • Windows: %LOCALAPPDATA%\SageTube\bin\

The settings referenced in Using it live in sagetube_config.json beside those binaries, in data/ rather than bin/. The subscriptions, feed cache, watch history and play queue are in sagetube_library.db in the same place.

Installing from source is a copy, not a link. pip install . snapshots the package into site-packages, so editing the checkout changes nothing until you reinstall. Use pip install -e . while developing, or reinstall each time.

Notes on YouTube playback reliability

Playback resolves streams through mpv's ytdl_hook using the managed yt-dlp. YouTube's CDN intermittently serves stalled streams to non-browser clients; the player retries automatically (twice, after 25 s of no playback) and shows a buffering indicator meanwhile. Keeping yt-dlp updated (built-in updater, stable or nightly channel) is the long-term fix channel for extractor breakage.

Notes on the embedded player

The player renders libmpv into a QOpenGLWidget through mpv's render API. The wid=winId() embedding path is not used: it does not work on native Wayland.

Two consequences worth knowing before changing anything in gui/ytsage_gui_player.py:

  • The mpv handle outlives the OpenGL context, and the render context must not. Qt destroys and recreates a widget's QOpenGLContext whenever the widget moves to another top-level window, and libmpv allows one render context per handle. initializeGL() therefore tears down before it creates, teardown clears the update callback and frees with the GL context current, and it is wired to QOpenGLContext.aboutToBeDestroyed. Getting this wrong segfaults rather than failing visibly.
  • Nothing may reparent the video widget, screenshot it, or put a QGraphicsEffect on an ancestor of it. Fullscreen fullscreens the main window for this reason; the tab cross-fade and the dialog blur both skip their grab() when an OpenGL surface is present.

Testing playback against YouTube is unreliable — it throttles non-browser clients unpredictably. Generate a clip instead:

ffmpeg -f lavfi -i testsrc=size=640x360:rate=25 -f lavfi -i sine=frequency=440 \
       -t 30 -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest testclip.mp4

QT_QPA_PLATFORM=offscreen works for GUI tests generally, but offscreen has no real GL, so the render context never succeeds there — use a real display for anything touching the player.

Release workflows

The .github/workflows/ files are inherited from upstream YTSage and target GitHub runners; they do not run on Gitea. If you set up Gitea Actions, note that the upstream Linux/Windows workflows download appimagetool and a bundled ffmpeg without checksum verification — add hash checks before reusing them for release artifacts.