git/intake/main
scripts/make-fixtures.sh
446 lines18 KB last change raw · history
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
#!/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 <name> <seconds> <freq>  -- 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 <album-dir> <spec...>
#   spec forms:
#     t:<title>            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."