The Live Music Archive pipeline
Bands, shows, nights and tours from archive.org; the legendary night; offline geocoding; streaming songs with taper credits.
Everything playable comes from the Internet Archive's Live Music Archive: about 7,800 band collections and hundreds of thousands of fan-made recordings. archive.py turns that into bands, years, tours and songs, and caches every raw record it fetches.
Bands
The band list is one advanced-search query for every collection inside etree (the Live Music Archive), sorted by downloads, cached for a day:
411def list_bands(data_dir: Path) -> list[dict]: 412 """Every band collection on the Live Music Archive, most downloaded first. 413 414 Cached under ``DATA_DIR/bandlist.json`` for a day: ``[{id, name, items}]``. 415 """ 416 cache = data_dir / "bandlist.json" 417 if cache.exists() and time.time() - cache.stat().st_mtime < MAX_BAND_LIST_AGE: 418 try: 419 return json.loads(cache.read_text())["bands"] 420 except (ValueError, KeyError): 421 pass 422 params = [ 423 ("q", "collection:etree AND mediatype:collection"), 424 ("fl[]", "identifier"), 425 ("fl[]", "title"), 426 ("fl[]", "item_count"), 427 ("sort[]", "downloads desc"), 428 ("rows", "20000"), 429 ("output", "json"), 430 ] 431 data = _get_json("https://archive.org/advancedsearch.php?" + urllib.parse.urlencode(params)) 432 bands = [ 433 {"id": d["identifier"], "name": d.get("title") or d["identifier"], "items": int(d.get("item_count") or 0)} 434 for d in data.get("response", {}).get("docs", []) 435 if d.get("identifier") 436 ] 437 bands = [b for b in bands if b["items"] > 0] 438 cache.parent.mkdir(parents=True, exist_ok=True) 439 cache.write_text(json.dumps({"fetched": time.time(), "bands": bands})) 440 return bands
Shows, nights, tours
load_shows pulls every recording in a band's collection through the archive's paginated scrape API and caches the raw list for 30 days. Many recordings can exist for one night, so concert_nights collapses them to one canonical night each and drops studio sessions and compilations. A year's nights then split into tours wherever there is a gap of more than eleven days:
293def _split_into_tours(nights: list[dict]) -> list[list[dict]]: 294 """Split date-sorted nights into runs, breaking where a long gap appears.""" 295 runs: list[list[dict]] = [] 296 current: list[dict] = [] 297 prev: date_cls | None = None 298 for n in nights: 299 d = date_cls.fromisoformat(n["ymd"]) 300 if prev is not None and (d - prev).days > _TOUR_GAP_DAYS: 301 runs.append(current) 302 current = [] 303 current.append(n) 304 prev = d 305 if current: 306 runs.append(current) 307 return runs
build_tour turns one run into the game's tour shape: it keeps the first night, the last, and the legendary night (the one with the most recordings, which is a good proxy for how celebrated a show is), samples the rest down to twelve stops, and geocodes the miles between them.
Placing towns on the map
City text such as "Alta, WY" is resolved offline against a bundled city index (towns of about 15,000 people and up). A state was named but the town isn't in the index? Rather than accept a foreign namesake, the resolver falls back to the middle of that state:
166def resolve(coverage: str | None) -> Place | None: 167 """Resolve a coverage string to a Place, falling back to the state's centre for 168 a US town too small for the city index.""" 169 place = _resolve_city(coverage) 170 if place is not None or not coverage: 171 return place 172 parts = [p.strip() for p in coverage.split(",")] 173 st = _region_to_us_state(parts[1]) if len(parts) > 1 and parts[1].strip() else None 174 if st and st in _STATE_CENTERS: 175 lat, lon = _STATE_CENTERS[st] 176 return Place(parts[0], lat, lon, st, "US", 0) 177 return None
The songs
show_tracks reads one recording's file list and keeps the MP3 derivatives the archive makes for every upload, in track order, with the taper and transfer credits from its metadata:
554def show_tracks(identifier: str, data_dir: Path) -> dict: 555 """A recording's playable songs plus who taped it, for the in-game player.""" 556 meta = fetch_metadata(identifier, data_dir) 557 md = meta.get("metadata", {}) 558 files = meta.get("files", []) 559 mp3 = [f for f in files if f.get("format") in ("VBR MP3", "MP3", "64Kbps MP3", "128Kbps MP3")] 560 # Prefer the VBR derivatives (one per song); fall back to whatever MP3s exist. 561 vbr = [f for f in mp3 if f.get("format") == "VBR MP3"] 562 picked = vbr or mp3 563 tracks = [] 564 for f in picked: 565 tracks.append( 566 { 567 "title": _first(f.get("title")) or f["name"].rsplit(".", 1)[0], 568 "length": _clock(_first(f.get("length"))), 569 "track": _first(f.get("track")), 570 "url": "https://archive.org/download/" + urllib.parse.quote(identifier) + "/" + urllib.parse.quote(f["name"]), 571 } 572 ) 573 574 def track_key(t: dict): 575 m = re.match(r"\d+", t["track"] or "") 576 return (int(m.group()) if m else 9999, t["url"]) 577 578 if all(t["track"] for t in tracks): 579 tracks.sort(key=track_key) 580 return { 581 "identifier": identifier, 582 "title": _first(md.get("title")), 583 "date": _first(md.get("date"))[:10], 584 "venue": _first(md.get("venue")), 585 "city": _first(md.get("coverage")), 586 "taper": _first(md.get("taper")), 587 "transferer": _first(md.get("transferer")), 588 "source": _first(md.get("source"))[:200], 589 "details": "https://archive.org/details/" + urllib.parse.quote(identifier), 590 "tracks": tracks, 591 }
Those URLs are played straight from archive.org: by the browser's Listen panel, and by the van stereo inside the game (chapter 17).
- Bands, shows and songs all come from archive.org and are cached raw under the data directory.
- A tour is a run of nights with no long gap; its legendary night is the most-recorded one.
- Small US towns land at their state's centre instead of a same-named city abroad.
- Songs stream directly from archive.org, credited to the taper who recorded them.