MeTube X is a lightweight, self-hosted web interface for yt-dlp that makes downloading videos, audio, subtitles, and thumbnails fast, configurable, and repeatable.
Built with Python 3.13+ (aiohttp + python-socketio) and Angular 21, MeTube X provides a modern, real-time interface for managing your media downloads with support for subscriptions, batch processing, presets, and more.
💡 MeTube X is a complete remaster of the original metube project, rebuilt from the ground up with modern technologies, new features, and improved architecture.
| Feature | Description |
|---|---|
| 🎬 Modern Web UI | Clean, responsive Angular interface with real-time Socket.IO updates |
| ⚡ Download Queue | Background queueing with concurrent download workers |
| 📺 Subscriptions | Channel/playlist watching with automatic enqueueing |
| 🎨 Presets | Pre-configured download templates (audio, video, custom) |
| 🔄 Per-Download Overrides | Fine-tune options per-download with freeform yt-dlp options |
| 📦 Batch Jobs | Process multiple URLs with job persistence and history |
| 🖼️ Thumbnails | Download and embed thumbnails into media files |
| 📝 Metadata Extraction | Extract and store video metadata (title, description, etc.) |
| 🔍 Search & Index | Full-text search across downloaded content |
| 📊 Monitoring | Built-in metrics endpoint (Prometheus-compatible) |
| 📚 OpenAPI Docs | Auto-generated API documentation with Swagger UI |
| 🔐 API Security | Optional API authentication and rate limiting |
| 💾 Persistent State | Safe atomic JSON writes for state management |
| 🐳 Docker Ready | Multi-arch support (amd64/arm64), production-optimized |
| 🧪 CI-Ready Tests | Comprehensive pytest suite for quality assurance |
- Docker installed
- A folder for downloads (e.g.,
C:\metube\downloadsor/srv/metube/downloads)
docker run -d \
-p 8081:8081 \
-v /path/to/downloads:/downloads \
-e PUID=1000 \
-e PGID=1000 \
ghcr.io/mohammadkobirshah/metubex:latestOpen your browser and navigate to: http://localhost:8081
services:
metube-x:
image: ghcr.io/mohammadkobirshah/metubex:latest
container_name: metube-x
restart: unless-stopped
ports:
- "8081:8081"
volumes:
- ./downloads:/downloads
environment:
- PUID=1000
- PGID=1000
- LOGLEVEL=INFO
- MAX_CONCURRENT_DOWNLOADS=3
- DEFAULT_THEME=auto
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/"]
interval: 30s
timeout: 10s
retries: 3| Variable | Default | Description |
|---|---|---|
DOWNLOAD_DIR |
/downloads |
Primary download directory |
AUDIO_DOWNLOAD_DIR |
%%DOWNLOAD_DIR |
Separate audio downloads directory |
TEMP_DIR |
%%DOWNLOAD_DIR |
Temporary file storage |
STATE_DIR |
/downloads/.metube |
JSON state storage location |
HOST |
0.0.0.0 |
Server binding host |
PORT |
8081 |
Server binding port |
MAX_CONCURRENT_DOWNLOADS |
3 |
Number of parallel download workers |
LOGLEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
DEFAULT_THEME |
auto |
UI theme (auto, light, dark) |
CORS_ALLOWED_ORIGINS |
(empty) | Comma-separated CORS origins |
ENABLE_ACCESSLOG |
false |
Enable HTTP access logging |
ENABLE_JSON_LOGGING |
false |
Enable structured JSON logging |
| Variable | Default | Description |
|---|---|---|
YTDL_OPTIONS |
{} |
Global yt-dlp options (JSON string) |
YTDL_OPTIONS_FILE |
(empty) | Path to JSON file with global options |
YTDL_OPTIONS_PRESETS |
{} |
Named presets (JSON object) |
YTDL_OPTIONS_PRESETS_FILE |
(empty) | Path to presets JSON file |
ALLOW_YTDL_OPTIONS_OVERRIDES |
false |
Enable per-download freeform options |
| Variable | Default | Description |
|---|---|---|
ENABLE_API_AUTH |
false |
Require API key authentication |
API_KEY |
(empty) | Secret API key for access |
ENABLE_RATE_LIMIT |
false |
Enable rate limiting |
RATE_LIMIT_PER_MINUTE |
60 |
Requests allowed per minute |
| Variable | Default | Description |
|---|---|---|
SUBSCRIPTION_DEFAULT_CHECK_INTERVAL |
60 |
Minutes between subscription checks |
SUBSCRIPTION_SCAN_PLAYLIST_END |
50 |
Number of playlist items to scan |
SUBSCRIPTION_MAX_SEEN_IDS |
50000 |
Maximum video IDs to track per subscription |
MeTube X supports three layers of yt-dlp configuration, applied in order:
Global Options → Named Presets → Per-Download Overrides
Base configuration applied to all downloads.
{
"format": "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"embed-thumbnail": true,
"add-metadata": true
}Pre-defined configurations selectable in the UI.
{
"audio-mp3": {
"format": "bestaudio/best",
"extractor-args": "youtube:player_client=android",
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "192"
}]
},
"video-1080p": {
"format": "bestvideo[height<=1080]+bestaudio/best[height<=1080]",
"output-template": "%(title)s_1080p.%(ext)s"
},
"audio-64k": {
"format": "bestaudio/best",
"postprocessors": [{
"key": "FFmpegExtractAudio",
"preferredcodec": "mp3",
"preferredquality": "64"
}]
}
}Enabled via ALLOW_YTDL_OPTIONS_OVERRIDES=true. Allows freeform JSON overrides per download.
⚠️ Security Note: Only enable overrides if you trust users, as they can pass arbitrary yt-dlp arguments.
# 1. Clone the repository
git clone https://github.com/MohammadKobirShah/MeTubeX.git
cd MeTubeX
# 2. Build the frontend
cd ui
pnpm install --frozen-lockfile
pnpm run build
cd ..
# 3. Install Python dependencies
uv sync --frozen --group dev
# 4. Run the server
uv run python3 app/main.pyOpen http://localhost:8081 in your browser.
Run the complete pytest suite from the repo root:
# Using uv (recommended)
uv run pytest -q
# Or with coverage report
uv run pytest --cov=app --cov-report=term-missing- API Tests: REST endpoint validation
- Download Queue: Queue management and persistence
- Subscriptions: Channel/playlist watching logic
- State Store: Atomic JSON operations
- Configuration: Environment variable parsing
MeTube X provides a versioned REST API (/api/v1/) with full OpenAPI 3.0 spec support. An interactive Swagger UI is available at /api/v1/docs/ui.
http://localhost:8081/api/v1
If ENABLE_API_AUTH=true, include your API key in requests:
curl -H "Authorization: Bearer YOUR_API_KEY" http://localhost:8081/api/v1/downloads| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check endpoint |
| GET | /version |
Get service version info |
| GET | /downloads |
List all downloads (queue, pending, done) |
| POST | /downloads/add |
Add a new download |
| POST | /downloads/add-batch |
Add multiple downloads |
| GET | /downloads/{id} |
Get download status by ID |
| POST | /downloads/{id}/start |
Start a pending download |
| GET | /presets/audio |
List audio presets |
| POST | /presets/audio |
Create/update audio preset |
| GET | /presets/video |
List video presets |
| POST | /subscriptions |
Create new subscription |
| GET | /subscriptions |
List all subscriptions |
| GET | /metadata/extract?url=... |
Extract metadata from URL |
| POST | /thumbnails/generate |
Generate thumbnail from video |
| GET | /search?q=... |
Full-text search |
| POST | /jobs/batch-add |
Submit batch job |
| POST | /analysis/transcribe |
Queue transcription |
| GET | /metrics |
Prometheus-compatible metrics |
GET /api/v1/healthResponse:
{ "status": "ok" }GET /api/v1/versionResponse:
{
"version": "1.0.0",
"yt-dlp": "2024.12.23"
}GET /api/v1/downloads?page=1Response:
{
"queue": [
{
"id": "abc123",
"title": "Sample Video",
"status": "downloading",
"filename": "Sample Video.mp4",
"size": 52428800
}
],
"pending": [],
"done": []
}POST /api/v1/downloads/add
Content-Type: application/jsonRequest Body:
{
"url": "https://youtube.com/watch?v=...",
"download_type": "video",
"quality": "1080p",
"codec": "mp4",
"folder": "videos",
"auto_start": true
}Response:
{
"status": "ok",
"msg": "Added to queue"
}POST /api/v1/downloads/add-batch
Content-Type: application/jsonRequest Body:
{
"items": [
{ "url": "https://youtube.com/watch?v=1", "download_type": "video", "quality": "720p" },
{ "url": "https://youtube.com/watch?v=2", "download_type": "audio", "codec": "mp3" }
]
}Response:
{ "status": "accepted" }GET /api/v1/downloads/{id}Response:
{
"id": "abc123",
"title": "Sample Video",
"status": "completed",
"filename": "Sample Video.mp4",
"size": 104857600
}POST /api/v1/downloads/{id}/startResponse:
{ "status": "started" }GET /api/v1/presets/audioResponse:
[
{
"name": "audio-mp3-192",
"bitrate_kbps": 192,
"description": "MP3 192kbps"
},
{
"name": "audio-64k",
"bitrate_kbps": 64,
"description": "Low quality 64kbps"
}
]POST /api/v1/presets/audio
Content-Type: application/jsonRequest Body:
{
"name": "custom-audio",
"bitrate_kbps": 256,
"description": "Custom 256kbps preset"
}GET /api/v1/presets/videoResponse:
[
{
"name": "video-1080p",
"resolution": "1920x1080",
"description": "Full HD 1080p"
},
{
"name": "video-4k",
"resolution": "3840x2160",
"description": "Ultra HD 4K"
}
]POST /api/v1/subscriptions
Content-Type: application/jsonRequest Body:
{
"url": "https://youtube.com/channel/...",
"check_interval_minutes": 60
}Response:
{ "status": "created", "id": "sub_abc123" }GET /api/v1/subscriptionsResponse:
[
{
"id": "sub_abc123",
"url": "https://youtube.com/channel/...",
"check_interval_minutes": 60,
"last_check": "2024-12-23T10:30:00Z",
"videos_found": 5
}
]GET /api/v1/metadata/extract?url=https://youtube.com/watch?v=...Response:
{
"title": "Sample Video Title",
"description": "Video description here...",
"tags": ["tutorial", "python", "coding"],
"upload_date": "2024-12-01",
"duration": 1200,
"uploader": "Channel Name"
}POST /api/v1/thumbnails/generate
Content-Type: application/jsonRequest Body:
{
"url": "https://youtube.com/watch?v=...",
"timestamp": 30.5
}Response:
{
"status": "generated",
"thumbnail_url": "/thumbnails/abc123.jpg"
}GET /api/v1/search?q=tutorialResponse:
{
"total": 10,
"items": [
{
"id": "abc123",
"title": "Python Tutorial",
"status": "completed",
"filename": "Python Tutorial.mp4",
"size": 52428800
}
]
}POST /api/v1/jobs/batch-add
Content-Type: application/jsonRequest Body:
{
"items": [
{ "url": "...", "download_type": "video" },
{ "url": "...", "download_type": "audio" }
]
}Response:
{
"job_id": "job_xyz789",
"status": "queued"
}POST /api/v1/analysis/transcribe
Content-Type: application/jsonRequest Body:
{
"url": "https://youtube.com/watch?v=..."
}Response:
{
"status": "transcription_queued",
"job_id": "trans_abc123"
}GET /api/v1/metricsResponse (Prometheus format):
# HELP metube_downloads_total Total downloads
# TYPE metube_downloads_total counter
metube_downloads_total 1234
# HELP metube_queue_size Current queue size
# TYPE metube_queue_size gauge
metube_queue_size 5
MeTube X supports API versioning via the URL path:
v1— Current stable API
Configure allowed versions with SUPPORTED_API_VERSIONS environment variable.
When ENABLE_RATE_LIMIT=true, API requests are limited to RATE_LIMIT_PER_MINUTE (default: 60) requests per minute per IP.
Rate Limited Response:
{
"error": "Rate limit exceeded",
"retry_after": 30
}Interactive API documentation is available at:
http://localhost:8081/api/v1/docs/ui
Raw OpenAPI spec:
http://localhost:8081/api/v1/docs/openapi.yaml
| Issue | Solution |
|---|---|
| Thumbnail embedding fails | Ensure ffmpeg is available in the container PATH |
| Docker build fails | Run uv lock locally and commit changes |
| Downloads not starting | Check MAX_CONCURRENT_DOWNLOADS setting |
| Subscription not working | Verify SUBSCRIPTION_DEFAULT_CHECK_INTERVAL |
| API returns 401 | Enable ENABLE_API_AUTH and set API_KEY |
| CORS errors | Configure CORS_ALLOWED_ORIGINS with your domain |
View container logs:
docker logs metube-xEnable debug logging:
docker run -e LOGLEVEL=DEBUG ...Contributions are welcome! Please follow these guidelines:
- Open an issue for discussion before implementing large features
- Keep PRs focused — one feature or fix per pull request
- Add tests when applicable — the project targets 100% test coverage
- Follow code style — 4-space indentation for Python, 2-space for TypeScript
- Update documentation for any user-facing changes
# Frontend with hot reload
cd ui && pnpm run start
# Backend with auto-reload
uv run --watch app/main.pyThis project is provided under the terms in LICENSE.
- Alexta69 — Creator of the original metube
- GitHub Container Registry:
ghcr.io/mohammadkobirshah/metubex - Docker Hub: metubex
| Layer | Technology |
|---|---|
| Backend | Python 3.13+, aiohttp, python-socketio 5.x |
| Frontend | Angular 21, TypeScript, Bootstrap 5, SASS |
| Download Engine | yt-dlp |
| State | Atomic JSON (json-based persistence) |
| Container | Multi-stage Docker, multi-arch (amd64/arm64) |
- yt-dlp — Powerful media downloader
- aiohttp — Async HTTP server/client
- python-socketio — Real-time communication
- Angular — Modern web framework
| Method | Link |
|---|---|
| 🐛 Bug Reports | GitHub Issues |
| 💡 Feature Requests | GitHub Issues |
| 💬 Discussions | GitHub Discussions |
| 🐙 Source Code | GitHub Repository |

