ZWScan: Turn a Folder of Videos Into a ZWMAP Playlist Automatically
Point it at a folder of videos. ZWScan groups them by subdirectory, auto-matches subtitles, chapters, thumbnails, and alternate audio tracks, and writes a single ZWMAP playlist JSON. Serve the folder with ZWServe and ZWPlayer plays the whole library — no manual list-building.
Download (pre-built, no install):
Free · Single self-contained binary · No runtime · No sign-up
The Problem: Building a Playlist by Hand Is Tedious
You already have a tidy folder of videos — a course series, a batch of lectures, a kid’s cartoon collection. The online Playlist Editor is great for crafting a polished list item by item, but when there are 50 or 500 files, adding each manually is a chore. And forgetting to wire up the matching .srt subtitle or .chapter.json for every video means the rich features (subtitles, chapters, thumbnails) silently go missing.
ZWScan does the tedious part for you: it walks a folder, groups the videos, matches every companion file by filename, and emits a complete ZWMAP/1.0 playlist in pass.
What ZWScan Does
Give it a root directory and a URL prefix. It will:
- Group: the deepest directory containing each video becomes its group (named after that folder). This works for nested layouts —
library/Course/Season1/ep01.mp4is grouped under “Season1”. Loose files directly under the root are grouped under the root directory’s own name, so they’re never lost. If two unrelated folders share a name (e.g. two courses each with a “Season1”), ZWScan disambiguates them asCourseA/Season1andCourseB/Season1rather than merging them. - Item: each video/audio file becomes playable item (mp4, mkv, mov, webm, m3u8, mpd, flv, mp3, m4a, aac, wav).
- Match companions: for every item, it looks in the same directory for files sharing the same name prefix and auto-attaches them — subtitles, chapters, annotations, thumbnails, watermarks, alternate audio tracks.
- Emit: writes a single ZWMAP/1.0
playlist.jsonwith every URL prefixed by your-prefix(typically your ZWServe address).
The output is a standards-compliant playlist that ZWPlayer loads directly.
Directory Convention
ZWScan reads the root directory you pass with -dir:
library/ ← -dir points here
├── intro.mp4 ← loose root file → group "library"
├── intro.srt ← subtitle auto-matched
├── 英语启蒙/ ← a group (group name = folder name)
│ ├── 01-ABC.mp4 ← an item
│ ├── 01-ABC.srt ← subtitle auto-matched to 01-ABC.mp4
│ ├── 01-ABC.chapter.json ← chapter auto-matched
│ ├── 02-Numbers.mp4
│ └── 02-Numbers.zh.srt ← subtitle with language tag
├── Course/ ← nested layout is supported
│ └── Season1/ ← group "Season1" (deepest dir wins)
│ ├── ep01.mp4 ← URL keeps full path: Course/Season1/ep01.mp4
│ └── ep02.mp4
├── Classic Cartoons/ ← another group
│ ├── ep01.mp4
│ ├── ep01_en.srt
│ └── ep01_ru.srt
└── playlist.json ← generated output (default location)
Grouping rule: the deepest directory containing a video is its group. Loose files directly under -dir are grouped under the root folder’s own name (here, “library”), so they are never dropped. Nested subdirectories (Course/Season1/) group by their last folder (“Season1”), and the playback URL preserves the full relative path. If two unrelated folders share a name, ZWScan disambiguates them with their parent (e.g. CourseA/Season1, CourseB/Season1).
Companion-File Matching
ZWScan matches companions by shared filename prefix in the same directory. Given an item 01.mp4 (base name 01), it looks for these patterns — separators ., _, or - are all accepted:
| Companion file pattern | Attaches to field | Example |
|---|---|---|
<base>.srt / .vtt / .ass |
subtitle (no language) |
01.srt |
<base>.<lang>.srt / .vtt |
subtitle with language |
01.zh.srt, 01_en.vtt |
<base>.chapter.json / .chapter.vtt |
chapter |
01.chapter.json |
<base>.annotation.json |
annotation |
01.annotation.json |
<base>.thumb.json / .thumbnail.json |
thumbnail |
01.thumb.json |
<base>.watermark.json |
watermark |
01.watermark.json |
<base>.<label>.m4a / .aac / .mp3 |
audioTracks |
01.dub.m4a |
<base>.poster.jpg / .png |
poster |
01.poster.jpg |
Subtitle language codes (zh, en, ja, …) are auto-expanded to human-readable labels (中文, English, 日本語, …) for the player’s subtitle menu. Anything that doesn’t match is simply left empty — no guessing.
Quick Start on Each Platform
Download the binary for your OS, then run it against your media folder.
Windows
.\zwscan-win.exe -dir D:\videos -prefix http://192.168.1.20:8000
macOS (Apple Silicon)
chmod +x zwscan-macos
./zwscan-macos -dir ./library -prefix http://192.168.1.20:8000
Linux
chmod +x zwscan-linux # or zwscan-linux-arm64 on ARM
./zwscan-linux -dir ./library -prefix http://192.168.1.20:8000
On startup ZWScan prints what it found:
ZWScan — from zwplayer.com (local media → ZWMAP playlist scanner)
-------------------------------------------------
scanning : /home/me/library
prefix : http://192.168.1.20:8000
[done] summary:
output : /home/me/library/playlist.json
groups : 4
items : 47
subtitles : 38
chapters : 12
annotations : 0
thumbnails : 0
watermarks : 0
audio tracks : 3
Command-Line Options
| Option | Default | Description |
|---|---|---|
-dir |
. |
Root directory to scan (each direct subdir = group). [required] |
-prefix |
— | Playback URL prefix, e.g. http://192.168.1.20:8000 (your ZWServe address). [required] |
-out |
<dir>/playlist.json |
Output playlist path. |
-title |
Local Library |
Playlist title. |
-h |
— | Show usage. |
Without -dir or -prefix, ZWScan prints help and exits without scanning.
The Full Workflow: ZWScan + ZWServe + ZWPlayer
ZWScan is the middle piece of a three-step workflow that turns a local folder into a playable network library:
# 1. Scan the folder → generate playlist.json (URLs prefixed with the server address)
zwscan -dir ./library -prefix http://192.168.1.20:8000 -out ./library/playlist.json
# 2. Serve the folder over HTTP (with CORS, so the player can reach it)
zwserv -dir ./library
# 3. Open ZWPlayer and load the playlist URL
# → http://192.168.1.20:8000/playlist.json
Once loaded in ZWPlayer, every group and item appears with its subtitles, chapters, and audio tracks already wired up — auto-play next, resume, and all the player’s features work on your local library.
Why the URL prefix?
Because ZWPlayer runs in a browser and loads videos over HTTP, the playlist needs absolute URLs that point at your ZWServe instance. ZWScan builds each item’s URL as <prefix>/<group>/<file>, so you pass the same address you’ll serve from.
FAQ
What happens to files that aren’t videos?
They’re used for companion matching. A .srt, .json, or .jpg is never itself an item — it’s either matched to a video by filename prefix, or ignored.
My subtitles are named movie_zh.srt, not movie.zh.srt. Does it match?
Yes. ZWScan accepts ., _, and - as the separator between the base name and the language/tag suffix (movie.zh.srt, movie_zh.srt, movie-zh.srt all match movie.mp4).
Can I edit the generated playlist afterwards?
Absolutely — playlist.json is plain ZWMAP/1.0 JSON. Open it in the online Playlist Editor to reorder, rename, tweak URLs, or add items ZWScan didn’t find.
Does it recurse into nested subdirectories?
Yes. ZWScan walks the entire tree under -dir. A video’s group is its immediate parent (deepest) directory, so library/Course/Season1/ep01.mp4 lands in a group named “Season1”. The playback URL preserves the full relative path. Loose files directly under the root are grouped under the root folder’s name.
Summary
ZWScan turns a folder of videos into a complete, subtitle-and-chapter-aware ZWMAP playlist in command. Paired with ZWServe to serve the folder and ZWPlayer to play it, you get a full local media library — grouped, annotated, and playable from any browser — without building a single playlist entry by hand.
- 🪟 Windows (.exe) · 🍎 macOS · 🐧 Linux x86 · 🐧 Linux ARM64
- ▶️ Open ZWPlayer to play the generated playlist
- ✏️ Playlist Editor to fine-tune the result
- 🖥️ ZWServe to serve the folder