Holen
Private YouTube downloader built with React, FastAPI, Docker, yt-dlp, and ffmpeg.
Holen is designed for a single person, household, homelab, or small trusted network. The downloader screen is open to anyone who can reach the app. The admin dashboard is protected by a local admin password and handles cached files, job history, cleanup, and optional media-library export for Plex, Jellyfin, Emby, or any archive folder.
[!IMPORTANT] Holen is self-host friendly, but it does not include a repository license yet. Add a license such as MIT, Apache-2.0, or AGPL-3.0 before presenting the project as open-source for public reuse.
Features
- YouTube videos, Shorts, and playlists
- Format choices: best MP4, 1080p, 720p, best audio, MP3
- Real-time download progress over Server-Sent Events
- Open downloader screen with no user accounts
- Password-protected admin dashboard
- Admin tools for cached files and job history
- Configurable cache limits and file TTL cleanup
- Optional cookies, proxy, and advanced
yt-dlparguments - Optional "Send" action for Plex, Jellyfin, Emby, or backup folders
Requirements
- Docker Desktop or Docker Engine with Docker Compose v2
- Node.js 18+ and npm only for local frontend development
Quick Start
1. Download HOLEN
Clone the repo, or download the ZIP from GitHub and open a terminal in the HOLEN folder.
git clone <repo-url> holen
cd holen
2. Run Guided Setup
Windows PowerShell:
Double-click setup.bat, or run:
powershell -ExecutionPolicy Bypass -File .\setup.ps1
macOS/Linux:
chmod +x setup.sh
./setup.sh
The setup asks for:
| Prompt | Recommended Answer |
|---|---|
| App URL | Press Enter for http://localhost:8888 |
| Local admin password | Press Enter for admin |
| Folder for Plex/Jellyfin/backups | Press Enter for ./media-library, or paste your media folder |
| Admin screen label | Press Enter for Media Library |
At the end, the script prints the app URL and admin password.
3. Open HOLEN
Open http://localhost:8888.
The downloader is available immediately. Press Admin and enter the admin password when you need the dashboard.
[!TIP] For Plex or Jellyfin, point your media server at the same folder you entered during setup. Admins can press Send on completed downloads to copy files into that library.
Manual Docker Setup
- Create your environment file:
Copy-Item .env.example .env
Linux/macOS:
cp .env.example .env
- Edit
.envand set an admin password:
PUBLIC_ORIGIN=http://localhost:8888
LOCAL_ADMIN_PASSWORD=admin
- Optional: choose where finished downloads can be copied for Plex/Jellyfin.
For a default local backup folder, keep:
MEDIA_LIBRARY_HOST_DIR=./media-library
MEDIA_LIBRARY_LABEL=Media Library
For Plex or Jellyfin, point MEDIA_LIBRARY_HOST_DIR at a real library folder on the host machine:
MEDIA_LIBRARY_HOST_DIR=D:\Media\Holen
MEDIA_LIBRARY_LABEL=Plex Library
Linux example:
MEDIA_LIBRARY_HOST_DIR=/srv/media/youtube
MEDIA_LIBRARY_LABEL=Jellyfin Library
- Start the app:
docker compose up -d --build
- Open:
Local Development
Run the backend:
cd backend
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txt
$env:LOCAL_ADMIN_PASSWORD="admin"
uvicorn app.main:app --reload
Linux/macOS shell equivalent:
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
LOCAL_ADMIN_PASSWORD=admin uvicorn app.main:app --reload
Run the frontend:
cd frontend
npm install
npm run dev
Open http://localhost:5173. Vite proxies /api to http://127.0.0.1:8000.
Configuration
| Variable | Default | Purpose |
|---|---|---|
PUBLIC_ORIGIN |
http://localhost:8888 |
Public app URL |
LOCAL_ADMIN_PASSWORD |
admin |
Admin dashboard password; change before exposing the app |
LOCAL_TOKEN_TTL_SECONDS |
604800 |
Admin session lifetime |
DOWNLOAD_DIR |
./downloads |
Host download/cache folder |
DATA_DIR |
./data |
Host folder for SQLite data when using Docker |
MAX_DURATION_SECONDS |
7200 |
Reject videos longer than this |
MAX_ACTIVE_JOBS |
1 |
Concurrent downloads |
MAX_QUEUED_JOBS |
25 |
Queue capacity |
MAX_TEMP_GB |
20 |
Stop accepting jobs when cache is full |
CACHE_PURGE_THRESHOLD_GB |
20 |
Start trimming old cached files at this size |
CACHE_PURGE_TARGET_GB |
16 |
Trim cache down to this size |
FILE_TTL_SECONDS |
3600 |
Auto-delete completed files after this many seconds |
CLEANUP_INTERVAL_SECONDS |
900 |
Cleanup loop interval |
YTDLP_COOKIES_PATH |
empty | Host path to Netscape-format cookies file |
YTDLP_PROXY |
empty | Proxy used only by yt-dlp |
YTDLP_EXTRA_ARGS |
empty | Extra advanced yt-dlp CLI arguments |
MEDIA_LIBRARY_HOST_DIR |
./media-library |
Host folder that receives files from the admin "Send" action |
MEDIA_LIBRARY_CONTAINER_DIR |
/media-library |
Container mount point for the media-library folder |
MEDIA_LIBRARY_DIR |
empty | Non-Docker local-dev export path; Docker sets this internally |
MEDIA_LIBRARY_LABEL |
Media Library |
Label shown in admin UI |
CLOUDFLARED_TOKEN |
empty | Optional Cloudflare Tunnel token |
Cookies And Proxies
Some YouTube requests require cookies. Export a dedicated browser profile or dedicated Google account to Netscape cookies format and set:
YTDLP_COOKIES_PATH=/absolute/path/to/cookies.txt
Treat cookies.txt like a password.
If a cloud provider IP is challenged by YouTube, cookies may still fail because the request source is the server IP. In that case, route only yt-dlp through an egress that works:
YTDLP_PROXY=socks5://user:pass@proxy-host:1080
Advanced yt-dlp options can be tested without changing code:
YTDLP_EXTRA_ARGS=--extractor-args "youtube:player-client=web"
Plex, Jellyfin, And Media Libraries
Holen keeps its download cache separate from your media server. Completed files stay in DOWNLOAD_DIR until they expire or are deleted. Admins can copy selected files into a media-library folder with the Send action.
For Docker, set the host folder in .env:
MEDIA_LIBRARY_HOST_DIR=/srv/media/youtube
MEDIA_LIBRARY_CONTAINER_DIR=/media-library
MEDIA_LIBRARY_LABEL=Jellyfin Library
Then restart:
docker compose up -d
Point Plex, Jellyfin, Emby, or another media app at the same host folder. Holen copies files into that path with clean filenames, so the media server can scan and ingest them without depending on Holen's temporary cache.
Cloudflare Tunnel
Set CLOUDFLARED_TOKEN in .env, then run:
docker compose --profile tunnel up -d --build
Point the tunnel at:
http://frontend:80
For public deployments, place Cloudflare Access, a VPN, or another access-control layer in front of Holen.
Remote Deploy Helper
deploy_to_server.py is optional. It uploads the repo to a remote Docker host and starts Docker Compose.
Install its dependency:
pip install paramiko
Example:
DEPLOY_HOST=203.0.113.10 DEPLOY_USER=ubuntu DEPLOY_PATH=/opt/holen python deploy_to_server.py
PowerShell:
$env:DEPLOY_HOST="203.0.113.10"
$env:DEPLOY_USER="ubuntu"
$env:DEPLOY_PATH="/opt/holen"
python deploy_to_server.py
If DEPLOY_PASSWORD is not set, the script uses SSH keys/agent auth.
Troubleshooting
Check health:
curl http://localhost:8888/api/health
Watch logs:
docker compose logs -f --tail=100
Update yt-dlp inside the backend image:
docker compose build --no-cache backend
docker compose up -d
Reset local app data:
docker compose down
rm -rf data downloads
docker compose up -d --build
PowerShell:
docker compose down
Remove-Item -Recurse -Force data, downloads
docker compose up -d --build
Project Structure
backend/ FastAPI API, SQLite jobs, yt-dlp runner
frontend/src/ React application
docker-compose.yml Local/production container stack
.env.example Environment template
setup.ps1 Guided Windows setup
setup.bat Double-click Windows setup launcher
setup.sh Guided macOS/Linux setup
deploy_to_server.py Optional generic SSH deploy helper
Security Notes
- Do not commit
.env,cookies.txt,downloads/, ordata/. - Do not commit
media-library/; it is local output for your media server or backups. - Use a dedicated Google account/profile for YouTube cookies.
- Keep
.envandcookies.txtprivate. - The downloader screen is open to anyone who can reach the app. Use Cloudflare Access, a VPN, or another network gate before exposing it publicly.
Built With
- batchfile
- css
- dockerfile
- html
- powershell
- python
- shell
- typescript
Log in or sign up for Devpost to join the conversation.