Turn any Twitch / Kick / m3u8 / MP4 stream into a Spotify playlist of every song that played.
What you get: a tracklist (TXT + CSV with timestamps, titles, artists, Spotify and YouTube links) for any VOD, plus an optional Spotify playlist auto-built from the identified tracks. In --streamer-mode, one rolling playlist per streamer that grows every week.
Who it's for: fans who want the setlist from their favorite streamer's last VOD, clippers who need music IDs before posting, streamers who want to publish their own VOD soundtrack.
Cost: free (MIT). No cloud APIs — ShazamIO is a free unofficial Shazam client, Spotify use is free, and the only thing running is your local ffmpeg and Python.
This is not: a live-stream identifier, a Spotify-to-Twitch bot, a Discord plugin, or a hosted service. It scans a recorded VOD and produces files locally — what you do with them is up to you.
Need ffmpeg + Python 3.10+ on your PATH. Then:
git clone https://github.com/NexRushVr/stream-tracklist.git
cd stream-tracklist
pip install -r requirements.txt
# Scan the latest Kick VOD for any channel (no Spotify account needed — outputs CSV/TXT)
python stream_songs.py "https://kick.com/abehamm"Want a Spotify playlist too? Add your Spotify app credentials to .env (see Spotify Setup) and pass --create-playlist. Don't need Spotify? Skip that whole section — the tool falls back to Spotify search links and YouTube links in the CSV/TXT output and works fine without any account.
Check yours: python --version. On Windows, install from python.org and tick Add Python to PATH during setup (don't use the Microsoft Store install — pip is awkward there).
Per-OS one-liners:
# Windows (winget)
winget install Gyan.FFmpeg
# macOS (Homebrew)
brew install ffmpeg
# Debian / Ubuntu
sudo apt install ffmpegVerify with ffmpeg -version. If "command not recognized," ffmpeg isn't on your PATH yet — restart the terminal, or for the Windows zip install copy ffmpeg.exe into a folder that's already on PATH (e.g. C:\Windows\System32).
git clone https://github.com/NexRushVr/stream-tracklist.git
cd stream-tracklist
pip install -r requirements.txtNo git? Click Code → Download ZIP on the GitHub page and unzip.
Only needed to auto-build a Spotify playlist or to get direct Spotify track URLs in the CSV. See Spotify Setup below. Without these you still get a tracklist file with Spotify search links.
python stream_songs.py [OPTIONS] SOURCESOURCE can be:
- A local
.mp4file path - A direct
.m3u8URL - A
vodvod.topchannel URL — auto-picks the latest VOD - A
kick.com/<handle>URL — uses Kick's public videos API
Windows users: grab stream-tracklist.exe from the latest release, drop it in a folder, run install.bat once to build the local .venv (downloads spotipy / shazamio / yt-dlp / flask), then double-click the exe.
From source (any platform):
python stream_songs.py --serveOpens a native window (or a browser tab with --no-window / when pywebview isn't installed) with forms for streamer-mode, YouTube→Spotify, single-source scans, and Spotify backfill — plus a live job feed showing sample progress, recent matches, and the playlist URL the moment it's created. Each job is a normal subprocess of the CLI, so anything you can do here works the same from the terminal. On Windows you can double-click stream-tracklist.example.bat (copy to stream-tracklist.bat, edit PY if your Python interpreter isn't first on PATH).
# 1. Scan a Kick streamer's latest VOD, build a Spotify playlist
python stream_songs.py --create-playlist "https://kick.com/abehamm"
# 2. Scan every VOD for a list of streamers, one rolling playlist per streamer
python stream_songs.py --streamer-mode --streamers eevi kick:abehamm
# 3. Rebuild a playlist from existing CSV output without re-scanning audio
python stream_songs.py --rebuild eeviAll flags (click to expand)
| Flag | Default | Description |
|---|---|---|
--interval INT |
120 |
Seconds between audio samples |
--clip-duration INT |
20 |
Length of each audio clip in seconds |
--output-dir PATH |
output |
Directory for TXT / CSV / *_matches.jsonl files (created on demand) |
--output-name TEXT |
derived | Base filename for TXT/CSV output |
--max-duration INT |
86400 |
Fallback cap when duration can't be probed |
--create-playlist |
off | Create a Spotify playlist from identified tracks |
--mega-playlist NAME |
— | One combined playlist for every VOD on a channel (--all mode) |
--private-playlist |
off | Make playlists private (default: public/shareable) |
--playlist-name TEXT |
source name | Override the playlist name (single-VOD mode only) |
--streamer-mode |
off | Per-streamer log + rolling playlist named after the handle |
--streamers HANDLE [...] |
— | Multi-streamer mode (implies --all). Prefix with kick: for Kick. |
--streamers-file PATH |
— | Read handles from a file (one per line, # comments OK); merges with --streamers, scopes --backfill-spotify |
--rescan |
off | In streamer mode, re-process VODs already in the log |
--fresh |
off | Ignore an existing _matches.jsonl and rescan from scratch (default: resume/re-resolve) |
--rebuild HANDLE [...] |
— | Rebuild playlist(s) from existing CSV files; no audio scanning |
--backfill-spotify [HANDLE ...] |
— | Retry Spotify resolution for unresolved (/search/) CSV rows after a quota reset; rewrites CSVs in place + updates playlists. No args = every CSV in --output-dir |
--from-youtube URL |
— | Convert a YouTube playlist into a Spotify playlist; no audio scanning (needs yt-dlp) |
--log-dir PATH |
logs |
Where per-streamer JSON logs are kept |
--list-streamers |
— | List streamers with logs and exit |
--show-streamer HANDLE |
— | Print the VOD log for a streamer and exit |
--retries INT |
4 |
Extra offsets per slot on no-match |
--verbose |
off | Print each match as it arrives |
--dry-run |
off | Print sample timestamps then exit |
--serve |
off | Launch the local web UI (pywebview window if installed, else browser) |
--port INT |
8731 |
Port for --serve |
--no-window |
off | With --serve, print the URL instead of opening a pywebview window |
--version |
— | Print version and exit |
Resolving source: https://kick.com/abehamm
Source name : Friday Night Music Stream
Media type : m3u8
Spotify : enabled
Duration : 03:59:03
Sampling 80 timestamps every 180s (10s clips)...
[10/80] 00:27:00 [match] "Song Title" by Artist A
[12/80] 00:33:00 [match] "Another Track" by Artist B
[16/80] 00:45:00 [match] "Third One" by Artist C
...
[80/80] 03:57:00 [match] "Closing Song" by Artist D
Looking up 21 track(s) on Spotify... 21 found.
Creating Spotify playlist: "Friday Night Music Stream" (21 tracks)...
Playlist created: https://open.spotify.com/playlist/XXXXXXXXXXXXXXXXXXXXXXX
Output written to:
.\abehamm_2026_05_13_songs.txt
.\abehamm_2026_05_13_songs.csv
Two files land in --output-dir: a human-readable .txt and a .csv (timestamp, title, artist, Spotify link, YouTube link).
# First run: scans every VOD, builds the "eevi" playlist, logs each VOD to logs/eevi.json
python stream_songs.py --streamer-mode --all "https://vodvod.top/channels/@eevi"
# Next week: skips VODs already in the log, appends only new tracks (deduped) to the same playlist
python stream_songs.py --streamer-mode --all "https://vodvod.top/channels/@eevi"
# Force re-scan of everything (e.g. after improving recognition settings)
python stream_songs.py --streamer-mode --all --rescan "https://vodvod.top/channels/@eevi"Each log entry records the tool version, song count, tracks appended, and original m3u8 URL — so you can replay or audit later. The playlist's description links back to the streamer's Twitch or Kick page.
# One vodvod handle + one Kick handle in a single run
python stream_songs.py --streamer-mode --streamers eevi kick:abehammHandles can be written with or without the leading @. The tool processes them sequentially (one streamer at a time so Shazam/Spotify rate limits stay sane) and prints a per-streamer summary at the end.
A Task Scheduler template lives at scheduled_weekly_scan.example.bat. To use it:
- Copy it to
scheduled_weekly_scan.bat(the non-example name is git-ignored so it stays local). - Edit
REPO_DIRandSTREAMERS. - Run the tool interactively once first to complete the Spotify OAuth flow — the cached token in
.spotify_token_cacheis what lets the scheduled run go headless. - In Task Scheduler → Create Task → check Run whether user is logged on or not (and supply your password). Point the action at the
.bat. - If you have multiple Python versions installed, replace
pythonin the.batwithpy -3.12(or your version) so it picks the right interpreter.
python stream_songs.py --rebuild eevi abehammFor each handle this globs <output-dir>/<handle>*_songs.csv, harvests Spotify URLs, dedupes, and appends to a playlist named after the handle. Useful as one-shot recovery if a playlist ended up empty; the regular --streamer-mode flow stays the source of truth going forward.
Spotify's Web API has an undocumented daily cap on search calls. A large batch can exhaust it mid-run — when that happens the scan no longer hangs: it finishes, and any tracks it couldn't resolve are written with a /search/ fallback URL (the recognitions and the new ISRC column are still saved). Once the quota window resets (next day), fill them in without re-scanning any audio:
python stream_songs.py --backfill-spotify moonbuvr zayi # scope to handles
python stream_songs.py --backfill-spotify # or every CSV in --output-dirThis retries only the unresolved rows — preferring an exact isrc: lookup when the ISRC column is present — rewrites the CSVs in place, and dedup-appends the newly-resolved tracks to each handle's playlist. The startup search cache (primed from your existing CSVs) means tracks already resolved in a prior run cost no API calls.
Every VOD scan streams its recognitions to <output-dir>/<name>_matches.jsonl as each slot completes — before any Spotify lookup. So a crash, Ctrl-C, or quota stall part-way through a multi-hour VOD never loses the work already done. Just run the same command again:
- An interrupted scan resumes from the last sampled timestamp ("Resuming: recovered N match(es), continuing from HH:MM:SS").
- A completed scan re-resolves from the artifact and skips audio entirely ("Found a complete matches artifact — re-resolving without re-scanning audio").
- Pass
--freshto ignore the artifact and rescan from zero.
In --streamer-mode this composes with the per-streamer log: finished VODs are skipped outright, and a VOD interrupted mid-scan resumes on the next run instead of restarting.
Keep a list of handles in a file and scan only their new VODs each day:
python stream_songs.py --streamer-mode --streamers-file daily_streamers.txt
python stream_songs.py --backfill-spotify --streamers-file daily_streamers.txt # next-day cleanupdaily_streamers.txt ships as a starting point (a vetted list of active VRChat-DJ and music streamers, one handle per line, # comments allowed). scheduled_daily_scan.example.bat wires both commands for Windows Task Scheduler. Because --streamer-mode skips already-logged VODs, a daily run only scans what's new — usually quick.
python stream_songs.py --from-youtube "https://youtube.com/playlist?list=PLeczTdFh8vGGSMdZddA8mxFLsTUfbZmUz"No audio scanning — yt-dlp enumerates the playlist (flat, it never downloads media), each video title is parsed into (artist, title), and matches are dedup-appended into a Spotify playlist named after the YouTube playlist (override with --playlist-name). Re-running is idempotent: already-present tracks are skipped. "X - Topic" (YouTube Music) channels and Artist - Title (Official Video) titles are both handled; version info that changes the track (Remix, feat., Acoustic, Live) is preserved for the search. Install yt-dlp with pip install yt-dlp.
python stream_songs.py --list-streamers # totals + playlist URL per streamer
python stream_songs.py --show-streamer eevi # one streamer's VOD historyRaw logs are plain JSON at logs/<handle>.json if you'd rather jq them.
Only needed for --create-playlist, --streamer-mode, --rebuild, --from-youtube, and direct Spotify track URLs in the CSV. Skip entirely otherwise.
- Go to developer.spotify.com/dashboard → Create app. Name + description can be anything; website can be blank.
- Set the redirect URI to exactly
http://127.0.0.1:8888/callback— click Add, then Save at the bottom.localhostwill not work — Spotify rejects it as of 2025; you must use127.0.0.1. - From the app's Settings page, copy Client ID and click "View client secret" to copy Client secret.
cp .env.example .env(Windows:copy .env.example .env), open.envin a text editor (on Windows: right-click → Open with → Notepad), and paste both values:
SPOTIFY_CLIENT_ID=...
SPOTIFY_CLIENT_SECRET=...
SPOTIFY_REDIRECT_URI=http://127.0.0.1:8888/callback
- First playlist creation triggers a one-time browser OAuth login. The browser will show "127.0.0.1 refused to connect" after you click Agree — that's normal and expected. The tool reads the redirect URL out of the browser bar; you can close the tab once it's done. The token is cached in
.spotify_token_cacheso subsequent runs are automatic.
ffmpeg : command not found / not recognized— ffmpeg isn't on PATH. Reinstall viawinget/brew/aptper Install, then close and reopen your terminal.pythonopens the Microsoft Store — you have the stub installed, not real Python. Install from python.org with Add Python to PATH checked.- Spotify OAuth: browser shows "site can't be reached" — that's intentional. The tool reads the auth code out of the URL bar. Just wait for the terminal to print "Playlist created."
- Spotify rejects the redirect URI — you typed
localhost. Change it to127.0.0.1in your Spotify app settings. HTTP 403on every ffmpeg sample — the m3u8 token expired (vodvod and Kick VOD URLs are short-lived). Re-run the tool; it will fetch a fresh URL.- No matches at all — music is probably buried under game/voice audio. Increase
--clip-durationor sample more frequently with--interval 60. Match rate naturally drops when music is in the background.
- Source resolution — for
vodvod.topand Kick URLs, the relevant API returns the latest VOD's M3U8. Direct M3U8 and local MP4 are used as-is. - Duration detection —
ffprobereads the total length. Falls back to--max-durationfor live or unprobeable streams. - Audio extraction —
ffmpegseeks to each timestamp with-ssbefore-i(fast seeking — for M3U8 this only downloads the segments around each sample, not the full stream) and extracts a short mono MP3 clip. - Recognition — ShazamIO sends the clip to Shazam's backend. Retries once on failure.
- Deduplication — same (title, artist) at multiple timestamps keeps the earliest.
- Spotify lookup — searches by
track:{title} artist:{artist}. Falls back to a broader query if no exact match. - Output — writes TXT + CSV, then optionally creates / appends to a Spotify playlist via
/me/playlists.
stream-tracklist/
├── stream_songs.py # CLI entry point
├── requirements.txt
├── .env.example
└── src/
├── source.py # Resolves MP4 / M3U8 / vodvod.top / kick.com input
├── extractor.py # ffmpeg audio extraction + ffprobe duration
├── recognizer.py # ShazamIO wrapper + deduplication
├── spotify_client.py # Spotify search, playlist create / find / dedup-append
├── streamer_log.py # Per-streamer JSON log
└── output.py # TXT/CSV writing, URL generation
Cutting clips from the same VODs? See twitch-highlights — same author, same source resolvers, captioned shorts instead of playlists.
pip install -r requirements-test.txt # just pytest — no heavy audio stack
pytestCovers the pure-logic units (formatting, dedup, source resolution, log JSON, handle validation, CSV-injection guard). External boundaries (Shazam, Spotify, vodvod/Kick APIs, ffmpeg) are mocked or omitted. CI runs the suite on Python 3.10 / 3.11 / 3.12 and on Windows for every push, PR, and once a day to catch upstream API drift.
- Match rate depends on how clean the music is in the mix. Background music with game audio or voice over it won't always match — most clearly audible tracks will.
- vodvod.top is its own API (not yt-dlp compatible). If their API blocks the tool, grab the
.m3u8URL from your browser's DevTools (Network tab, filterm3u8) and pass it directly. - ShazamIO is a free unofficial Shazam client — no API key required.
See CONTRIBUTING.md. Bug fixes for vodvod/Kick API drift and Spotify edge cases are especially welcome.
See SECURITY.md for the responsible-disclosure path.
Built collaboratively with agentic AI assistance.
MIT — see LICENSE.
