Automated YouTube video downloader with channel monitoring, retention management, and a web interface.
- Monitor YouTube channels and automatically download new videos
- Per-channel and per-video retention policies with cutoff dates
- Web UI for configuration and management
- Channels list sorted alphabetically with expandable downloaded-video panels
- Download status indicator for individually tracked videos
- Color-coded scoped logs with channel/video filtering
- REST API for programmatic control
- Cookie support for bypassing rate limits
- Automatic yt-dlp updates
- Docker support with multi-stage builds
- Concurrent downloads with configurable limits
# Using docker-compose
docker-compose up -d
# Or manually
docker build -t ytdm .
docker run -d -p 8080:8080 \
-v $(pwd)/data:/app/data \
-v $(pwd)/downloads:/app/downloads \
ytdmAccess the web UI at http://localhost:8080
Requirements: Go 1.21+, Python 3, yt-dlp, ffmpeg (optional)
# Quick start script
./run-local.sh
# Or manually
go build -o ytdm
./ytdmAll configuration can be managed through the web UI or by editing data/config.json:
{
"check_interval_seconds": "5m0s",
"retention_days": 7,
"disable_pruning": false,
"download_dir": "../downloads",
"file_name_pattern": "%(title)s-%(id)s.%(ext)s",
"max_concurrent_downloads": 3,
"yt_dlp": {
"path": "yt-dlp",
"update_interval_seconds": "24h0m0s",
"cookies_browser": "firefox",
"cookies_file": "data/cookies.txt",
"extractor_sleep_interval_seconds": "0s",
"download_throughput_limit": "",
"restrict_filenames": false,
"cache_dir": "data/yt-dlp-cache"
}
}Note: Duration fields in data/config.json use Go duration strings (e.g. "5m0s", "24h0m0s"). The web UI still accepts seconds and converts internally.
- check_interval_seconds: How often to check for new videos (default:
5m0s) - retention_days: Default retention period in days (default: 7)
- disable_pruning: Disable all automatic pruning globally (default: false)
- max_concurrent_downloads: Number of simultaneous downloads (default: 3)
- yt_dlp: Settings for yt-dlp
- path: Path to yt-dlp executable
- update_interval_seconds: How often to auto-update yt-dlp (e.g.
24h0m0s,0sto disable) - cookies_browser: Extract cookies from browser (
firefoxorchrome) - cookies_file: Path to Netscape format cookies file
- extractor_sleep_interval_seconds: Sleep between extractor requests (e.g.
0sto disable) - download_throughput_limit: Limit download speed (e.g.
100K,4.2M) - restrict_filenames: Use yt-dlp's filename restrictions
- cache_dir: Cache directory used by yt-dlp
Each channel can override the global retention with its own retention period and cutoff date:
- Retention Days: Keep videos for N days (0 = use global setting)
- Keep indefinitely (disable pruning): Skip automatic pruning for this channel/video entry
- Cutoff Date: Only download videos published on or after this date
Channel monitoring discovery behavior:
- If
cutoff_dateis set: videos on/aftercutoff_dateare eligible for first-time download (cutoff-first backlog behavior) - If
cutoff_dateis not set: discovery usespublish_date >= now - retention_days
Single-entry requested videos are always attempted when present in the list (publish date is not used as a download gate for single entries).
Pruning is based on download age (now - retention_days), with per-entry No Prune still respected.
Channel cutoff date is used for channel discovery eligibility and does not trigger pruning by publish date.
For channel entries, videos already downloaded and later pruned are tracked in persistence to prevent re-download loops.
YouTube may require authentication to avoid rate limiting. Two options:
Select browser in the Configuration tab. Requires the browser to be running and logged into YouTube.
- Export cookies using a browser extension (Cookie Editor, etc.)
- Paste Netscape format cookies in the Configuration tab
- Click "Save Pasted Cookies"
Example format:
# Netscape HTTP Cookie File
.youtube.com TRUE / TRUE 1805237469 COOKIE_NAME cookie_value
GET /api/channels- List all channelsPOST /api/channels- Add a channelDELETE /api/channels/{id}- Remove a channel
GET /api/videos- List all videosPOST /api/videos- Add a videoDELETE /api/videos/{id}- Remove a video
GET /api/config- Get configurationPUT /api/config- Update configuration
POST /api/cookies- Save pasted cookiesPOST /api/cookies/clear- Clear all cookies
GET /api/status- Service statusGET /api/logs- Recent logs- Optional query params:
scope_type=channel|video,scope_id=<id>
- Optional query params:
ytdm/
├── data/
│ ├── config.json # Application configuration
│ ├── data.json # Channel/video state
│ └── cookies.txt # YouTube cookies
├── downloads/ # Downloaded videos (organized by channel)
├── static/
│ ├── index.html # Web UI
│ └── app.js # UI JavaScript
├── *.go # Source files
├── *_test.go # Test files
├── Dockerfile
├── docker-compose.yml
└── run-local.sh # Local run script
# Run all tests
go test -v ./...
# Run specific test suite
go test -v -run TestStorage
go test -v -run TestConfig
go test -v -run TestVideoInfoBuilt with Go 1.21 using only the standard library (yt-dlp runs as subprocess).
Project structure:
main.go- Entry point and lifecycle managementconfig.go- Configuration with thread-safe operationsstorage.go- Persistent data managementdownloader.go- yt-dlp wrapperscheduler.go- Background task schedulingapi.go- REST API and web serverupdater.go- yt-dlp auto-updater
yt-dlp automatically updates itself to the latest version:
- Default: Updates every 24 hours
- Configurable via web UI or API
- Set to 0 to disable auto-updates
- Uses yt-dlp's built-in self-update mechanism (
yt-dlp -U)
ffmpeg updates require rebuilding the Docker image:
docker build -t ytdm:latest --no-cache .GET /api/channels- List all channelsPOST /api/channels- Add a new channel{ "name": "Channel Name", "url": "https://youtube.com/@channelname", "retention_days": 60, "cutoff_date": "2024-01-01T00:00:00Z" }cutoff_date(optional): Don't download videos published before this date
DELETE /api/channels/{id}- Remove a channel
GET /api/videos- List all videosPOST /api/videos- Add a new video{ "title": "Video Title", "url": "https://youtube.com/watch?v=VIDEO_ID", "retention_days": 90 }added_dateis set automatically when omitted.
DELETE /api/videos/{id}- Remove a video
GET /api/config- Get current configurationPUT /api/config- Update configuration{ "check_interval_seconds": 600, "retention_days": 60 }
GET /api/status- Get service statusGET /api/logs- Get recent logs (supportsscope_typeandscope_idfilters)
ytdm/
├── main.go # Entry point
├── config.go # Configuration management
├── storage.go # Persistent data storage
├── downloader.go # yt-dlp wrapper
├── scheduler.go # Background task scheduler
├── api.go # REST API handlers
├── static/
│ └── index.html # Web interface
├── Dockerfile # Docker build configuration
├── .dockerignore # Docker ignore file
└── README.md # This file
Downloaded videos are organized by channel:
/downloads/
├── Channel_Name_1/
│ ├── video1.mp4
│ └── video2.mp4
└── Channel_Name_2/
└── video3.mp4
All operations are logged to stdout, including:
- Video downloads
- Removal of old videos
- API requests
- Configuration changes
- Errors and warnings
The web UI logs tab supports:
- Color-coded channel/video scoped log lines
- Structured filtering by channel or individual video entry
- Jump-to-logs actions from the Channels and Videos views
- Channel-scoped lifecycle visibility including feed checks, download attempts, download outcomes, and channel prune events
Metadata lookups prioritize non-yt-dlp sources first (for example, YouTube oEmbed) and only fall back to yt-dlp when required.
The service is designed to handle errors gracefully:
- Failed downloads don't stop other downloads
- API errors return proper HTTP status codes
- Service continues running even if individual operations fail
- Automatic retry on transient failures
The application handles system signals (SIGINT/SIGTERM) gracefully:
- In-progress downloads: Always complete before shutdown
- Pending work: Skipped to speed up shutdown
- Timeout: 5-minute maximum wait for downloads to finish
- Clean exit: All resources properly released
When you press Ctrl+C or send a termination signal:
- Service stops accepting new download tasks
- In-progress downloads are allowed to complete
- Once all downloads finish, service exits cleanly
- If downloads take longer than 5 minutes, forces exit
The project includes comprehensive unit tests covering utility functions, storage operations, configuration management, and skip detection logic.
# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run specific tests
go test -v -run TestSanitizeFilename
# Run with coverage report
go test ./... -cover
# Generate detailed coverage report
go test ./... -coverprofile=coverage.out
go tool cover -func=coverage.out
# View coverage in browser
go tool cover -html=coverage.outMIT License