This project is in active development. Features may change between releases. Please report issues on GitHub.
Audio file integrity checker that performs a full decode test using ffmpeg. Catches audio stream corruption like Decoding error: Invalid data and Decode error rate exceeds maximum.
Default mode is report-only. No files are moved or deleted until you explicitly choose to.
FLAC, MP3, M4A, OGG, Opus, WAV, WMA, AAC, AIFF, APE, WavPack, ALAC, M4B, M4P, MP2, MPC, DSF, DFF
Each audio file is decoded end-to-end with ffmpeg -v error -xerror -nostdin -i <file> -map 0:a -f null -. The -xerror flag makes ffmpeg exit immediately on any decode error and -map 0:a ensures only audio streams are tested. Files that fail are flagged as corrupt. A clean corrupt.txt list is written for scripting or interactive deletion.
| Mode | What it does | Music mount | Safe? |
|---|---|---|---|
setup |
Container starts idle, no scanning (default) | ro |
Yes |
report |
Scan and log only, writes corrupt.txt |
ro |
Yes |
delete |
Interactive — prompts per album folder to delete | rw |
You choose |
move |
Auto-move corrupt files to quarantine folder | rw |
Destructive |
You can change modes without restarting — see Rescan below.
Start the container normally. It scans your library, writes results, and waits for next interval:
BeatsCheck v1.0.0 starting
2026-01-15 10:00:00 | INFO | BeatsCheck v1.0.0
2026-01-15 10:00:00 | INFO | Mode: report
2026-01-15 10:00:00 | INFO | Workers: 6
2026-01-15 10:00:00 | INFO | Library: 98432 files (2.8 TB)
2026-01-15 10:00:00 | INFO | To scan: 98432 files (0 already processed)
2026-01-15 10:05:00 | INFO | [5%] 5000/98432 checked, 2 corrupt, ETA 3h42m
Check the logs on the host:
# Clean list of corrupt file paths
cat /path/to/config/corrupt.txt
# Full log with error details
cat /path/to/config/beats_check.logImportant: The music mount must be rw (not ro) for delete mode to work.
From the container console or via docker exec:
# Inside container console
delete
# Or from the host
docker exec -it beatscheck deleteYou'll see a menu:
Found 5 corrupt files across 3 folders (142.5 MB)
[a] delete ALL corrupt files now
[i] interactive (decide per folder)
[q] quit
Choice: i
[1/3] /data/Artist Name/Album Name/
track01.flac (45.2 MB)
-> invalid residual | decode_frame() failed | Decoding error: Invalid data found
track05.flac (512 B)
-> File too small (512 bytes)
(2 corrupt / 12 total files in folder)
Action? [y/f/n/a/q]
Interactive options:
y— delete entire folder (nuke the album, re-download later)f— delete just the corrupt files, keep the restn— skip this foldera— delete all remaining folders without askingq— quit
After deletion, corrupt.txt is updated to remove handled entries.
Trigger a rescan or change modes without restarting the container:
# Rescan with current mode
rescan
# Change mode and scan (works from setup mode too)
rescan report
rescan move
# Full rescan (clear resume cache)
rescan --fresh report
# From the host
docker exec beatscheck rescan report- Setup mode by default — container starts idle, nothing happens until you choose a mode
- Read-only music mount — kernel-enforced via Docker
:roflag (change torwonly fordelete/movemodes) -xerrorflag — fail-fast on decode errors, no false positives from partial decodes- Symlink boundary check — won't traverse symlinks that point outside the music directory
- Graceful shutdown — responds to SIGTERM/SIGINT, finishes in-progress files then exits cleanly
- Resume support — tracks already-checked files in
processed.txtacross runs (essential for multi-hour scans) - CPU throttled —
nice(10)+ configurable--cpusto avoid impacting other services - 10 min per-file timeout — prevents hangs on severely corrupt files
- Docker HEALTHCHECK — verifies process is running and heartbeat is fresh
- Hardlink aware — logs link count when corrupt files have multiple hard links
- Atomic JSON writes — crash-safe output files (no corruption on power loss)
- Auto-delete safety threshold — aborts if too many files flagged (prevents catastrophic deletion)
On first run, BeatsCheck creates /config/beatscheck.conf with all options and their defaults. Edit this file to configure scanning, scheduling, and Lidarr integration. Credentials stored here stay out of docker inspect and process listings.
| Setting | Default | Description |
|---|---|---|
output_dir |
/corrupted |
Quarantine destination for move mode. Must match a mounted volume |
mode |
setup |
setup (idle), report, delete, or move. Can be changed at runtime via rescan |
workers |
4 |
Parallel ffmpeg decode workers. 2 = conservative, 4 = balanced, 8+ = fast |
run_interval |
0 |
Hours between scans. 0 = run once and exit. 168 = weekly. 24 = daily |
delete_after |
0 |
Days before corrupt files are auto-deleted. 0 = never (manual only). 7 = 7 day review window |
max_auto_delete |
50 |
Safety threshold — abort auto-delete if more than this many files would be removed. 0 = no limit |
min_file_age |
30 |
Skip files modified within this many minutes. Prevents flagging active downloads |
log_level |
INFO |
Logging verbosity: DEBUG, INFO, WARNING, ERROR |
max_log_mb |
50 |
Rotate log and do fresh full scan when log exceeds this size. 0 = never rotate |
lidarr_url |
(empty) | Lidarr instance URL (e.g. http://lidarr:8686). Enables Lidarr API integration |
lidarr_api_key |
(empty) | Lidarr API key (Settings → General in Lidarr). Also reads from /run/secrets/lidarr_api_key |
lidarr_search |
false |
Queue search for unmonitored albums after auto-delete. Monitored albums are auto-searched by Lidarr. 5 albums/hour during idle |
lidarr_blocklist |
false |
Blocklist the release in Lidarr before deleting, preventing re-download of the same corrupt copy |
webui |
false |
Enable the built-in web interface. Requires a published port |
webui_port |
8484 |
Port for the web interface |
Environment variables (uppercase, e.g. MODE, WORKERS) override the config file if set.
These Docker-level settings are configured as environment variables:
| Env Var | Default | Description |
|---|---|---|
PUID |
99 |
User ID for file ownership |
PGID |
100 |
Group ID for file ownership |
TZ |
UTC |
Timezone for log timestamps. Auto-detected if /etc/localtime is bind-mounted |
UMASK |
002 |
File creation mask |
Volume paths (MUSIC_DIR, OUTPUT_DIR, CONFIG_DIR) default to /data, /corrupted, /config and are set via Docker volume mounts.
BeatsCheck includes an optional web interface for monitoring and control. Disabled by default.
-
Edit
/config/beatscheck.conf:webui = true webui_port = 8484 -
Publish the port in your Docker setup:
# docker-compose.yml ports: - "8484:8484"
-
Access at
http://your-server:8484 -
On first visit, create your login credentials via the setup wizard
- Authentication — first-run setup wizard, session-based login with PBKDF2-hashed passwords
- Dashboard — live scan status, progress bar with ETA, library stats
- Corrupt Files — sortable/searchable table with individual and bulk delete
- Configuration — edit all settings from the browser (config key allowlist enforced)
- Logs — real-time log viewer with syntax highlighting, level filter, search, copy/download
- Dark/Light mode — toggle with one click, preference saved
- Mobile responsive — full functionality on phones and tablets
- Accessible — keyboard navigation, ARIA labels, skip-to-content, reduced motion support
If you forget your WebUI password:
docker exec beatscheck reset-webui-passwordThis removes the credential file. The next visit to the WebUI will show the setup wizard to create new credentials.
- Authentication required — all API endpoints require a valid session (PBKDF2-SHA256 hashed passwords, HttpOnly session cookies)
- Setup wizard — credentials created on first access, stored hashed in
/config/webui_auth.json - Config allowlist — only known configuration keys are accepted (arbitrary key injection blocked)
- Thread-safe config writes — concurrent requests cannot corrupt the config file
- Delete validation — files must be in
corrupt.txtand inside the music directory; symlinks rejected - Path traversal protection — static file serving validates all paths against the static directory
- API key masking — Lidarr API key shown as
********in the UI - No external dependencies — built on Python stdlib only (no supply chain risk)
Important: The WebUI is designed for trusted LAN / Docker bridge networks. For remote access, use a reverse proxy with HTTPS and authentication (Nginx, Caddy, Traefik). Do not expose the WebUI port directly to the internet.
A complete docker-compose.yml is included in the repository:
# Copy and edit the compose file
cp docker-compose.yml /path/to/your/docker-compose.yml
# Edit paths and settings, then:
docker compose up -d# Scan (report mode, run once)
docker run --rm \
-v /path/to/music:/data:ro \
-v /path/to/config:/config \
-e MODE=report \
-e WORKERS=6 \
--cpus=2 \
ghcr.io/chodeus/beatscheck:latest
# Daemon mode (weekly scans, stays running)
docker run -d --restart unless-stopped \
--name beatscheck \
-v /path/to/music:/data:ro \
-v /path/to/config:/config \
-e MODE=report \
-e RUN_INTERVAL=168 \
-e WORKERS=6 \
ghcr.io/chodeus/beatscheck:latest
# Interactive delete (requires rw music mount)
docker exec -it beatscheck delete
# Trigger a rescan
docker exec beatscheck rescandocker pull ghcr.io/chodeus/beatscheck:latestwget -O /boot/config/plugins/dockerMan/templates-user/my-BeatsCheck.xml \
https://raw.githubusercontent.com/chodeus/BeatsCheck/main/beats-check.xml- Go to Docker tab in Unraid web UI
- Click Add Container
- Select BeatsCheck from the template dropdown
- Verify paths match your setup:
- Music Library:
/mnt/user/data/media/music(or your music directory) - Config:
/mnt/user/appdata/beatscheck
- Music Library:
- Configure your settings (paths, workers, Lidarr if needed)
- Click Apply — the container starts idle in setup mode
- Open the container console and type
rescan reportto start scanning
The container defaults to setup mode — it starts idle and waits for you to trigger a scan. This lets you configure everything before any scanning begins.
To start scanning: run rescan report from the container console or set MODE=report and restart.
With RUN_INTERVAL set (e.g., 168 for weekly):
After the first scan, the container sleeps for the interval, then scans again. Only new/changed files are checked on subsequent runs (resume support).
With RUN_INTERVAL=0 (default):
The container scans once and stays idle. Use rescan to trigger another scan without restarting.
Three options — pick what suits your workflow:
Option 1: Auto-delete after X days (fully automated)
Set DELETE_AFTER=7 in the container config. Corrupt files are automatically deleted 7 days after being first detected. This gives you time to review corrupt.txt before anything is removed.
Option 2: Interactive delete (on demand)
Change the music mount to rw, then from the Unraid terminal:
docker exec -it BeatsCheck deleteOption 3: Manual delete from corrupt.txt
cat /mnt/user/appdata/beatscheck/corrupt.txt
while IFS= read -r f; do rm -v "$f"; done < /mnt/user/appdata/beatscheck/corrupt.txtUse a User Scripts wrapper to get notified after scans:
#!/bin/bash
LOG_DIR="/mnt/user/appdata/beatscheck"
if [ -f "$LOG_DIR/summary.json" ]; then
CHECKED=$(jq -r '.files_checked' "$LOG_DIR/summary.json")
CORRUPT=$(jq -r '.corrupted' "$LOG_DIR/summary.json")
DURATION=$(jq -r '.duration' "$LOG_DIR/summary.json")
SIZE=$(jq -r '.library_size_human' "$LOG_DIR/summary.json")
if [ "$CORRUPT" -gt 0 ]; then
/usr/local/emhttp/webGui/scripts/notify \
-i warning -s "BeatsCheck" \
-d "Scan complete: $CHECKED files ($SIZE), $CORRUPT corrupt found ($DURATION)"
else
/usr/local/emhttp/webGui/scripts/notify \
-i normal -s "BeatsCheck" \
-d "Scan complete: $CHECKED files ($SIZE), no corruption ($DURATION)"
fi
fiRequires Python 3.9+ and ffmpeg installed.
MODE=report WORKERS=6 python3 beats_check.py /path/to/music /path/to/quarantine /path/to/config/beats_check.logThe third argument is the log file path. All state files (processed.txt, corrupt.txt, etc.) are written to the same directory as the log file. Unix-only (requires fcntl).
| File | Contents |
|---|---|
beats_check.log |
Full scan log — errors, moves, deletes, and scan summaries |
beats_check.log.1 .2 .3 |
Previous logs after rotation (last 3 kept) |
processed.txt |
Resume cache — one checked file path per line. Rotated alongside the log |
corrupt.txt |
One corrupt file path per line (deduplicated) — for scripting or delete mode |
corrupt_details.json |
Path-to-error mapping with Lidarr trackfile/album IDs (when configured) |
corrupt_tracking.json |
Path-to-first-seen timestamps — used by DELETE_AFTER auto-delete |
summary.json |
Machine-readable scan results for notification scripts |
search_queue.json |
Pending Lidarr album search queue — drained during idle (5/hour) |
webui_auth.json |
WebUI login credentials (username + PBKDF2-hashed password) |
.scanning |
Lock file (exists only during active scans, uses flock) |
.heartbeat |
Timestamp updated during scans and idle — used by Docker healthcheck |
When the log exceeds 50 MB (MAX_LOG_MB=50), it's rotated to beats_check.log.1 (keeping up to 3 old copies). The resume cache (processed.txt) is rotated alongside it, so the next scan does a fresh full re-check. Set MAX_LOG_MB=0 to disable rotation.
For a 100K file library, expect ~10 MB per full scan. With daemon mode, subsequent scans only log corrupt files so growth is slow.
| Resource | Impact |
|---|---|
| CPU | 10-20% total with 6 workers (audio decode is light) |
| RAM | Under 500MB |
| Disk I/O | The bottleneck — expect 6-15 hours depending on array speed |
| Other services | Unaffected (low priority + CPU cap) |
When LIDARR_URL and LIDARR_API_KEY are set, BeatsCheck uses the Lidarr API for all deletion operations. Track file records are removed via the API while albums stay monitored — Lidarr automatically re-downloads monitored albums after deletion.
How it works:
- Scan — corrupt files are detected and matched to Lidarr trackfile IDs via path suffix matching. IDs are stored in
corrupt_details.jsonso delete operations don't need to re-resolve them. - Blocklist (
LIDARR_BLOCKLIST=true) — before deletion, the most recent grab for each affected album is marked as failed. Lidarr auto-creates a blocklist entry so the same corrupt release is not re-downloaded. If blocklist fails, deletion is aborted. - Delete — track files are removed via Lidarr's bulk delete API. Albums stay monitored and show as "missing". If the API call fails, deletion is aborted (no silent fallback to filesystem delete).
- Re-download — monitored albums are automatically re-searched by Lidarr after the trackfiles are deleted. No manual intervention needed.
Unmonitored albums:
If a deleted album is unmonitored in Lidarr, it won't be auto-searched. In interactive delete, you'll be prompted:
2 deleted albums are unmonitored in Lidarr. Queue search? [y/n]
With LIDARR_SEARCH=true and auto-delete, unmonitored album IDs are written to a persistent search queue (search_queue.json). The container drains this queue during idle — one album at a time, rate limited to 5/hour to avoid flooding indexers.
Path handling:
BeatsCheck runs in a container where music is mounted at /data, while Lidarr sees the same files at the same or different mount path. When both containers mount the same host path to /data, the paths match exactly. At scan time, corrupt files are matched to Lidarr trackfiles by comparing path components from the right (suffix matching). This works regardless of how the music directory is mounted in each container.
Error handling:
All Lidarr API operations are fail-safe:
- If blocklist fails → delete is aborted, error shown
- If bulk delete API fails → delete is aborted, error shown
- If Lidarr is unreachable at scan time → files are tracked without Lidarr IDs, direct filesystem delete is used as fallback
- Files not tracked by Lidarr (cover art, .nfo, etc.) are deleted directly via
os.remove
Security:
- API key is sent only via HTTP header, never in URLs or logs
- Lidarr URL is masked in all log output
- Config file support — store the API key in
/config/beatscheck.confto keep it out ofdocker inspectand process listings - Also supports Docker secrets (
/run/secrets/lidarr_api_key) - HTTP redirects are blocked to prevent credential leaking
- All API calls have explicit timeouts
## /config/beatscheck.conf — API key stays off the command line
lidarr_url = "http://lidarr:8686"
lidarr_api_key = "your-api-key-here"
lidarr_search = true
lidarr_blocklist = true
docker pull ghcr.io/chodeus/beatscheck:latestThen restart the container.
See SECURITY.md for the security policy, vulnerability reporting, and container security measures.