YouTube Playlist to MP3 Ripper with web interface
1.9K
Download YouTube playlists as MP3 files with a beautiful web interface or CLI.
Pull and run the pre-built image:
# Using Docker
docker run -d -p 5000:5000 \
-v ./downloads:/app/downloads \
--name music-ripper \
awkto/music-ripper:latest
# Using Podman
podman run -d -p 5000:5000 \
-v ./downloads:/app/downloads \
--name music-ripper \
docker.io/awkto/music-ripper:latest
# Access the web interface at http://localhost:5000
# Clone the repository
git clone https://github.com/awkto/music-ripper.git
cd music-ripper
# Start with Docker Compose
docker-compose up -d
# Or with Podman
podman-compose up -d
# Access the web interface at http://localhost:5000
# Clone the repository
git clone https://github.com/awkto/music-ripper.git
cd music-ripper
# Install dependencies
pip install -r requirements.txt
# Start the web server
python3 app.py
# Access the web interface at http://localhost:5000
# Install dependencies
pip install yt-dlp
# Run with pure playlist URL
python3 playlist_ripper.py 'https://www.youtube.com/playlist?list=PLxxxx'
# Run with mixed video+playlist URL
python3 playlist_ripper.py 'https://www.youtube.com/watch?v=xxxxx&list=PLxxxx'
# With custom folder name
python3 playlist_ripper.py '<youtube_playlist_url>' 'MyPlaylist'
Note: Both pure playlist URLs and mixed video+playlist URLs are supported. The tool will always download the entire playlist.
music-ripper/
├── app.py # Web application
├── playlist_ripper.py # CLI tool
├── templates/
│ └── index.html # Web interface
├── downloads/ # Web downloads directory
│ ├── PlaylistName/
│ │ ├── Artist-SongTitle.mp3
│ │ └── ...
│ └── PlaylistName.m3u
└── BoruBiro/ # CLI downloads (if used)
├── Artist-SongTitle.mp3
└── ...
The web application exposes the following REST API:
GET / - Web interfacePOST /api/download - Start a new download jobGET /api/jobs - List all jobsGET /api/jobs/<job_id> - Get job statusGET /api/download/<job_id>/zip - Download completed playlist as ZIPDELETE /api/jobs/<job_id> - Delete a job and its filesAuthentication is off by default — set RIPPER_PASSWORD to turn it on.
This is used by the Android share app so you can share YouTube
links to your server without exposing it fully.
| Env var | Purpose |
|---|---|
RIPPER_PASSWORD | Enables auth. Password for the web login (/login). Unset = fully open (original behaviour). |
SECRET_KEY | Flask session-signing key. Set a fixed random value so logins survive restarts; otherwise a random one is generated each boot. |
TOKENS_FILE | Where API tokens are persisted. Defaults to downloads/.tokens.json (inside the mounted volume, so it survives restarts). |
When enabled:
/login./settings page (behind login): add short,
human-typable tokens for the mobile app, or auto-generate one.POST /api/download and GET /api/jobs[/<id>] accept either a logged-in
session or a valid X-API-Token header. Tokens are scoped: they can only
queue downloads and read job progress — not manage tokens or browse files.docker run -d -p 5010:5000 \
-e RIPPER_PASSWORD='your-web-password' \
-e SECRET_KEY='long-random-string' \
-v /path/to/downloads:/app/downloads \
--name awkto-mp3 awkto/music-ripper:latest
The application uses Docker Compose with volume mounting for persistent downloads:
services:
music-ripper:
ports:
- "5000:5000"
volumes:
- ./downloads:/app/downloads # Persist downloads on host
# Create virtual environment
python3 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txt
# Run in development mode
python3 app.py
pip install --upgrade yt-dlppip install flaskMIT
Everything the UI does is a plain JSON API, so it can be scripted. When auth is
enabled (RIPPER_PASSWORD set), pass X-API-Token: <token> (or Authorization: Bearer <token>) on the endpoints below; tokens are managed in Settings.
# List a playlist's tracks (metadata only, nothing downloaded)
curl -X POST /api/playlist/preview -H 'Content-Type: application/json' \
-d '{"playlist_url": "https://www.youtube.com/playlist?list=PLxxxx"}'
# -> {"playlist_title": "...", "track_count": 43, "tracks": [{"id", "title", "uploader", "duration", "index"}]}
# Download the whole playlist
curl -X POST /api/download -d '{"playlist_url": "...", "custom_name": "MyFolder"}'
# Download only hand-picked tracks (ids from the preview)
curl -X POST /api/download -d '{"playlist_url": "...", "video_ids": ["lLZvJ_rtZO8", "x4OI91W2jFE"]}'
# Job progress
curl /api/jobs # all jobs
curl /api/jobs/<job_id> # one job
A sync target binds a downloads folder to a playlist URL. Syncing diffs the
folder against the live playlist: new tracks are downloaded, tracks removed from
the playlist are deleted. Only files the app downloaded itself (tracked in
downloads/.ripper.db by YouTube video ID) are ever deleted — anything else in
the folder is reported as unmanaged and never touched.
# Manage targets
curl /api/sync/targets
curl -X POST /api/sync/targets -d '{"playlist_url": "...", "directory": "MyFolder", "name": "optional"}'
curl -X DELETE /api/sync/targets/<id>
# Dry run: what would change? (downloads/deletes nothing)
curl -X POST /api/sync/targets/<id>/preview
# -> {"to_add": [...], "to_remove": [...], "unchanged_count": N, "unmanaged": [...]}
# Reconcile (runs as a job; poll /api/jobs/<job_id>)
curl -X POST /api/sync/targets/<id>/apply -d '{"delete_removed": true}'
curl /api/browse # top-level folders/files
curl /api/browse/<dir> # files in a folder (with tracked video_id where known)
curl /api/download/file/<dir>/<file> # fetch one MP3
curl /api/stream/<dir>/<file> # stream inline
curl -X DELETE /api/browse/file/<dir>/<file> # delete (also untracks + un-archives it)
curl /api/download/directory/<dir> # folder as ZIP
Content type
Image
Digest
sha256:a448c6dc0…
Size
330.1 MB
Last updated
about 1 month ago
docker pull awkto/music-ripper