#!/usr/bin/env bash # Generate pathological albums: the shapes downloads actually arrive in. # # Kept apart from make-fixtures.sh because these are about surviving bad input # rather than exercising the normal paths. Each directory gets an EDGE.md saying # what is odd about it and what intake should do with it. # # Usage: scripts/make-edge-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" tone() { # tone [extra ffmpeg args...] local freq="$1" out="$2"; shift 2 ffmpeg -hide_banner -loglevel error -y -f lavfi -i "sine=frequency=$freq:duration=1" "$@" "$out" } for i in 1 2 3 4 5 6; do tone $((300 + i * 90)) "$POOL/t$i.flac" -ac 2 -ar 44100 done tag() { # tag ... local f="$1"; shift if [ "${f##*.}" = "flac" ]; then printf '%s\n' "$@" > "$f.tags" metaflac --remove-all-tags --import-tags-from="$f.tags" "$f" rm -f "$f.tags" return fi local args=() local kv for kv in "$@"; do args+=(-metadata "$kv"); done # the temp name keeps the extension, or ffmpeg cannot pick a muxer local tmp="$f.tagged.${f##*.}" ffmpeg -hide_banner -loglevel error -y -i "$f" "${args[@]}" -c copy "$tmp" mv "$tmp" "$f" } note() { # note mkdir -p "$STAGING/$1" printf '%s\n' "$2" > "$STAGING/$1/EDGE.md" } album() { mkdir -p "$STAGING/$1"; } echo "==> building edge cases" # -------------------------------------------------------------------------- # naming and structure # -------------------------------------------------------------------------- # A folder with no artist separator, so nothing can be inferred from it. album "e01-no-hint-anywhere" cp "$POOL/t1.flac" "$STAGING/e01-no-hint-anywhere/01 one.flac" cp "$POOL/t2.flac" "$STAGING/e01-no-hint-anywhere/02 two.flac" note "e01-no-hint-anywhere" "# no artist or album anywhere Files are untagged and the folder name carries no separator, so neither tag nor name gives an artist. Expected: refuse. This is the case the name editor exists for, and guessing 'one' as the artist would be worse than stopping." # An album folder nested inside another folder of the same name. album "e02-nested/An Album" for i in 1 2; do cp "$POOL/t$i.flac" "$STAGING/e02-nested/An Album/0$i track.flac" tag "$STAGING/e02-nested/An Album/0$i track.flac" \ "TITLE=Track $i" "ALBUM=An Album" "ARTIST=Nested Artist" \ "ALBUMARTIST=Nested Artist" "TRACKNUMBER=$i" "DISCNUMBER=1" "DATE=2010" done note "e02-nested" "# album nested one level down The album is inside a wrapper folder rather than directly in staging. Expected: either file the inner album correctly or say clearly that the wrapper holds no audio. It must not file the wrapper as an album with zero tracks." # Files sitting directly in the staging root, belonging to no album. mkdir -p "$STAGING/e03-loose-files" cp "$POOL/t1.flac" "$STAGING/e03-loose-files/loose one.flac" tag "$STAGING/e03-loose-files/loose one.flac" \ "TITLE=Loose" "ALBUM=Loose Album" "ARTIST=Loose Artist" \ "ALBUMARTIST=Loose Artist" "TRACKNUMBER=1" "DISCNUMBER=1" rm -f "$STAGING/e03-loose-files/EDGE.md" note "e03-loose-files" "# a release of exactly one track A single audio file with full tags and nothing else. (Files sitting loose in the staging root, not in any folder, are a separate case: they are ignored, so that staging itself is never treated as an album. That behaviour does not fit the fixture layout here, which is one directory per case, so this fixture covers the one-track release instead.) Expected: files normally. A single-track release is not an error." # "CD1" / "CD2" folders rather than tagged disc numbers. album "e04-cd-folders/CD1" album "e04-cd-folders/CD2" for d in 1 2; do for t in 1 2; do f="$STAGING/e04-cd-folders/CD$d/$t track.flac" cp "$POOL/t$((d * 2 + t - 2)).flac" "$f" tag "$f" "TITLE=CD$d Track $t" "ALBUM=CD Folders" "ARTIST=CD Artist" \ "ALBUMARTIST=CD Artist" "TRACKNUMBER=$t" "DATE=2005" done done note "e04-cd-folders" "# discs as CD1 / CD2 subfolders, no DISCNUMBER tags Expected: either infer disc 1 and 2 from the folder names, or file everything as disc 1. What it must not do is invent track numbers that collide, or drop a disc." # -------------------------------------------------------------------------- # tags # -------------------------------------------------------------------------- # ALBUM differs from track to track. album "e05-inconsistent-album" for i in 1 2 3; do f="$STAGING/e05-inconsistent-album/0$i track.flac" cp "$POOL/t$i.flac" "$f" # the second track is mistagged, which is the ordinary version of this alb="Right Album" [ "$i" = "2" ] && alb="Rong Album" tag "$f" "TITLE=Inconsistent $i" "ALBUM=$alb" "ARTIST=Inc Artist" \ "ALBUMARTIST=Inc Artist" "TRACKNUMBER=$i" "DISCNUMBER=1" done note "e05-inconsistent-album" "# one track out of three has a mistyped ALBUM tag Expected: two of three agree, so the majority is used and the disagreement is reported. A single bad tag should not stop an otherwise fine album." # Duplicate track numbers within one disc. album "e06-duplicate-numbers" for i in 1 2; do f="$STAGING/e06-duplicate-numbers/0$i track.flac" cp "$POOL/t$i.flac" "$f" tag "$f" "TITLE=Duplicate $i" "ALBUM=Dupe Numbers" "ARTIST=Dupe Artist" \ "ALBUMARTIST=Dupe Artist" "TRACKNUMBER=1" "DISCNUMBER=1" done note "e06-duplicate-numbers" "# two tracks both numbered 1 Expected: the target names differ because the titles do, so this files. Two files claiming track 1 is a bad rip rather than an intake problem, but it is worth a warning." # Track numbers that are not numbers. album "e07-junk-track-numbers" for n in "A" "B"; do f="$STAGING/e07-junk-track-numbers/$n song.flac" cp "$POOL/t1.flac" "$f" tag "$f" "TITLE=$n Song" "ALBUM=Junk Numbers" "ARTIST=Junk Artist" \ "ALBUMARTIST=Junk Artist" "TRACKNUMBER=$n" "DISCNUMBER=1" done note "e07-junk-track-numbers" "# TRACKNUMBER=A and TRACKNUMBER=B Expected: fall back to numbering in file order, and say it did." # Titles that normalise to the same name. album "e08-title-collision" i=1 for t in "Song!" "Song?"; do f="$STAGING/e08-title-collision/track $i.flac" cp "$POOL/t1.flac" "$f" tag "$f" "TITLE=$t" "ALBUM=Collide" "ARTIST=Collide Artist" \ "ALBUMARTIST=Collide Artist" "TRACKNUMBER=$i" "DISCNUMBER=1" i=$((i + 1)) done note "e08-title-collision" "# 'Song!' and 'Song?' normalise to the same name Expected: refuse with a hard issue naming both offending files, before anything is written. Silent clobbering here would lose a track." # An artist whose name is entirely symbols. album "e09-symbol-artist" cp "$POOL/t1.flac" "$STAGING/e09-symbol-artist/01 track.flac" tag "$STAGING/e09-symbol-artist/01 track.flac" \ "TITLE=Symbol Song" "ALBUM=Symbol Album" "ARTIST=★★★" "ALBUMARTIST=★★★" \ "TRACKNUMBER=1" "DISCNUMBER=1" note "e09-symbol-artist" "# ALBUMARTIST is entirely non-ASCII symbols Expected: refuse. A folder name cannot be made from it, and the editor is the way out. It must not create a directory of punctuation." # Windows reserved device names as titles. album "e10-reserved-names" for t in con nul aux; do f="$STAGING/e10-reserved-names/$t.flac" cp "$POOL/t1.flac" "$f" tag "$f" "TITLE=$t" "ALBUM=Reserved" "ARTIST=Reserved Artist" \ "ALBUMARTIST=Reserved Artist" "TRACKNUMBER=1" "DISCNUMBER=1" done note "e10-reserved-names" "# titles are con, nul and aux These are device names on Windows and cannot be used as file names there. Expected: decide and document. On Linux they are legal, so filing them is defensible, but a library meant to be portable will hit trouble copying it to a Windows machine." # -------------------------------------------------------------------------- # files # -------------------------------------------------------------------------- # A mixture of formats in one album. album "e11-mixed-formats" cp "$POOL/t1.flac" "$STAGING/e11-mixed-formats/01 lossless.flac" tone 700 "$STAGING/e11-mixed-formats/02 lossy.mp3" -ac 2 -ar 44100 -b:a 128k tag "$STAGING/e11-mixed-formats/01 lossless.flac" \ "TITLE=Lossless" "ALBUM=Mixed Formats" "ARTIST=Mixed Artist" \ "ALBUMARTIST=Mixed Artist" "TRACKNUMBER=1" "DISCNUMBER=1" tag "$STAGING/e11-mixed-formats/02 lossy.mp3" \ "TITLE=Lossy" "ALBUM=Mixed Formats" "ARTIST=Mixed Artist" \ "ALBUMARTIST=Mixed Artist" "TRACKNUMBER=2" "DISCNUMBER=1" note "e11-mixed-formats" "# one flac and one mp3 in the same album Expected: both are filed, each keeping its own extension. Transcoding a lossy file into the library should not pretend it is lossless, and the warning is worth having." # A truncated file: valid header, no data. album "e12-truncated" head -c 4096 "$POOL/t1.flac" > "$STAGING/e12-truncated/01 truncated.flac" tag "$STAGING/e12-truncated/01 truncated.flac" \ "TITLE=Truncated" "ALBUM=Broken" "ARTIST=Broken Artist" \ "ALBUMARTIST=Broken Artist" "TRACKNUMBER=1" "DISCNUMBER=1" 2>/dev/null || true note "e12-truncated" "# a flac truncated to its first 4KB Expected: refuse before promoting anything, and leave staging untouched. A half-downloaded file is the most common bad file on Soulseek." # A file that is not audio at all, wearing an audio extension. album "e13-not-audio" printf 'this is not audio, it is a text file\n' > "$STAGING/e13-not-audio/01 fake.flac" note "e13-not-audio" "# text file named .flac Expected: refuse. It must not be copied into the archive and presented as a master just because it has the right extension." # A zero byte file. album "e14-empty-file" : > "$STAGING/e14-empty-file/01 empty.flac" note "e14-empty-file" "# zero byte flac Expected: refuse." # High resolution, mono and a very short track in one album. album "e15-unusual-audio" tone 400 "$STAGING/e15-unusual-audio/01 hires.flac" -ac 2 -ar 96000 -sample_fmt s32 tone 500 "$STAGING/e15-unusual-audio/02 mono.flac" -ac 1 -ar 22050 tone 600 "$STAGING/e15-unusual-audio/03 short.flac" -ac 2 -ar 44100 -t 0.3 i=1 for f in "$STAGING/e15-unusual-audio"/*.flac; do tag "$f" "TITLE=Unusual $i" "ALBUM=Unusual Audio" "ARTIST=Unusual Artist" \ "ALBUMARTIST=Unusual Artist" "TRACKNUMBER=$i" "DISCNUMBER=1" i=$((i + 1)) done note "e15-unusual-audio" "# 96kHz 32-bit, mono, and a 0.3 second track Expected: all three file. Nothing about the sample rate, channel count or duration should cause a refusal." # -------------------------------------------------------------------------- # clutter and sidecars # -------------------------------------------------------------------------- album "e16-clutter" for i in 1 2; do f="$STAGING/e16-clutter/0$i track.flac" cp "$POOL/t$i.flac" "$f" tag "$f" "TITLE=Clutter $i" "ALBUM=Clutter" "ARTIST=Clutter Artist" \ "ALBUMARTIST=Clutter Artist" "TRACKNUMBER=$i" "DISCNUMBER=1" done printf 'PERFORMER "Clutter Artist"\nFILE "01 track.flac" WAVE\n' > "$STAGING/e16-clutter/album.cue" printf 'Exact Audio Copy log\n' > "$STAGING/e16-clutter/album.log" printf 'release info\n' > "$STAGING/e16-clutter/album.nfo" printf 'x\n' > "$STAGING/e16-clutter/album.sfv" printf '#EXTM3U\n' > "$STAGING/e16-clutter/album.m3u" printf 'partial download\n' > "$STAGING/e16-clutter/03 track.flac.part" printf 'junk\n' > "$STAGING/e16-clutter/.DS_Store" printf 'junk\n' > "$STAGING/e16-clutter/._01 track.flac" printf 'junk\n' > "$STAGING/e16-clutter/Thumbs.db" printf 'junk\n' > "$STAGING/e16-clutter/desktop.ini" note "e16-clutter" "# every sidecar a rip comes with cue, log, nfo, sfv, m3u, a .part, and the hidden files macOS and Windows leave behind. Expected: audio is filed, all of it is removed from staging, and the removals are reported. Nothing here should be mistaken for a track or for cover art." # A single file album plus a cue sheet, which is how a lot of rips arrive. album "e17-cue-single-file" cp "$POOL/t1.flac" "$STAGING/e17-cue-single-file/whole album.flac" tag "$STAGING/e17-cue-single-file/whole album.flac" \ "TITLE=Whole Album" "ALBUM=Cue Album" "ARTIST=Cue Artist" \ "ALBUMARTIST=Cue Artist" "TRACKNUMBER=1" "DISCNUMBER=1" printf 'FILE "whole album.flac" WAVE\n TRACK 01 AUDIO\n TITLE "One"\n TRACK 02 AUDIO\n TITLE "Two"\n' \ > "$STAGING/e17-cue-single-file/album.cue" note "e17-cue-single-file" "# one long file plus a cue sheet describing many tracks Expected: this is NOT supported, and intake should say so rather than filing the whole thing as a one-track album without comment. Splitting on a cue sheet is a separate feature. The important part is that the user is told, because otherwise they end up with an album whose track list is wrong." # -------------------------------------------------------------------------- # cover art # -------------------------------------------------------------------------- # Art that is not an image. album "e18-broken-art" cp "$POOL/t1.flac" "$STAGING/e18-broken-art/01 track.flac" tag "$STAGING/e18-broken-art/01 track.flac" \ "TITLE=Broken Art" "ALBUM=Broken Art" "ARTIST=Art Artist" \ "ALBUMARTIST=Art Artist" "TRACKNUMBER=1" "DISCNUMBER=1" printf 'not an image at all\n' > "$STAGING/e18-broken-art/cover.jpg" note "e18-broken-art" "# cover.jpg is a text file Expected: ignore it as artwork and carry on, falling back to a placeholder or to another source. A bad image file must not fail the album." # Art in a subfolder, which is how scans usually arrive. album "e19-art-in-subfolder/Scans" cp "$POOL/t1.flac" "$STAGING/e19-art-in-subfolder/01 track.flac" tag "$STAGING/e19-art-in-subfolder/01 track.flac" \ "TITLE=Scanned" "ALBUM=Scanned Album" "ARTIST=Scan Artist" \ "ALBUMARTIST=Scan Artist" "TRACKNUMBER=1" "DISCNUMBER=1" tone 800 "$POOL/art.flac" ffmpeg -hide_banner -loglevel error -y -f lavfi -i "color=c=blue:s=600x600:d=1" -frames:v 1 \ "$STAGING/e19-art-in-subfolder/Scans/front.jpg" note "e19-art-in-subfolder" "# the only artwork is Scans/front.jpg Expected: ideally found and offered. If not, it falls back to a placeholder, which is acceptable but worth knowing." # -------------------------------------------------------------------------- # text # -------------------------------------------------------------------------- # Emoji, zero width space and a right-to-left mark in the names. album "e20-odd-text" cp "$POOL/t1.flac" "$STAGING/e20-odd-text/01 track.flac" tag "$STAGING/e20-odd-text/01 track.flac" \ "TITLE=Song 🎵 With Emoji" "ALBUM=Album​With Zero Width" \ "ARTIST=RTL ‏Artist" "ALBUMARTIST=RTL ‏Artist" "TRACKNUMBER=1" "DISCNUMBER=1" note "e20-odd-text" "# emoji and zero-width characters in tags Expected: they are stripped or kept consistently and never produce an empty or invisible name. A zero-width space surviving into a file name is the failure mode worth catching." # A very long album name, and one containing a slash. album "e21-long-names" cp "$POOL/t1.flac" "$STAGING/e21-long-names/01 track.flac" tag "$STAGING/e21-long-names/01 track.flac" \ "TITLE=A Song" "ALBUM=$(printf 'Very Long Album Name %.0s' {1..20})" \ "ARTIST=Long Artist" "ALBUMARTIST=Long Artist" "TRACKNUMBER=1" "DISCNUMBER=1" note "e21-long-names" "# an album name far past 200 bytes Expected: the directory name is truncated to something legal, and the shortening is reported rather than happening silently." album "e22-slash-in-name" cp "$POOL/t1.flac" "$STAGING/e22-slash-in-name/01 track.flac" tag "$STAGING/e22-slash-in-name/01 track.flac" \ "TITLE=Slash Song" "ALBUM=AC/DC Live" "ARTIST=Slash Artist" \ "ALBUMARTIST=Slash Artist" "TRACKNUMBER=1" "DISCNUMBER=1" note "e22-slash-in-name" "# the album name contains a slash Expected: the slash must not become a directory separator, or the album is written into a tree one level deeper than intended. It should be replaced." echo echo "==> done. Edge cases in $STAGING" for d in "$STAGING"/e*/; do n=$(find "$d" -name '*.flac' -o -name '*.mp3' 2>/dev/null | wc -l) printf ' %-28s %3s audio file(s)\n' "$(basename "$d")" "$n" done