There are many video-sharing platforms, but this one is yours.
Self-hosted YouTube video downloader with a web UI, persistent queue, and library management.
Current features:
- FastAPI app with lifespan-managed Alembic migrations and
/health - HTMX server-rendered web UI for lookup, queue, library, and settings
- Persistent SQLite-backed queue and library state
- Worker-thread download execution with cancellation and startup recovery
- Detached-safe queue claiming (Phase 5): the worker pool never holds a
session-bound ORM
Downloadinstance across the claim boundary - yt-dlp JS runtime pinned to Node.js (Phase 5): the shipped Docker
image bundles
nodejsandnpmso YouTube extraction works out of the box - Lightweight runtime diagnostics (Phase 5): the settings page shows a warning panel when the environment is degraded (missing Node.js, workers disabled)
- Paginated YouTube playlist preview with page-local selection, per-video formats, and independent queue jobs
- Docker and Docker Compose packaging for local deployment
cp .env.example .env # adjust paths as needed
uv sync # install dependencies
uv run uvicorn app.main:app --reloadOpen http://127.0.0.1:8000/health.
Build and run with the bundled Docker setup:
docker compose up -d --build
curl -fsS http://localhost:8000/healthBy default, Compose uses named volumes for /data and /downloads.
If you want to store data on your host, bind-mount directories explicitly:
mkdir -p "$HOME/Downloads/YouTube/data" "$HOME/Downloads/YouTube/downloads"
YT_UID="$(id -u)" YT_GID="$(id -g)" YT_HOST_DATA_DIR="$HOME/Downloads/YouTube/data" YT_HOST_DOWNLOADS_DIR="$HOME/Downloads/YouTube/downloads" docker compose up -d --buildThis maps:
YT_DATA_DIR=/datainside the container to$HOME/Downloads/YouTube/dataon the hostYT_DOWNLOADS_DIR=/downloadsinside the container to$HOME/Downloads/YouTube/downloadson the host
Use absolute paths or ${HOME} when setting these variables. Do not put a raw ~/... path directly into docker-compose.yml.
If you switch to host UID/GID overrides for bind mounts, rebuild and recreate the container so the latest image startup command is used:
YT_UID="$(id -u)" YT_GID="$(id -g)" YT_HOST_DATA_DIR="$HOME/Downloads/YouTube/data" YT_HOST_DOWNLOADS_DIR="$HOME/Downloads/YouTube/downloads" docker compose up -d --build --force-recreateAll settings are loaded from environment variables prefixed with YT_
(see app/config.py):
| Variable | Default | Description |
|---|---|---|
YT_HOST |
127.0.0.1 |
Bind address |
YT_PORT |
8000 |
Bind port |
YT_DATA_DIR |
./tmp/data |
Database and runtime data directory |
YT_DOWNLOADS_DIR |
./tmp/downloads |
Completed download output directory |
YT_DATABASE_URL |
(derived from YT_DATA_DIR) |
SQLAlchemy database URL |
YT_LOG_LEVEL |
INFO |
Logging level |
YT_WORKERS |
1 |
Worker thread count |
yourtube/
├── app/
│ ├── main.py — FastAPI app, lifespan, /health endpoint
│ ├── config.py — pydantic-settings model
│ ├── db.py — Engine, session factory, SQLite pragmas
│ ├── models.py — SQLAlchemy ORM models
│ ├── schemas.py — Pydantic request/response contracts
│ ├── routes/ — JSON API and page route handlers
│ └── services/ — Queue, downloader, library, and settings logic
├── alembic/
│ ├── env.py
│ └── versions/ — Schema migrations
└── tests/
├── conftest.py — Migrated DB fixtures
├── test_config.py
├── test_db.py
└── test_health.py
uv run pytestTests use a temporary SQLite database built by Alembic migrations — no manual setup required.
MIT