Scans a media library directory, inspects every video file with ffprobe, and reports codec and subtitle issues that may trigger transcoding when played on a Samsung NU6900 TV (and likely other Samsung models or TV brands) via Jellyfin/DLNA. Outputs a console summary plus CSV and/or HTML reports.
./scan_transcode.sh /path/to/media [options]| Option | Description |
|---|---|
--format FMT |
Output format: csv, html, both (default: both) |
--output FILE |
Basename stem (e.g. --output report → report.csv + report.html) |
--file-list FILE |
Incremental mode: scan only listed paths and merge them into the existing cache/report |
--no-check-subtitles |
Skip subtitle stream compatibility checks for a codec-only scan. Subtitle checks are on by default. |
--jobs N |
Parallel ffprobe workers (default: CPU count) |
--exts "e1,e2,..." |
Override default extension list |
--verbose |
Print each file as it is processed |
--help |
Show help text |
# Full scan — console + CSV + HTML
./scan_transcode.sh /mnt/media
# HTML report only, custom filename stem
./scan_transcode.sh /mnt/media --format html --output ~/media_report
# CSV only
./scan_transcode.sh /mnt/media --format csv
# Incrementally rescan changed files listed in changed-files.txt
./scan_transcode.sh /mnt/media --format both --output ~/media_report --file-list changed-files.txtPositional argument is the scan directory. All --options are parsed with a while loop. --output strips .csv/.html extensions if provided, using the clean stem for both output files.
The directory is validated to exist, and ffprobe is checked to be on PATH.
Uses find with -follow (follow symlinks) to recursively locate all files matching the supported extensions (default: mkv, mp4, avi, mov, ts, m2ts, vob, flv, webm, wmv, rmvb). The extension list is user-overridable via --exts. A file count is printed before processing begins so you can confirm discovery worked.
find pipes null-terminated file paths to xargs -0 -P, which spawns up to N parallel workers. Each worker runs the process_file function on one file. This is the key performance feature — a large library is scanned in minutes rather than hours.
Results are cached in a .cache file alongside the CSV report. Each entry stores the file's modification time (mtime) alongside its scan verdict. On subsequent runs:
- Files whose mtime hasn't changed are pulled from the cache and skipped by workers
- Files that previously errored (unreadable) are also cached so they aren't retried
- When all files are up to date and the requested reports already exist, cached results are reused for the summary and the report files are left unchanged
--file-listskips the full directory walk, rescans only the listed media paths, removes missing listed paths from the cache, and regenerates reports from the merged cache- The cache includes a config header; if subtitle checking is toggled with
--no-check-subtitles, the cache is invalidated and a full re-scan runs - Delete the
.cachefile to force a full re-scan
For each file, ffprobe is called once with JSON output containing format info and all stream metadata. jq extracts and iterates over every stream:
- Container: compared against the supported container list
- Attached pictures: streams where
disposition.attached_pic == 1(embedded cover art, thumbnails) are excluded from the codec check sopng/jpegalbum art doesn't trigger false positives - Video streams: codec name checked against H.264, HEVC, MPEG-2, MPEG-4, VP8, VP9, MJPEG
- Audio streams: codec name and tag string checked against the transcode blocklist: DTS, DTS-HD MA, DTS:X, Dolby TrueHD. Unlike container/video, all audio streams must be unsupported for the file to be flagged — if at least one compatible stream exists (e.g. AC3 alongside DTS), the file is not given an audio codec issue
- Subtitle streams: codec name checked against SRT,
mov_text, ASS, SSA, SMI, SUB, MicroDVD, TTXT, XML — anything else (PGS, VobSub) is flagged unless--no-check-subtitlesis used
- No Issues — no codec or subtitle issues found across all checked streams
- Codec Issues — container/video is unsupported, or all audio streams are in the transcode blocklist. Each specific reason is recorded (e.g., "All audio tracks may require transcoding: dts")
- Subtitle Issues — subtitle format may require burn-in/transcoding on Samsung/Jellyfin
- Codec + Subtitle Issues — both issue types are present. In HTML, this file appears when either the Codec Issues or Subtitle Issues filter is enabled.
Console (always unless --format html): summary-only status output with total files, no issues, codec issues, subtitle issues, both issue types, unreadable files, and changes since the previous run. File-level details are intentionally kept in the HTML and CSV reports.
CSV (if format includes csv): machine-readable with columns path,container,video_codec,audio_codec,subtitle_codec,verdict,issue_type,reason. If files were skipped, a # Skipped files: N comment block is appended at the end.
HTML (if format includes html): self-contained single file with:
- Summary banner with total / no issues / codec issues / subtitle issues / both counts
- Collapsible Skipped Files section (orange) listing unreadable files
- Color-coded table rows (green = No Issues, amber = Subtitle Issues, red = Codec Issues, split red/amber = both)
- Three independent JS-powered checkboxes to show/hide No Issues, Codec Issues, and Subtitle Issues. Codec/subtitle filters overlap, so a file with both issue types remains visible if either issue filter is checked.
- Reasons displayed as italic text below each file row
- No external dependencies — CSS and JS are inline
A case statement on $FORMAT calls the appropriate generation functions:
generate_csv— writes sorted CSV with headergenerate_html— writes single-file HTML with inline styles and JSgenerate_console— writes summary and last-run status to stdout
ffprobe(from FFmpeg)jq- Standard POSIX tools:
find,xargs,grep,sort,realpath,sed
The watch_media/ directory contains a prototype local-pull workflow for server-side file watching. It is intended as an accelerator for known changes, not a replacement for periodic full scans. See watch_media/README.md.
- All temp files (RESULTS, SKIPPED, CACHE_HITS, FILE_LIST, DIFF_RESULT) are cleaned up on exit via
trap - The
process_filefunction isexport -fsoxargscan invoke it in child shells jqis used overgrep/awkfor reliable JSON parsing with proper escaping- Results are written to a temp file, sorted, and then emitted — no interleaving from parallel workers
--verboseprints each file's relative path asxargsdispatches it, useful for debugging slow or skipped scans- Unreadable files (corrupt, permission-denied, etc.) are tracked in a separate temp file and reported in a Skipped Files section rather than aborting the entire scan. Each entry includes the
ffprobeerror message (e.g.,Invalid data found when processing input) to help diagnose the issue. --outputintelligently strips.csv/.htmlextensions so old habits don't cause doubled extensions
Planned with help by Geoffrey McClinsey, built by OpenCode