#!/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 <freq> <path> [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 <file> <key=value>...
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 <album-dir> <text>
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=AlbumWith 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