VGM Archiver is a single-file Python CLI for building stable local archives of YouTube playlist audio. It downloads only new videos, uses Opus by default, and adds an HD cover frame when possible.
Disclaimer: This tool is provided for personal and educational purposes only. You are solely responsible for ensuring that your use complies with applicable laws and the terms of service of any platform you use it with. The author does not encourage or condone downloading copyrighted content without permission.
- Opus-first output: by default, native Opus streams are remuxed; otherwise the best audio is transcoded directly to Opus once. Use
--formatto explicitly requestbest, MP3, FLAC, or WAV. - HD cover art: an extracted video frame replaces the embedded thumbnail when possible; failures keep the existing thumbnail.
- Stable local numbering: filenames combine a local number and video title; the manifest keeps each video ID bound to that number regardless of playlist order.
- Incremental operation: every run analyzes the full playlist and downloads only unknown video IDs.
- Resilience: failed videos retry independently, unavailable videos are reported, and later videos continue.
- Final summary: shows downloaded, skipped, unavailable, and failed counts, with titles, URLs, and errors where applicable.
- Store-mode archives: py7zr creates uncompressed 7z snapshots without recompressing audio.
- Python 3.14
ffmpeginPATH- Deno in
PATHfor yt-dlp's YouTube challenge solver
Install the pinned Python dependencies:
python3 -m pip install --disable-pip-version-check -r requirements.txtOn Ubuntu or Debian, install ffmpeg with sudo apt install ffmpeg. Install a
direct official Deno release binary for your platform and place it in PATH.
For convenience, build the Docker image: it already includes ffmpeg, checksum-verified Deno and Python dependencies.
Use one dedicated output directory per playlist. Do not mix multiple playlists in one collection directory because its manifest and yt-dlp archive belong to a single playlist.
# Archive a playlist as Opus in ./downloads
python3 main.py "https://www.youtube.com/playlist?list=PLxxxxxx"
# Choose an output folder or a compatible MP3 format
python3 main.py --output ~/Music/Myplaylist "https://www.youtube.com/playlist?list=PLxxxxxx"
python3 main.py --format mp3 "https://www.youtube.com/playlist?list=PLxxxxxx"
# Use Firefox cookies for restricted videos
python3 main.py --cookies-from-browser firefox "https://..."
# Preview planned downloads without writing media or collection state
python3 main.py --dry-run --output ~/Music "https://..."
# Skip the optional yt-dlp update availability check
python3 main.py --skip-version-check "https://..."| Option | Default | Description |
|---|---|---|
url |
required | YouTube playlist or video URL |
-f, --format |
opus |
Output format: opus, best, mp3, flac, or wav |
-o, --output |
./downloads |
Output directory; use one directory per playlist |
--retries |
10 |
Attempts per video; minimum 1 |
--cookies-from-browser |
none | Read cookies from the selected browser for restricted videos |
--no-video-cover |
off | Keep the YouTube thumbnail instead of extracting an HD video frame |
--no-zip |
off | Do not create a 7z archive after completion |
--zip-name |
playlist title plus timestamp | Custom 7z name without .7z |
--dry-run |
off | Analyze the playlist without changing files |
--skip-version-check |
off | Skip the optional yt-dlp update availability check |
| Format | Behavior |
|---|---|
opus |
Default; remuxes a native Opus source or directly transcodes the best available source to Opus once |
best |
Keeps the source audio codec without conversion |
mp3 |
Directly converts once to 320 kbps MP3 for broad compatibility |
flac |
Directly converts once; a lossy YouTube source does not gain quality |
wav |
Directly converts once; large files without embedded cover art |
Use one output directory per playlist. Managed media files are named
{padded_index} - {title}.{ext}. The collection has two state files:
.playlist_manifest.jsonmaps each video ID to its local number and filename..downloaded_archive.txtis yt-dlp's completion archive.
Do not edit either file. A video enters the manifest only after its final media file exists and yt-dlp has written its archive entry. Validation requires a one-to-one match between media files, manifest records, and archive IDs.
Temporary project files live in .tmp_runtime, which is removed after a normal
run and cleared at the next normal invocation after an interruption.
The default 7z archive contains managed media, the manifest, and the yt-dlp archive. Existing archives are never overwritten. When there are no new downloads and an archive already exists, archive creation is skipped.
Run the command again to update the collection. Existing media is never renamed,
removed, or downloaded again by normal incremental operation. Deleted or private
entries are listed in the final summary. --dry-run does not create or modify
media, archives, manifests, or thumbnail files.
Keep changes focused and preserve download, manifest, and archive integrity.
The Dockerfile provides a dedicated verify target for the isolated test suite.
Tests run only in Docker with Python 3.14:
# Build and run the standalone test runner without network access.
docker build --target verify -t vgm-archiver:verify .
docker run --rm --network=none vgm-archiver:verify
# Build and smoke-test the runtime image.
docker build --target runtime -t vgm-archiver .
docker run --rm --network=none vgm-archiver --help.github/workflows/ci.yml runs on new and updated pull requests. It runs flake8
and pylint on the GitHub runner, then runs the Docker test suite and runtime smoke
test after linting succeeds.
.github/workflows/release.yml runs only for pull requests merged into main.
It creates a GitHub Release with generated release notes when the merged pull
request contains Conventional Commit messages. Across all commits in the pull
request, type!: produces a major version, feat: a minor version, and fix:
or revert: a patch version. Commits without one of these prefixes create no
release.
Every commit must use a meaningful Conventional Commit message with one of these Semantic Versioning prefixes.