#!/usr/bin/env bash # Generate synthetic albums of tiny tones for testing. No real music. # # Usage: scripts/make-fixtures.sh [STAGING_DIR] (default ~/music/staging) set -euo pipefail STAGING="${1:-$HOME/music/staging}" POOL="$(mktemp -d)" trap 'rm -rf "$POOL"' EXIT command -v ffmpeg >/dev/null || { echo "need ffmpeg" >&2; exit 1; } command -v metaflac >/dev/null || { echo "need metaflac" >&2; exit 1; } echo "==> staging dir: $STAGING" mkdir -p "$STAGING" # --------------------------------------------------------------------------- # helpers # --------------------------------------------------------------------------- # base -- one lossless tone in the reusable pool base() { local name="$1" dur="$2" freq="$3" ffmpeg -hide_banner -loglevel error -y \ -f lavfi -i "sine=frequency=${freq}:duration=${dur}:sample_rate=44100" \ -af "volume=0.2,afade=t=in:d=0.05,afade=t=out:st=$(awk "BEGIN{print $dur-0.1}"):d=0.1" \ -c:a flac -compression_level 5 "$POOL/$name.flac" } # make # spec forms: # t: title (required, may contain spaces/odd chars) # any:key=value a tag; may be repeated # Disc/track numbers fall back to position within the current disc. make() { local dir="$STAGING/$1"; shift mkdir -p "$dir" local disc=1 track=0 i=0 sz local -a titles=() tags=() # first pass: split specs local spec for spec in "$@"; do case "$spec" in t:*) titles+=("${spec#t:}") ;; any:*) tags+=("${spec#any:}") ;; *) echo "bad spec: $spec" >&2; return 1 ;; esac done for spec in "${titles[@]}"; do track=$((track+1)) i=$((i+1)) # cycle the tone pool so files are distinct but generation is cheap case $((i % 3)) in 0) sz=pool_a ;; 1) sz=pool_b ;; *) sz=pool_c ;; esac cp "$POOL/$sz.flac" "$dir/tmp_$i.flac" local -a alltags=() # per-track tags supplied via any: apply to every track; callers that need # per-track differences use the special any:ARTIST@N= form handled below. local t for t in "${tags[@]}"; do case "$t" in *"@"[0-9]*=*) ;; # per-track, handled after *) alltags+=("$t") ;; esac done alltags+=("TITLE=$spec") # per-track overrides: KEY@N=value applies to track N for t in "${tags[@]}"; do case "$t" in *"@"[0-9]*=*) local key rest num val key="${t%%@*}" rest="${t#*@}" num="${rest%%=*}" val="${rest#*=}" if [ "$num" = "$i" ]; then alltags+=("$key=$val"); fi ;; esac done if [ ${#alltags[@]} -gt 0 ]; then printf '%s\n' "${alltags[@]}" > "$dir/tmp_$i.tags" metaflac --remove-all-tags --import-tags-from="$dir/tmp_$i.tags" "$dir/tmp_$i.flac" rm -f "$dir/tmp_$i.tags" else metaflac --remove-all-tags "$dir/tmp_$i.flac" fi # name it something deliberately messy so renaming is exercised mv "$dir/tmp_$i.flac" "$dir/$(printf '%02d' "$track") ${titles[$((i-1))]}.flac" done echo " made $(basename "$dir") (${#titles[@]} tracks)" } # art <album-dir> <width> <height> <colour> <label> art() { local dir="$STAGING/$1" w="$2" h="$3" colour="$4" label="$5" ffmpeg -hide_banner -loglevel error -y -f lavfi \ -i "color=c=${colour}:s=${w}x${h}:d=1" -frames:v 1 \ -vf "drawtext=text='${label}':fontcolor=white:fontsize=$((w/12)):x=(w-text_w)/2:y=(h-text_h)/2" \ "$dir/source_art.png" 2>/dev/null \ || ffmpeg -hide_banner -loglevel error -y -f lavfi \ -i "color=c=${colour}:s=${w}x${h}:d=1" -frames:v 1 "$dir/source_art.png" } # embed <album-dir> -- embed source_art.png as a front cover in every track embed() { local dir="$STAGING/$1" f for f in "$dir"/*.flac; do metaflac --import-picture-from="$dir/source_art.png" "$f" done rm -f "$dir/source_art.png" } # note <album-dir> <text> -- write FIXTURE.md note() { printf '%s\n' "$2" > "$STAGING/$1/FIXTURE.md"; } # rmclean <album-dir> <name...> -- remove paths (files or dirs), ignoring absence rmclean() { local d="$1"; shift; local n; for n in "$@"; do rm -rf "$d/$n"; done; } # --------------------------------------------------------------------------- # the tone pool # --------------------------------------------------------------------------- echo "==> generating tone pool" base pool_a 2 220 base pool_b 3 330 base pool_c 4 440 # --------------------------------------------------------------------------- # fixtures # --------------------------------------------------------------------------- echo "==> building fixtures" # --- 1. clean, well-tagged single-disc album ------------------------------- make "01-clean-single-disc" \ "t:First Light" "t:Second Wind" "t:Third Rail" \ any:ALBUM="Clean Single Disc" \ any:ALBUMARTIST="Test Artist" \ any:ARTIST="Test Artist" \ any:DATE="2015" \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 any:TRACKNUMBER@3=3 \ any:DISCNUMBER=1 note "01-clean-single-disc" "# 01 clean single disc Tests: the happy path. No extra tags, no crazy names, album art absent. Expected: - Common metadata displays ALBUM / ALBUMARTIST / DATE = 2015. - No extra tags to strip (nothing to remove). - Cover step must offer CAA/Deezer/iTunes plus placeholder (no embedded art). - Names become 1-01_first-light.flac etc. - Lands in archive/test-artist/clean-single-disc." # --- 2. clean, well-tagged TWO-DISC album --------------------------------- make "02-multi-disc" \ "t:Disc One Opener" "t:Disc One Closer" \ any:ALBUM="Multi Disc Set" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2019" any:DISCNUMBER=1 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 make "02-multi-disc/Disc 2" \ "t:Disc Two Opener" "t:Disc Two Closer" \ any:ALBUM="Multi Disc Set" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2019" any:DISCNUMBER=2 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 note "02-multi-disc" "# 02 multi-disc Tests: DISC number handling and the always-on disc prefix. Expected: - Recognised as ONE album with 2 discs, not two albums. - Names: 1-01_disc-one-opener.flac, 1-02_disc-one-closer.flac, 2-01_disc-two-opener.flac, 2-02_disc-two-closer.flac. - All four land in the same archive/test-artist/multi-disc directory." # --- 3. messy real-world tags -------------------------------------------- make "03-messy-tags" \ "t:Track With Genre" "t:Track With Comment" \ any:ALBUM="Messy Tags" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2007-03-14" any:DISCNUMBER=1 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 \ any:GENRE="IDM" any:COMMENT="downloaded from somewhere" any:RATING="5" \ any:LABEL="Some Label" any:CATALOGNUMBER="CAT-001" \ any:REPLAYGAIN_TRACK_GAIN="-3.2 dB" any:ENCODER="old ripper 0.1" \ any:MOOD="chill" any:BPM="120" note "03-messy-tags" "# 03 messy tags Tests: tag stripping and DATE normalisation. Expected: - Review shows the extra tags being REMOVED: GENRE, COMMENT, RATING, LABEL, CATALOGNUMBER, REPLAYGAIN_TRACK_GAIN, ENCODER, MOOD, BPM. - DATE '2007-03-14' normalises to 2007. - After writing, only the 7 whitelisted tags remain. - Lands in archive/test-artist/messy-tags." # --- 4. non-Latin titles, collisions, empty normalisation ---------------- make "04-unicode-and-symbols" \ "t:夜明け" "t:夜明け (Reprise)" "t:Café Münster" \ "t:Don't Stop" "t:Track (feat. Someone) [Remix]" "t:★★★★★" \ any:ALBUM="Unicode And Symbols" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2021" any:DISCNUMBER=1 note "04-unicode-and-symbols" "# 04 unicode, symbols, collisions Tests: THE RISKY ONE. NFKD transliteration, lossy normalisation, collisions, and the name editor as the way out. Expected with --yes and no editor action: - '夜明け' and '★★★★★' both normalise to an empty name, so intake REFUSES to guess rather than writing '1-06_.flac'. Nothing is written. Expected interactively (or with a scripted editor): - The editor opens on the names, with the empty ones shown blank and the originals in the trailing comment. - Filling in names lets the album through; the TITLE tags keep the original Japanese and symbols, because a name the user typed is a filename, not a licence to rewrite the tag. - 'Café Münster' -> 'cafe-munster' (accent folding works), and \"Don't Stop\" -> 'dont-stop' need no editing at all." # --- 5. mixed art and mixed formats -------------------------------------- rmclean "$STAGING/05-art-and-formats" source_art.png cover.jpg README.txt notes.txt broken.flac broken.mp3 make "05-art-and-formats" \ "t:Has Embedded Art" "t:Also Has Art" \ any:ALBUM="Art And Formats" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2012" any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 any:DISCNUMBER=1 art "05-art-and-formats" 300 300 darkgreen "300x300" embed "05-art-and-formats" note "05-art-and-formats" "# 05 embedded art + a folder image + stray formats Tests: cover candidates and non-audio clutter. Expected: - Cover step offers the EMBEDDED 300x300 image as a candidate. - A separate cover.jpg is also present as a folder-image candidate. - The 300x300 embedded art is LOW resolution: intake should warn that a better source exists and prefer/preview the larger candidates. - A 'broken.mp3' containing plain text and README.txt / notes.txt must be ignored, not choke the scan." # a larger folder image + non-audio clutter art "05-art-and-formats" 1200 1200 navy "1200x1200" mv "$STAGING/05-art-and-formats/source_art.png" "$STAGING/05-art-and-formats/cover.jpg" printf 'clutter that must be ignored\n' > "$STAGING/05-art-and-formats/README.txt" printf 'more clutter\n' > "$STAGING/05-art-and-formats/notes.txt" printf 'not really an mp3\n' > "$STAGING/05-art-and-formats/broken.mp3" # --- 6. compilation / various artists ------------------------------------ make "06-compilation" \ "t:Comp Track One" "t:Comp Track Two" "t:Comp Track Three" \ any:ALBUM="Big Compilation" any:ALBUMARTIST="Various Artists" \ any:DATE="2003" any:DISCNUMBER=1 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 any:TRACKNUMBER@3=3 \ any:ARTIST@1="Artist One" any:ARTIST@2="Artist Two" any:ARTIST@3="Artist Three" note "06-compilation" "# 06 compilation / various artists Tests: per-track ARTIST differing, ALBUMARTIST = Various Artists. Expected: - Review must show ARTIST varies per track (not hidden as 'common'). - Directory key is ALBUMARTIST, i.e. archive/various-artists/big-compilation (NOT artist-one/big-compilation). - Per-track ARTIST is preserved in the written tags." # --- 7. no ARTIST tag at all (only ALBUMARTIST) -------------------------- make "07-missing-artist" \ "t:Only Album Artist" "t:Still Only Album Artist" \ any:ALBUM="Missing Artist" any:ALBUMARTIST="Fallback Artist" \ any:DATE="1999" any:DISCNUMBER=1 any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 note "07-missing-artist" "# 07 missing ARTIST Tests: ARTIST fallback. Expected: - ARTIST is absent on every track; intake fills it from ALBUMARTIST. - Review should say so explicitly rather than silently inventing it. - Lands in archive/fallback-artist/missing-artist." # --- 8. tags stripped bare: filename is the only clue ------------------- make "08-no-tags-at-all" \ "t:Untagged One" "t:Untagged Two" "t:Untagged Three" \ any:DISCNUMBER=1 note "08-no-tags-at-all" "# 08 no usable tags Tests: fully untagged audio (only DISCNUMBER present). The editor is the way to supply everything. Expected: - ALBUM and ALBUMARTIST are missing, so intake does NOT invent them from the directory name; it opens the editor with 'album:' and 'artist:' blank. - Track numbers are missing: inferred from the leading filename digits, and the inference is shown. - Whatever the user types becomes both the tags and the folders. - Nothing is written to archive/ until that happens." # --- 9. 120 tracks: zero-padding past 99 -------------------------------- specs=() for n in $(seq 1 120); do specs+=("t:Long Player Track $n"); done specs+=("any:ALBUM=Hundred Twenty" "any:ALBUMARTIST=Test Artist" "any:ARTIST=Test Artist" "any:DATE=2020" "any:DISCNUMBER=1") make "09-past-ninety-nine" "${specs[@]}" note "09-past-ninety-nine" "# 09 more than 99 tracks Tests: zero-padding width decision. Expected: - With 120 tracks, 2-digit padding is impossible to keep unique AND consistent. intake should widen the whole album to 3 digits (1-001_... through 1-120_...) rather than mixing 1-99 and 1-100 widths. - This fixture exists to make that decision concrete." # --- 10. unusual album/artist names for directory normalisation --------- make "10-naming-edge-cases" \ "t:Opener" "t:Closer" \ any:ALBUM="Álbum: The (Deluxe) Edition!" \ any:ALBUMARTIST="Björk & Sigur Rós" \ any:ARTIST="Björk & Sigur Rós" \ any:DATE="2001" any:DISCNUMBER=1 any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 note "10-naming-edge-cases" "# 10 directory-name edge cases Tests: normalisation of artist and album into directory names. Expected: - ALBUMARTIST 'Björk & Sigur Rós' -> 'bjork-and-sigur-ros' (or 'bjork-sigur-ros'; the exact treatment of '&' is what this fixture pins down). - ALBUM 'Álbum: The (Deluxe) Edition!' -> 'album-the-deluxe-edition'. - Confirm the review shows the proposed directories BEFORE creating them." # --- 10b. artist credit with no ASCII form ------------------------------ make "10b-unromanisable-artist" \ "t:First Track" "t:Second Track" \ any:ALBUM="夜明け" any:ALBUMARTIST="椎名林檎" any:ARTIST="椎名林檎" \ any:DATE="2019" any:DISCNUMBER=1 any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 note "10b-unromanisable-artist" "# 10b a credit and title with no ASCII form Tests: the name editor and unromanisable artist resolution. Kanji carry no readings in Unicode, so neither the artist folder nor the album folder can be derived automatically. Expected: - The artist cannot be romanised, so intake asks for a folder name (or, under --yes, uses a placeholder and defers to the editor). - The editor opens with 'album:' and 'artist:' blank and the hints explaining why, plus a track line whose name is empty after the generated prefix. - Whatever the user types becomes the folder names; the ALBUM and ALBUMARTIST TAGS keep the original Japanese text, so no metadata is lost. - The decision is written to artists.toml, so the next album by this artist resolves with no prompt." # --- 11. empty staging entry (not an album) ----------------------------- mkdir -p "$STAGING/11-empty-dir" note "11-empty-dir" "# 11 empty directory Tests: an empty (or later, partially-downloaded) directory in staging. Expected: - scan reports it as not-an-album / incomplete and SKIPS it. - intake album on it refuses with a clear message, exits non-zero." # --- 12. single track --------------------------------------------------- make "12-single-track" \ "t:The Only Song" \ any:ALBUM="Just One Song" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2022" any:DISCNUMBER=1 any:TRACKNUMBER=1 note "12-single-track" "# 12 single-track release Tests: degenerate album (one track). Common metadata must still display. Expected: - Works exactly like any other album with one track. - Lands in archive/test-artist/just-one-song/1-01_the-only-song.flac." # --- 13. long titles for filesystem limits ------------------------------ make "13-long-and-odd-names" \ "t:A Very Long Track Title That Goes On And On And Should Still Be Handled Gracefully Without Blowing Up The Filesystem Limit Because That Would Be Bad" \ "t:This Title Is Absurdly Long On Purpose To Force The Normaliser To Actually Truncate The Resulting Filename Rather Than Merely Come Close To The Limit Which Would Not Test Anything Useful At All" \ "t:trailing dots..." \ "t: leading and trailing spaces " \ "t:MIXED Case TiTlE" \ any:ALBUM="Long And Odd Names" any:ALBUMARTIST="Test Artist" any:ARTIST="Test Artist" \ any:DATE="2018" any:DISCNUMBER=1 note "13-long-and-odd-names" "# 13 long / odd / whitespace titles Tests: filename length limits and whitespace hygiene. Expected: - The second track's normalised name would exceed 200 bytes, so intake must TRUNCATE it, preserve the .flac extension, and show the shortening as a warning before writing. - The first track is long but under the cap, so it must NOT be truncated. - Trailing dots and leading/trailing spaces are stripped (they break Windows and are invisible). - 'MIXED Case TiTlE' -> 'mixed-case-title'." make "14-compilation-untagged" \ "t:Split One" "t:Split Two" "t:Split Three" "t:Split Four" \ any:ALBUM="Untagged Compilation" any:DATE="2001" any:DISCNUMBER=1 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 any:TRACKNUMBER@3=3 any:TRACKNUMBER@4=4 \ any:ARTIST@1="One Artist" any:ARTIST@2="Two Artist" \ any:ARTIST@3="Three Artist" any:ARTIST@4="Four Artist" note "14-compilation-untagged" "# 14 compilation with no ALBUMARTIST Tests: a genuine compilation that never had an ALBUMARTIST tag. Expected: - Files under archive/various-artists/untagged-compilation. No majority artist among four different credits, so this is the various-artists case. - Must NOT be refused. The album artist is only unknown when there is also no reason to think it is a compilation; four different credits is that reason." make "15-guest-tracks" \ "t:Solo One" "t:Solo Two" "t:Solo Three" "t:With A Guest" \ any:ALBUM="Guests" any:DATE="2012" any:DISCNUMBER=1 \ any:TRACKNUMBER@1=1 any:TRACKNUMBER@2=2 any:TRACKNUMBER@3=3 any:TRACKNUMBER@4=4 \ "any:ALBUM ARTIST=Main Artist" \ any:ARTIST@1="Main Artist" any:ARTIST@2="Main Artist" \ any:ARTIST@3="Main Artist" any:ARTIST@4="Main Artist + Guest" note "15-guest-tracks" "# 15 album with guest tracks Tests: an album whose tracks credit a guest, tagged ALBUM ARTIST with a space. Expected: - 'ALBUM ARTIST' means the same as ALBUMARTIST, so it is used and must not appear in the list of tags being removed. - Files under archive/main-artist/guests, NOT various-artists. One track crediting a guest does not make an album a compilation. - Per-track ARTIST keeps the guest credit on the written tags." echo echo "==> done. Fixtures in $STAGING" echo "==> albums:" for d in "$STAGING"/*/; do n=$(find "$d" -name '*.flac' | wc -l) printf ' %-28s %3s tracks\n' "$(basename "$d")" "$n" done echo echo "NOTE: 11-empty-dir is intentionally empty."