#!/usr/bin/env bash # Seeds the devnet's fixed accounts with a follow graph, a small shared # catalog of releases, and crate entries (grades, ratings, notes, prices, # multi-event timelines, cover art, and deliberate feed choreography) so the # following/followers, feed, and record-detail UI have data to render # without touching the live network. The feed is built from ADOPTION rows: # entries whose `release` field points at a catalog.release record, so most # genesis entries below carry one. # # Not idempotent: expects a freshly wiped devnet. Every account, follow, # release, and entry below is unconditionally created, so a second run # duplicates the whole graph and changes the feed shape the choreography # comments describe. set -euo pipefail if ((BASH_VERSINFO[0] < 4)); then echo "seed-social.sh: requires bash >= 4 (this script's declare -A needs it)." >&2 echo "macOS ships bash 3.2; install a newer one (e.g. 'brew install bash') and rerun with that." >&2 exit 1 fi PDS="${PDS_URL:-http://localhost:2584}" PASSWORD="${TEST_ACCOUNT_PASSWORD:-e2e-test-password}" # Every string value below is JSON-encoded through this instead of hand-quoted, # so a title/artist/note containing a quote, backslash, or newline can never # produce malformed JSON. python3 is already a dependency (the PNG encoder). json_str() { python3 -c 'import json, sys; print(json.dumps(sys.argv[1]))' "$1" } # BSD `date -r ` reads an epoch; GNU `date -r ` stats a file of # that name and fails, falling through to `-d @`. Trying -r first # keeps this portable across macOS and Linux without a uname branch. ts_offset() { local epoch="$1" date -u -r "$epoch" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "@$epoch" +%Y-%m-%dT%H:%M:%SZ } NOW_EPOCH="$(date -u +%s)" T0="$(ts_offset "$NOW_EPOCH")" T1="$(ts_offset $((NOW_EPOCH + 3600)))" T2="$(ts_offset $((NOW_EPOCH + 7200)))" # Well outside any converge/batch collapse window, for the one entry that # must read as a standalone reasonSingle rather than join dave's batch. T_PAST="$(ts_offset $((NOW_EPOCH - 90000)))" session() { curl -sf "$PDS/xrpc/com.atproto.server.createSession" \ -H 'content-type: application/json' \ -d "{\"identifier\":$(json_str "$1"),\"password\":$(json_str "$PASSWORD")}" } did_of() { echo "$1" | grep -o '"did":"[^"]*"' | head -1 | cut -d'"' -f4; } jwt_of() { echo "$1" | grep -o '"accessJwt":"[^"]*"' | cut -d'"' -f4; } uri_of() { echo "$1" | grep -o '"uri":"[^"]*"' | head -1 | cut -d'"' -f4; } cid_of() { echo "$1" | grep -o '"cid":"[^"]*"' | head -1 | cut -d'"' -f4; } create_record() { local jwt="$1" repo="$2" collection="$3" record="$4" curl -sf "$PDS/xrpc/com.atproto.repo.createRecord" \ -H "authorization: Bearer $jwt" -H 'content-type: application/json' \ -d "{\"repo\":$(json_str "$repo"),\"collection\":$(json_str "$collection"),\"record\":$record}" } follow() { local jwt="$1" repo="$2" subject_did="$3" create_record "$jwt" "$repo" dev.mokkenstorm.crate.graph.follow \ "{\"\$type\":\"dev.mokkenstorm.crate.graph.follow\",\"subject\":$(json_str "$subject_did"),\"createdAt\":$(json_str "$T0")}" \ > /dev/null } # Appends `,"key":` when value is non-empty, else nothing; lets # JSON builders below stay flat instead of branching per optional field. opt_str() { [[ -z "${2:-}" ]] && return 0 printf ',"%s":%s' "$1" "$(json_str "$2")" } # Same as opt_str but for values that are already JSON (ints, objects). opt_raw() { [[ -z "${2:-}" ]] && return 0 printf ',"%s":%s' "$1" "$2" } subject_ref() { printf '{"uri":%s,"cid":%s}' "$(json_str "$1")" "$(json_str "$2")"; } # catalogRef and strongRef share the {uri, cid} shape we need here; named # separately so call sites read as what they point at. catalog_ref() { subject_ref "$1" "$2"; } # Writes a genesis shelf entry (no subject) and returns the raw createRecord # response so chained callers can pull uri/cid for follow-up events. write_genesis() { local jwt="$1" repo="$2" action="$3" created="$4" artist="$5" title="$6" \ media="$7" sleeve="$8" rating="$9" folder="${10}" notes="${11}" \ counterparty="${12}" price_amount="${13}" price_currency="${14}" cover_json="${15}" \ release_json="${16:-}" local price_json="" [[ -n "$price_amount" ]] && price_json="{\"amount\":$price_amount,\"currency\":$(json_str "$price_currency")}" local snapshot_json="{\"artistDisplay\":$(json_str "$artist"),\"title\":$(json_str "$title")$(opt_raw cover "$cover_json")}" local record record="{\"\$type\":\"dev.mokkenstorm.crate.shelf.entry\",\"action\":$(json_str "$action"),\"createdAt\":$(json_str "$created"),\"snapshot\":$snapshot_json$(opt_str folder "$folder")$(opt_str mediaGrade "$media")$(opt_str sleeveGrade "$sleeve")$(opt_raw rating "$rating")$(opt_str notes "$notes")$(opt_str counterparty "$counterparty")$(opt_raw price "$price_json")$(opt_raw release "$release_json")}" create_record "$jwt" "$repo" dev.mokkenstorm.crate.shelf.entry "$record" } # Appends a follow-up event referencing a prior entry's strongRef. Discards # the response: nothing here chains a third time. write_event() { local jwt="$1" repo="$2" action="$3" created="$4" subject_json="$5" \ rating="${6:-}" notes="${7:-}" media="${8:-}" sleeve="${9:-}" local record record="{\"\$type\":\"dev.mokkenstorm.crate.shelf.entry\",\"action\":$(json_str "$action"),\"createdAt\":$(json_str "$created"),\"subject\":$subject_json$(opt_raw rating "$rating")$(opt_str notes "$notes")$(opt_str mediaGrade "$media")$(opt_str sleeveGrade "$sleeve")}" create_record "$jwt" "$repo" dev.mokkenstorm.crate.shelf.entry "$record" > /dev/null } owned_entry() { local jwt="$1" repo="$2" artist="$3" title="$4" media="$5" sleeve="$6" \ rating="$7" folder="$8" notes="$9" cover="${10}" release="${11:-}" write_genesis "$jwt" "$repo" acquired "$T0" "$artist" "$title" \ "$media" "$sleeve" "$rating" "$folder" "$notes" "" "" "" "$cover" "$release" > /dev/null } wanted_entry() { local jwt="$1" repo="$2" artist="$3" title="$4" folder="$5" release="${6:-}" write_genesis "$jwt" "$repo" wanted "$T0" "$artist" "$title" \ "" "" "" "$folder" "" "" "" "" "" "$release" > /dev/null } # Writes an owned genesis entry then two follow-up events (rated, then # annotated), each a bit later than the last so ordering is meaningful. owned_with_rating_and_notes() { local jwt="$1" repo="$2" artist="$3" title="$4" media="$5" sleeve="$6" \ folder="$7" counterparty="$8" price_amount="$9" price_currency="${10}" \ cover="${11}" rating="${12}" notes="${13}" release="${14:-}" local resp uri cid subject resp="$(write_genesis "$jwt" "$repo" acquired "$T0" "$artist" "$title" \ "$media" "$sleeve" "" "$folder" "" "$counterparty" "$price_amount" "$price_currency" "$cover" "$release")" uri="$(uri_of "$resp")" cid="$(cid_of "$resp")" subject="$(subject_ref "$uri" "$cid")" write_event "$jwt" "$repo" rated "$T1" "$subject" "$rating" "" "" "" write_event "$jwt" "$repo" annotated "$T2" "$subject" "" "$notes" "" "" } # Writes an owned genesis entry then a regraded follow-up with updated # grades, e.g. after a closer inspection. owned_with_regrade() { local jwt="$1" repo="$2" artist="$3" title="$4" media="$5" sleeve="$6" \ folder="$7" price_amount="$8" price_currency="$9" cover="${10}" \ new_media="${11}" new_sleeve="${12}" release="${13:-}" local resp uri cid subject resp="$(write_genesis "$jwt" "$repo" acquired "$T0" "$artist" "$title" \ "$media" "$sleeve" "" "$folder" "" "" "$price_amount" "$price_currency" "$cover" "$release")" uri="$(uri_of "$resp")" cid="$(cid_of "$resp")" subject="$(subject_ref "$uri" "$cid")" write_event "$jwt" "$repo" regraded "$T1" "$subject" "" "" "$new_media" "$new_sleeve" } # Mints a shared-catalog release. The lexicon calls this "lazy promotion on # first reference", so minting right before the entries that adopt it # matches how the real flow orders things. mint_release() { local jwt="$1" repo="$2" artist="$3" title="$4" year="$5" genre="$6" local resp resp="$(create_record "$jwt" "$repo" dev.mokkenstorm.crate.catalog.release \ "{\"\$type\":\"dev.mokkenstorm.crate.catalog.release\",\"title\":$(json_str "$title"),\"createdAt\":$(json_str "$T0"),\"artistDisplay\":$(json_str "$artist"),\"released\":$(json_str "$year"),\"genres\":[$(json_str "$genre")]}")" catalog_ref "$(uri_of "$resp")" "$(cid_of "$resp")" } # Hand-rolled 300x300 solid PNG (stdlib struct+zlib only, no Pillow) so each # account gets a distinct cover color without shipping binary fixtures. generate_cover() { local out="$1" r="$2" g="$3" b="$4" python3 - "$out" "$r" "$g" "$b" 300 <<'PY' import struct import sys import zlib path, r, g, b, size = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4]), int(sys.argv[5]) def chunk(tag, data): return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) sig = b"\x89PNG\r\n\x1a\n" ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) row = bytes([0]) + bytes([r, g, b]) * size raw = row * size idat = zlib.compress(raw, 9) with open(path, "wb") as f: f.write(sig) f.write(chunk(b"IHDR", ihdr)) f.write(chunk(b"IDAT", idat)) f.write(chunk(b"IEND", b"")) PY } # Best-effort: generates a cover, uploads it, and echoes the blob JSON # fragment verbatim (per com.atproto.repo.uploadBlob's `{blob: {$type, ref: # {$link}, mimeType, size}}` shape). Warns and returns empty on any failure # instead of aborting the whole seed run. make_cover_blob() { local handle="$1" jwt="$2" r="$3" g="$4" b="$5" local png resp blob_json # `mktemp XXXXXX.png` is not a template on BSD/macOS mktemp (the X's # must be trailing); it would return that literal path. `-t` picks a temp # dir and suffixes a real random name; the upload sets content-type # explicitly, so the .png extension was never load-bearing. png="$(mktemp -t crate-cover)" if ! generate_cover "$png" "$r" "$g" "$b" 2>/dev/null; then echo "warning: cover generation failed for $handle, skipping cover" >&2 rm -f "$png" return 0 fi resp="$(curl -sf "$PDS/xrpc/com.atproto.repo.uploadBlob" \ -H "authorization: Bearer $jwt" -H 'content-type: image/png' \ --data-binary "@$png" 2>/dev/null || true)" rm -f "$png" if blob_json="$(printf '%s' "$resp" | python3 -c 'import json, sys; print(json.dumps(json.load(sys.stdin)["blob"]))' 2>/dev/null)"; then echo "$blob_json" return 0 fi echo "warning: cover upload failed for $handle, skipping cover" >&2 } declare -A DID JWT COVER RELEASE for handle in alice.test bob.test carol.test dave.test erin.test; do s="$(session "$handle")" DID[$handle]="$(did_of "$s")" JWT[$handle]="$(jwt_of "$s")" echo "session $handle -> ${DID[$handle]}" done # Asymmetric graph for varied counts: alice is popular, dave follows all, # erin only has followers. follow "${JWT[alice.test]}" "${DID[alice.test]}" "${DID[bob.test]}" follow "${JWT[alice.test]}" "${DID[alice.test]}" "${DID[carol.test]}" follow "${JWT[bob.test]}" "${DID[bob.test]}" "${DID[alice.test]}" follow "${JWT[bob.test]}" "${DID[bob.test]}" "${DID[carol.test]}" follow "${JWT[carol.test]}" "${DID[carol.test]}" "${DID[bob.test]}" follow "${JWT[carol.test]}" "${DID[carol.test]}" "${DID[alice.test]}" follow "${JWT[dave.test]}" "${DID[dave.test]}" "${DID[alice.test]}" follow "${JWT[dave.test]}" "${DID[dave.test]}" "${DID[bob.test]}" follow "${JWT[dave.test]}" "${DID[dave.test]}" "${DID[carol.test]}" follow "${JWT[dave.test]}" "${DID[dave.test]}" "${DID[erin.test]}" echo "follow graph seeded" COVER[alice.test]="$(make_cover_blob alice.test "${JWT[alice.test]}" 196 62 44)" COVER[bob.test]="$(make_cover_blob bob.test "${JWT[bob.test]}" 44 110 196)" COVER[carol.test]="$(make_cover_blob carol.test "${JWT[carol.test]}" 68 163 91)" COVER[dave.test]="$(make_cover_blob dave.test "${JWT[dave.test]}" 196 154 44)" COVER[erin.test]="$(make_cover_blob erin.test "${JWT[erin.test]}" 140 68 196)" echo "cover art seeded" # Small shared catalog: alice mints most of it, bob mints a couple, matching # how a real lazy-promotion catalog fills in from whoever gets there first. RELEASE[R1]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "Alice Coltrane" "Journey in Satchidananda" 1971 jazz)" RELEASE[R2]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "Pharoah Sanders" "Karma" 1969 jazz)" RELEASE[R3]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "Can" "Tago Mago" 1971 krautrock)" RELEASE[R4]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "J Dilla" "Donuts" 2006 hiphop)" RELEASE[R5]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "Stereolab" "Dots and Loops" 1997 electronic)" RELEASE[R6]="$(mint_release "${JWT[alice.test]}" "${DID[alice.test]}" "Arthur Russell" "Calling Out of Context" 2004 ambient)" RELEASE[R7]="$(mint_release "${JWT[bob.test]}" "${DID[bob.test]}" "Faust" "Faust IV" 1973 krautrock)" RELEASE[R8]="$(mint_release "${JWT[bob.test]}" "${DID[bob.test]}" "Broadcast" "Tender Buttons" 2005 electronic)" echo "catalog releases minted" # alice: jazz. Two chained timelines (rated+annotated, then regraded), one # plain owned entry, and a couple of wanted entries. Most owned/wanted rows # adopt a catalog release; the plain owned entry and one wanted stay # snapshot-only so that state is still represented. owned_with_rating_and_notes "${JWT[alice.test]}" "${DID[alice.test]}" \ "Alice Coltrane" "Journey in Satchidananda" NM VG+ jazz "Bleecker Street Records" 4500 USD \ "${COVER[alice.test]}" 5 "Found at a stoop sale; the dead wax is pristine." "${RELEASE[R1]}" owned_with_regrade "${JWT[alice.test]}" "${DID[alice.test]}" \ "Pharoah Sanders" "Karma" VG+ VG jazz 3200 USD "${COVER[alice.test]}" VG VG "${RELEASE[R2]}" owned_entry "${JWT[alice.test]}" "${DID[alice.test]}" \ "John Coltrane" "A Love Supreme" NM NM 5 jazz "Repress, but it hits." "${COVER[alice.test]}" wanted_entry "${JWT[alice.test]}" "${DID[alice.test]}" "Dorothy Ashby" "Afro-Harping" jazz wanted_entry "${JWT[alice.test]}" "${DID[alice.test]}" "Sun Ra" "Space Is the Place" jazz "${RELEASE[R6]}" # bob: krautrock. owned_with_rating_and_notes "${JWT[bob.test]}" "${DID[bob.test]}" \ "Can" "Tago Mago" NM NM krautrock "Record fair, Cologne" 5500 EUR \ "${COVER[bob.test]}" 5 "Side two still turns heads at parties." "${RELEASE[R3]}" owned_with_regrade "${JWT[bob.test]}" "${DID[bob.test]}" \ "Faust" "Faust IV" VG VG+ krautrock "" "" "${COVER[bob.test]}" VG+ VG "${RELEASE[R7]}" owned_entry "${JWT[bob.test]}" "${DID[bob.test]}" \ "Cluster" "Zuckerzeit" G+ G 3 krautrock "Beat to hell but plays fine." "${COVER[bob.test]}" wanted_entry "${JWT[bob.test]}" "${DID[bob.test]}" "Neu!" "Neu! 75" krautrock wanted_entry "${JWT[bob.test]}" "${DID[bob.test]}" "Harmonia" "Deluxe" krautrock "${RELEASE[R8]}" # carol: electronic/pop, plus a subjectConverge entry (see below). owned_with_rating_and_notes "${JWT[carol.test]}" "${DID[carol.test]}" \ "Stereolab" "Dots and Loops" NM NM electronic "" 3800 EUR \ "${COVER[carol.test]}" 5 "Pulled this straight to the top of the pile." "${RELEASE[R5]}" owned_with_regrade "${JWT[carol.test]}" "${DID[carol.test]}" \ "Broadcast" "Tender Buttons" VG+ VG electronic "" "" "${COVER[carol.test]}" VG+ VG+ "${RELEASE[R8]}" owned_entry "${JWT[carol.test]}" "${DID[carol.test]}" \ "Laetitia Sadier" "Onetwothreefour" VG+ VG+ 4 electronic "Solo record, still slept on." "${COVER[carol.test]}" wanted_entry "${JWT[carol.test]}" "${DID[carol.test]}" "Broadcast" "Haha Sound" electronic wanted_entry "${JWT[carol.test]}" "${DID[carol.test]}" "Add N to X" "Add Insult to Injury" electronic "${RELEASE[R7]}" # subjectConverge: bob (above) and carol both adopt Tago Mago within the T0 # window, on two distinct dids, so the feed should collapse them together. owned_entry "${JWT[carol.test]}" "${DID[carol.test]}" \ "Can" "Tago Mago" NM VG+ 4 krautrock "Bought on a whim after bob wouldn't stop talking about it." \ "${COVER[carol.test]}" "${RELEASE[R3]}" # dave: hip-hop, plus an actorBatch burst and an isolated single (see below). owned_with_rating_and_notes "${JWT[dave.test]}" "${DID[dave.test]}" \ "J Dilla" "Donuts" NM NM hiphop "Discogs seller" 6000 USD \ "${COVER[dave.test]}" 5 "Played it front to back the day it arrived." "${RELEASE[R4]}" owned_with_regrade "${JWT[dave.test]}" "${DID[dave.test]}" \ "Madlib" "Shades of Blue" VG+ VG hiphop "" "" "${COVER[dave.test]}" VG+ VG+ owned_entry "${JWT[dave.test]}" "${DID[dave.test]}" \ "MF DOOM" "Operation: Doomsday" VG VG 5 hiphop "Original press, crackly but worth it." "${COVER[dave.test]}" wanted_entry "${JWT[dave.test]}" "${DID[dave.test]}" "MF DOOM" "Madvillainy" hiphop "${RELEASE[R2]}" wanted_entry "${JWT[dave.test]}" "${DID[dave.test]}" "Flying Lotus" "Cosmogramma" hiphop "${RELEASE[R8]}" # actorBatch: feed_skeleton.collapse runs the subjectConverge pass first, so # any release a co-adopter also touches in the T0 window is swept away from # dave before the actor pass ever sees it -- the Madvillainy (R2) and # Cosmogramma (R8) rows above are exactly that: alice also adopts R2 and # bob/carol/erin also adopt R8, so both converge instead of batching. R9 and # R10 are minted here and adopted only by dave, so together with Donuts (R4, # also dave-exclusive) they give dave exactly 3 same-did rows in the T0 # window: >= actor_batch_threshold (2) and < import_count_threshold (5), # which is what actually collapses to one actorBatch card. RELEASE[R9]="$(mint_release "${JWT[dave.test]}" "${DID[dave.test]}" "Quasimoto" "The Unseen" 2000 hiphop)" RELEASE[R10]="$(mint_release "${JWT[dave.test]}" "${DID[dave.test]}" "Danny Brown" "XXX" 2011 hiphop)" owned_entry "${JWT[dave.test]}" "${DID[dave.test]}" \ "Quasimoto" "The Unseen" VG+ VG 4 hiphop "" "${COVER[dave.test]}" "${RELEASE[R9]}" owned_entry "${JWT[dave.test]}" "${DID[dave.test]}" \ "Danny Brown" "XXX" NM VG+ 4 hiphop "" "${COVER[dave.test]}" "${RELEASE[R10]}" # reasonSingle: a crate-digging trip well before the T0 batch window. Every # did's leftover rows get re-clustered on their own timeline, and T_PAST sits # more than 6h from the R4/R9/R10 trio, so this opens a fresh window of one # and reads as dave's own standalone entry rather than folding into the batch. write_genesis "${JWT[dave.test]}" "${DID[dave.test]}" acquired "$T_PAST" \ "Arthur Russell" "Calling Out of Context" VG VG "" ambient "" "" "" "" \ "${COVER[dave.test]}" "${RELEASE[R6]}" > /dev/null # erin: ambient, plus a reasonImport burst (see below). owned_with_rating_and_notes "${JWT[erin.test]}" "${DID[erin.test]}" \ "Arthur Russell" "Calling Out of Context" NM NM ambient "" 4200 USD \ "${COVER[erin.test]}" 5 "Warmer and stranger every time it spins." "${RELEASE[R6]}" owned_with_regrade "${JWT[erin.test]}" "${DID[erin.test]}" \ "Popol Vuh" "Hosianna Mantra" VG+ VG ambient "" "" "${COVER[erin.test]}" VG+ VG+ owned_entry "${JWT[erin.test]}" "${DID[erin.test]}" \ "Julius Eastman" "Femenine" VG VG 4 ambient "Reissue, still gorgeous." "${COVER[erin.test]}" "${RELEASE[R8]}" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Arthur Russell" "World of Echo" ambient wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Harold Budd & Brian Eno" "Ambient 2: The Plateaux of Mirror" ambient # reasonImport: five wanted rows in the same T0 window, on releases minted # right here and adopted only by erin (R11-R15). Reusing the shared catalog # (R1-R8) here would let subjectConverge steal these rows the same way it # steals dave's above, since alice/bob/carol/dave already touch those # releases in this window; dedicated releases mean nothing converges them # away. That leaves erin with 5 same-did rows in one window, clearing # import_count_threshold (5) regardless of actor_batch_threshold (2), so the # feed collapses them into one reasonImport card, shaped like a bulk import. RELEASE[R11]="$(mint_release "${JWT[erin.test]}" "${DID[erin.test]}" "Brian Eno" "Ambient 1: Music for Airports" 1978 ambient)" RELEASE[R12]="$(mint_release "${JWT[erin.test]}" "${DID[erin.test]}" "Grouper" "Ruins" 2014 ambient)" RELEASE[R13]="$(mint_release "${JWT[erin.test]}" "${DID[erin.test]}" "William Basinski" "The Disintegration Loops" 2002 ambient)" RELEASE[R14]="$(mint_release "${JWT[erin.test]}" "${DID[erin.test]}" "Stars of the Lid" "The Tired Sounds of Stars of the Lid" 2001 ambient)" RELEASE[R15]="$(mint_release "${JWT[erin.test]}" "${DID[erin.test]}" "Tim Hecker" "Ravedeath, 1972" 2011 ambient)" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Brian Eno" "Ambient 1: Music for Airports" ambient "${RELEASE[R11]}" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Grouper" "Ruins" ambient "${RELEASE[R12]}" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "William Basinski" "The Disintegration Loops" ambient "${RELEASE[R13]}" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Stars of the Lid" "The Tired Sounds of Stars of the Lid" ambient "${RELEASE[R14]}" wanted_entry "${JWT[erin.test]}" "${DID[erin.test]}" "Tim Hecker" "Ravedeath, 1972" ambient "${RELEASE[R15]}" echo "crate entries seeded"