Created
August 14, 2025 21:04
-
-
Save thetzel/601bd93a57a178a4a65c726d09905242 to your computer and use it in GitHub Desktop.
Gets the beginning of a mp3 file and looks for chapters until it finds them
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # fetch the smallest possible head of an MP3 to list chapters (HH:MM:SS) | |
| # deps: curl, ffprobe (ffmpeg), jq, column, awk | |
| set -euo pipefail | |
| usage() { | |
| cat <<EOF | |
| Usage: $0 <mp3_url> [--json] [--max-mb N] [--quiet] | |
| --json Output chapters as JSON (seconds), not a table | |
| --max-mb N Stop after N megabytes (default: 32) | |
| --quiet Suppress progress messages | |
| EOF | |
| } | |
| if [ $# -lt 1 ]; then usage; exit 1; fi | |
| URL="$1"; shift || true | |
| JSON_OUT="no" | |
| MAX_MB=32 | |
| QUIET="no" | |
| while [ $# -gt 0 ]; do | |
| case "$1" in | |
| --json) JSON_OUT="yes" ;; | |
| --max-mb) shift; MAX_MB="${1:-32}" ;; | |
| --quiet) QUIET="yes" ;; | |
| -h|--help) usage; exit 0 ;; | |
| *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; | |
| esac | |
| shift || true | |
| done | |
| TMP="$(mktemp -t mp3head_chaps.XXXXXX).mp3" | |
| HDR="$(mktemp -t mp3head_hdr.XXXXXX)" | |
| trap 'rm -f "$TMP" "$HDR"' EXIT | |
| # 1 MB chunk size | |
| MB=$((1024*1024)) | |
| chunk_mb=1 | |
| start_byte=0 | |
| end_byte=$((chunk_mb*MB - 1)) | |
| say() { [ "$QUIET" = "yes" ] || echo "$@"; } | |
| fetch_range() { | |
| local s="$1" e="$2" | |
| : > "$HDR" | |
| # Append bytes s..e to TMP | |
| curl -fsSL --range "${s}-${e}" -D "$HDR" "$URL" -o - >> "$TMP" || return $? | |
| # Detect if server honored range (206) or sent full file (200) | |
| local code | |
| code="$(awk 'BEGIN{c=""} /^HTTP\/[0-9.]+ /{c=$2} END{print c}' "$HDR")" | |
| if [ "$code" = "200" ]; then | |
| say "(server ignored range request; likely sent full file)" | |
| # No point continuing — we already have the entire response | |
| return 200 | |
| fi | |
| if grep -qi '^HTTP/.* 416' "$HDR"; then | |
| return 416 # past EOF | |
| fi | |
| return 0 | |
| } | |
| have_chapters() { | |
| ffprobe -v error -print_format json -show_chapters "$TMP" \ | |
| | jq -e '.chapters | length > 0' >/dev/null | |
| } | |
| print_chapters_table() { | |
| ffprobe -v error -print_format json -show_chapters "$TMP" \ | |
| | jq -r ' | |
| def two(n): ("0\((n|floor))" | (.[-2:] // .)); | |
| def hms(t): | |
| (t|tonumber|floor) as $s | |
| | ($s/3600|floor) as $h | |
| | (($s%3600)/60|floor) as $m | |
| | ($s%60|floor) as $sec | |
| | (two($h) + ":" + two($m) + ":" + two($sec)); | |
| .chapters[] | |
| | [hms(.start_time), hms(.end_time), (.tags.title // .tags.TIT2 // "")] | |
| | @tsv | |
| ' \ | |
| | awk 'BEGIN{print "start_time\tend_time\ttitle"}1' \ | |
| | column -t -s $'\t' | |
| } | |
| print_chapters_json() { | |
| ffprobe -v error -print_format json -show_chapters "$TMP" \ | |
| | jq '.chapters | |
| | map({start: (.start_time|tonumber), | |
| end: (.end_time|tonumber), | |
| title: (.tags.title // .tags.TIT2 // "")})' | |
| } | |
| # Incrementally fetch until chapters appear or we hit the cap | |
| while : ; do | |
| say "Fetching bytes ${start_byte}..${end_byte} (chunk: ${chunk_mb} MB; total: $((end_byte+1)) bytes)…" | |
| if ! fetch_range "$start_byte" "$end_byte"; then | |
| rc=$? | |
| if [ $rc -eq 200 ]; then | |
| # Full content delivered despite range; proceed to parse once. | |
| : | |
| elif [ $rc -eq 416 ]; then | |
| say "Reached end of file without finding chapters." | |
| break | |
| else | |
| echo "Download failed (curl exit $rc)." >&2 | |
| exit $rc | |
| fi | |
| fi | |
| if have_chapters; then | |
| say "—— Chapters ——" | |
| if [ "$JSON_OUT" = "yes" ]; then | |
| print_chapters_json | |
| else | |
| print_chapters_table | |
| fi | |
| exit 0 | |
| fi | |
| # If server ignored range (got full file), no point continuing | |
| if grep -q '^HTTP/.* 200' "$HDR"; then | |
| say "No chapter markers found." | |
| exit 3 | |
| fi | |
| # Next chunk | |
| chunk_mb=$((chunk_mb + 1)) | |
| if [ $chunk_mb -gt $MAX_MB ]; then | |
| echo "Gave up after ${MAX_MB} MB without finding chapters." >&2 | |
| exit 4 | |
| fi | |
| start_byte=$((end_byte + 1)) | |
| end_byte=$((end_byte + MB)) | |
| done | |
| # If we exit the loop without chapters: | |
| say "No chapter markers found." | |
| exit 3 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment