🍪 ISOHungry ME EAT DVD. ME MAKE ISO. OM NOM NOM. Feed it discs, it eats them. Watches every optical drive it can see, works out what each disc is — film, album or data — rips it, burps, ejects, and waits for the next one. Several drives at once. Runs in a container, so the only thing you install is Docker. There's a web UI too, on :8080 — the same information plus a log viewer, a settings panel, and the ability to…
Hungry little disc ripper in a container. Feed it DVDs, audio CDs or data discs and it figures out what each one is: films to ISO, albums to tagged MP3/FLAC, data to images. Multi-drive, never re-rips the same disc, web UI. Om nom nom.
Feed it discs, it eats them. Watches every optical drive it can see, works out
what each disc is — film, album or data — rips it, burps, ejects, and waits
for the next one. Several drives at once. Runs in a container, so the only
thing you install is Docker.
🍪 ISOHungry — me eat DVD! (updated 14:22:07)
========================================================
sr0 | 🍪 NOM NOM NOM! Eating BLADE_RUN | 00:12:41
sr1 | 🎵 NOM NOM! Slurping CD | 00:04:52
sr2 | 🤤 BUUURP! Me ate THE_MATRIX | 00:44:52
sr3 | 🍪 Me hungry... feed me disc! | --:--:--
There’s a web UI too, on :8080 — the same information plus a log viewer, a
settings panel, and the ability to name a disc while it’s still ripping or
identify an album the lookup missed.
Quick start
Linux — the easy case, drives just work:
git clone <this-repo> && cd ISOHungry
./setup.sh
docker compose up -d
docker compose logs -f
git clone <this-repo>; cd ISOHungry
powershell -ExecutionPolicy Bypass -File scripts\setup-windows.ps1 # once, ~20 min
.\scripts\attach-drives.ps1 # after each reboot
docker compose up -d
macOS — not possible. Docker Desktop on macOS has no USB or optical
passthrough and there’s no usbipd equivalent. ./setup.sh explains your
options: run the script natively via Homebrew, or point a Linux box at it.
Then open http://localhost:8080, or watch the terminal display with
docker compose logs -f. Output is sorted by what the disc turned out to be:
Docker Desktop runs containers inside the WSL2 VM, and Microsoft’s stock WSL2
kernel is built without optical-drive support:
# CONFIG_BLK_DEV_SR is not set <- no /dev/sr* can ever appear
# CONFIG_USB_STORAGE is not set <- USB mass storage not recognised
No container flag works around a missing driver — not --privileged, not
--device, not bind-mounting /dev. setup-windows.ps1 builds a WSL2 kernel
from Microsoft’s own source with exactly three symbols added
(BLK_DEV_SR, USB_STORAGE, USB_UAS), points .wslconfig at it, and
installs usbipd-win. Everything else the ripper needs — SCSI core, sg,
ISO9660, UDF, the usbip virtual hub — is already enabled upstream.
USB drives only on Windows. Internal SATA optical drives cannot be
forwarded into WSL2; there is no SATA passthrough. Linux has no such limit.
What it eats
Discs are identified automatically and sorted into subdirectories.
Sector-for-sector image, bounded by the volume size
Audio is checked first, so an enhanced CD (audio tracks plus a data session)
is ripped as music rather than as a data image. Music lands as
music/Artist/Album/01 - Track.mp3. Choose FLAC instead of MP3, or turn data
discs off, from the web UI’s Settings panel — or with AUDIO_FORMAT and
RIP_DATA_DISCS if you’d rather set them in docker-compose.yml.
While an audio CD rips, the display shows the album as soon as the lookup
resolves and then follows the track count — Track 3/12: Artist / Album.
Tags are written by ISOHungry, not by abcde. abcde’s own tagging step
reports success while leaving the files untagged: it sends the tagger’s output
to /dev/null and only checks the exit status, and the tagger exits 0 having
done nothing. So the files are tagged afterwards from the CDDB data abcde
fetched, which also carries properly spaced titles where the filenames have
had spaces munged to underscores. Various-artists discs get the right
per-track artist.
If a disc won’t give up every track, the readable ones are still kept and
UNREADABLE_TRACKS.txt in the album folder records what’s missing.
How things get named
Nothing ever silently overwrites or gets skipped as a duplicate.
Disc label
Result
Distinctive (THE_MATRIX)
THE_MATRIX.iso
Generic (DVD_VIDEO, WB_DVD, UNTITLED…)
DVD_VIDEO_2026-07-27_150000.iso
No label at all
unknown_sr0_2026-07-27_150000.iso
Distinctive, already seen
THE_MATRIX_disc2.iso, _disc3, …
That last row is the box-set case. Multi-disc sets routinely stamp every disc
with the same volume label, so a repeated distinctive label is treated as
another disc in the set and numbered, keeping the set together on disk. A
repeated generic label is treated as an unrelated film and timestamped
instead — half of Warner’s catalogue is stamped WB_DVD, and those discs have
nothing to do with each other. GENERIC_LABELS controls that list.
Duplicate detection doesn’t use the label. Each disc is fingerprinted —
ISO9660 metadata for discs, the TOC for audio CDs — so two different films both
labelled DVD_VIDEO are seen as different discs, while the same disc
reinserted next week is recognised and skipped rather than becoming _disc2.
Markers live in out/.ripped/<fp>; delete one to make ISOHungry hungry for
that disc again.
Web UI
http://localhost:8080 — a status page that mirrors the terminal display:
every drive, what it’s eating, progress bars, finished items with download
links, and a log viewer. The terminal display stays exactly as it was; the web
UI is additive and reads the same state files.
Progress is measured in whatever unit is honest for the disc: bytes for films
and data images, tracks for audio CDs. Encoded audio is a small fraction of
the raw disc, so measuring bytes against CD size would show a finished album
sitting at about 10%.
Three things it can change, and nothing else. Name a rip while it’s running
— applied when the ISO is finalised, so you can type the real film title while
the disc spins. Rename a finished item, keeping its fingerprint marker in
step so the disc stays recognised. And identify an album the disc lookup
missed: anything that landed as Unknown Artist or Track 1..N is marked
not identified, and the Identify button searches MusicBrainz by name,
shows the candidates with a * beside those whose track count agrees, then
tags, renames and files the album under Artist/Album. There is no eject and
no delete.
It also carries a Settings panel for the options you actually change disc
to disc — music format, whether to rip data discs, how hard to fight a
scratched CD, how many discs at once, and when to give up. Changes are written
to out/settings.conf and picked up before the next scan, so they apply to the
next disc without a restart; a rip already running is unaffected.
Those settings override the environment, so docker-compose.yml still sets
the defaults. Values are whitelisted on both sides — the file is writable from
the web UI and one of its values reaches a command line, so the ripper
validates every key it reads rather than sourcing the file.
WEB_UI=0 disables it entirely.
Reaching it from other machines
It binds to 127.0.0.1 by default, so only the host can see it. To open it to
your LAN, create a .env file next to docker-compose.yml:
WEB_BIND=0.0.0.0
then docker compose up -d --force-recreate. It’s now at
http://<host-lan-ip>:8080 from any device on the network — phone included,
which is the point.
Note -Profile Private. If your network is classified Public, either change
the network to Private or add Public here — but understand what that means
before you do it on a network you don’t control.
Know what you’re exposing. There is no authentication and no TLS. Anyone
who can reach the port can browse your library, download whole ISOs, and
rename things. That’s fine on a home LAN and a bad idea anywhere else.
Better: a private network instead of a firewall hole
If you run Tailscale (or another WAN), bind to its interface rather than
opening a LAN port. You get device-authenticated access from anywhere, no
firewall rule, and nothing exposed to other machines on the local network:
# .env — substitute your own tailnet address
WEB_BIND=100.x.y.z
Then reach it at http://100.x.y.z:8080 from any device on your tailnet.
tailscale ip -4 prints the address.
Or tunnel over SSH and skip network exposure altogether:
ssh -L 8080:127.0.0.1:8080 you@host # then use http://localhost:8080
What it says
Status
Meaning
🍪 Me hungry... feed me disc!
Drive empty, waiting
🍪 NOM NOM NOM! Eating X
Ripping a video DVD
🎵 NOM NOM! Slurping X
Ripping an audio CD, before it’s identified
🎵 NOM NOM! Artist / Album
The disc has been identified
🎵 Track 3/12: Artist / Album
Ripping that track
🎵 No match — slurping untagged
No metadata found; ripping anyway
🍪 NOM NOM! Eating data X
Imaging a data disc
😋 Om nom nom... chewing into ISO
Building the ISO
🤤 BUUURP! Me ate X
Done
💨 BURP! Spit out X
Ejected, ready for the next
🤕 Ate 13/14 — track 1 no good
Some tracks were unreadable; the rest were kept
🙃 Me already ate dis! (X)
Seen before, ejected untouched
😋 Me full! Waiting (2/2 eating)
At MAX_PARALLEL, queued
😢 Tummy full! Need NNNMB
Not enough disk space; waits
🙅 Me no eat data discs
A data disc, and RIP_DATA_DISCS=0
🤮 Me no like dis disc!
Video/data rip failed — check out/logs/
🤮 Me no like dis CD!
Audio rip failed — check out/logs/
🤮 Me choke on dis!
ISO build failed — check out/logs/
😝 Dis not movie!
Claimed to have VIDEO_TS, but the extract had none
😝 No music came out!
Audio CD produced no encoded tracks
😤 Disc stuck in me teeth!
Eject failed, pull it out yourself
😵 Drive no talk to me!
Drive stopped responding
Configuration
Set these in docker-compose.yml under environment:, except WEB_BIND,
which belongs in a .env file beside it because it’s machine-specific.
The five marked UI can also be changed from the web UI’s Settings panel,
which writes out/settings.conf and overrides the environment without a
restart.
Variable
Default
Meaning
BASE_OUTPUT_DIR
/output
Where output is written
POLL_INTERVAL
5
Seconds between drive scans
MAX_PARALLEL(UI)
2
Concurrent rips — USB drives on one controller thrash above this
SPACE_FACTOR
22
Free space required, in tenths of disc size (2.2×)
PROBE_TIMEOUT
30
Seconds before a wedged drive is given up on
GENERIC_LABELS
see script
Labels too common to trust as filenames
AUDIO_FORMAT(UI)
mp3
mp3 (LAME -V0, ~245 kbps VBR) or flac
AUDIO_RIP_TIMEOUT(UI)
5400
Seconds before an audio rip gives up; 0 disables
AUDIO_CDPARANOIA_OPTS(UI)
—
Extra cdparanoia flags; -Y or -Z for damaged discs
RIP_DATA_DISCS(UI)
1
0 to ignore non-video, non-audio discs
WEB_UI / WEB_PORT
1 / 8080
Web status page
WEB_BIND(.env)
127.0.0.1
Interface the UI listens on
DEVICE_GLOB
/dev/sr*
Which devices to watch
TZ
—
Timezone for the clock
Disk space. Video DVDs are the hungry case: dvdbackup extracts the disc
and then genisoimage builds the ISO from the extract, so roughly 2× disc
size is needed while working — the default SPACE_FACTOR=22 reserves 2.2×.
Data discs need about 1×, and audio CDs need far less once encoded. If there
isn’t room, the drive waits rather than failing partway through.
Attaching drives on Windows
.\scripts\attach-drives.ps1 finds every USB optical drive, binds it (one UAC
prompt) and attaches it into the VM. Re-run after every reboot or
wsl --shutdown — binding persists, attaching doesn’t. -Detach gives the
drives back to Windows.
Drives are matched by USB instance ID, not vendor/product ID, so two
identical drives are never mixed up.
Why not just usbipd attach --wsl?
It fails here. usbipd insists on modprobe vhci_hcd, which can’t succeed when
the driver is built into the kernel rather than a module — and our kernel
builds it in. The script drives the usbip client directly instead.
It must run with --network host. The TCP socket backing the virtual USB port
lives in the caller’s network namespace; in a normal container namespace that
socket dies with the container, leaving the device present but wedged in
unkillable I/O that survives timeout and needs wsl --shutdown to clear.
Troubleshooting
No drives listed — on Windows, did you run attach-drives.ps1? Check with
docker run --rm --privileged -v /dev:/dev alpine ls /dev/sr0.
🤮 Me no like dis disc! / Me no like dis CD! — usually a scratched or
unusually protected disc. The real error is in out/logs/<device>.log.
😝 Dis not movie! — the disc’s index advertised a VIDEO_TS directory,
but the extraction produced none. That means a failed or partial dvdbackup
run, not a data disc: discs without VIDEO_TS are detected up front and
imaged into out/data/ instead. Check the log.
😝 No music came out! — abcde finished but wrote no tracks. Usually a
MusicBrainz lookup that failed alongside a read error; the log has both.
Music has no artist or album name — lookup at rip time is by disc TOC, so
a disc that isn’t in the database becomes Unknown Artist / Unknown Album with
Track 1..N. You don’t need the disc back to fix it: search MusicBrainz by
name instead and match on track count.
Easiest from the web UI — such albums are badged not identified and have
an Identify button. From the command line:
It lists candidate releases, marking those whose track count agrees, tags the
files, renames them to the real titles, and moves the album to
Artist/Album. Use --query if the folder name isn’t a good search term,
--pick N to choose a different match, and --no-move to tag in place.
🤕 Ate 13/14 — track 1 no good — the disc wouldn’t give up every track.
What was readable is kept, and UNREADABLE_TRACKS.txt in the album folder
lists what’s missing. The read errors are in the log. Usually a scratch or a
failing drive: try the disc in another drive, or push past the damage with
AUDIO_CDPARANOIA_OPTS=-Z (faster, less accurate). To retry, delete the
disc’s marker in out/.ripped/ and reinsert it.
A rip grinds for hours on one track — cdparanoia retries a bad sector
hard, and on a failing drive each retry costs seconds. AUDIO_RIP_TIMEOUT
(default 90 min) caps it and keeps whatever was read.
Music files have no embedded tags — for albums ripped before this was
fixed, retag them from their filenames:
It skips files that already have tags unless you pass --force.
Rename says “file is in use” — something has the file open. On Windows the
usual culprit is the ISO being mounted as a virtual drive: double-clicking an
ISO in Explorer mounts it, and Windows then holds an exclusive lock, so neither
the container nor Windows itself can rename it. Eject the virtual drive and try
again. To check:
😵 Drive no talk to me! — the drive stopped responding. On Windows this
usually means the usbip attachment dropped; re-run attach-drives.ps1.
A drive disappears from the display — a badly damaged disc can knock a USB
drive off the bus entirely; usbipd list still shows it Attached but the
device behind it is gone. Detach and re-attach it:
Rips are slow — USB optical drives read at 2–10 MB/s, so a full disc takes
20–60 minutes. That’s the drive, not the software.
Kernel didn’t take — uname -r inside a container should end in +.
Docker Desktop pins the VM alive, so wsl --shutdown without quitting Docker
Desktop first often leaves the old kernel running.
Reverting (Windows)
Delete the kernel= line from %USERPROFILE%\.wslconfig and run
wsl --shutdown. Optionally winget uninstall dorssel.usbipd-win and delete
%USERPROFILE%\wsl-kernel.
Layout
Path
Purpose
isohungry.sh
The ripper
web/server.py, web/index.html
Web status page (stdlib only)
entrypoint.sh
Starts the web UI, then runs the terminal display
Dockerfile
Two stages: libdvdcss + gum, then a Debian runtime
docker-compose.yml
Privileged service, /dev and ./out mounts
.env(optional, untracked)
Machine-specific settings — currently just WEB_BIND
setup.sh
Linux/macOS setup
scripts/setup-windows.ps1
Windows one-time setup
scripts/attach-drives.ps1
Per-boot USB attach/detach
scripts/retag-music.sh
Retro-tags albums ripped before tagging worked
scripts/identify-album.py
Identifies an album MusicBrainz missed at rip time
kernel/build-wsl-kernel.sh
Reproducible WSL2 kernel build
Commercial DVDs are CSS-scrambled; libdvdcss is built from VideoLAN source in
a throwaway stage, so libdvdread finds it at rip time with nothing extra to
install. Audio CDs go through abcde, which wraps cdparanoia, MusicBrainz and
LAME/FLAC.
License
PolyForm Noncommercial License 1.0.0 — free for personal, hobby,
research and educational use. Commercial use or resale requires a separate
written licence from the author.
ISOHungry ships no third-party code itself. The container image installs
dvdbackup, cdrkit, libdvdcss, abcde, cdparanoia, LAME, FLAC and usbip at build
time, and each stays under its own terms (mostly GPL/LGPL); gum is MIT, and
the custom WSL2 kernel remains GPLv2.
Rip only discs you’re entitled to copy — rules vary by jurisdiction, and what
you do with this is on you.
评论