Building & running SageTube
Requirements
- Python >= 3.10
- libmpv (system library;
python-mpvbinds 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.dllnext to the executable (builds from https://github.com/shinchiro/mpv-winbuild-cmake/releases), or install mpv and add it to PATH
- Arch:
- 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
QOpenGLContextwhenever 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 toQOpenGLContext.aboutToBeDestroyed. Getting this wrong segfaults rather than failing visibly. - Nothing may reparent the video widget, screenshot it, or put a
QGraphicsEffecton an ancestor of it. Fullscreen fullscreens the main window for this reason; the tab cross-fade and the dialog blur both skip theirgrab()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.