vinylgauntlet/assets/extract_tunes.py
type-two a1f4588deb Tier 2: the Trainspotter's Notebook — real records from discogs_full
assets/extract_tunes.py pulls 523 canon tunes (repress-count ranked, club pool
12"-only) into public/tunes.json across shop/fair/club genre pools; top ~8% of
each chunk flagged grail ("CERTIFIED CLASSIC", gold flash, 2000 pts). Level
clear reveals a real record (artist — title, label, year), the announcer speaks
it, and first IDs land in a persistent localStorage notebook (+1000). Title
screen shows notebook progress. Also: overlay/sub text got backgrounds for
readability, and prompt() is guarded for webviews that lack it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:17:34 +10:00

93 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Extract the trainspotting tune pools from local discogs_full -> public/tunes.json.
Canon = most-pressed masters (repress count across 10.4M releases = mass-owned classic).
The club pool additionally requires the main release to be a 12" — keeps it club wax,
not Madonna albums tagged House. Grail flag = top ~8% of each chunk (certified classics,
gold flash in-game). Null label renders as "White Label", which is exactly right.
Read-only against discogs_full (canonical). release.master_id is indexed; each chunk
query returns in ~1s.
python3 assets/extract_tunes.py
"""
import json
import subprocess
from pathlib import Path
PSQL = "/opt/homebrew/opt/postgresql@16/bin/psql"
OUT = Path(__file__).resolve().parent.parent / "public" / "tunes.json"
# pool -> chunks of (tag, is_genre, year_lo, year_hi, n, twelve_inch_only)
POOLS = {
"shop": [ # RECORD SHOP: disco / soul / funk
("Disco", False, 1968, 1989, 70, False),
("Soul", False, 1962, 1985, 70, False),
("Funk", False, 1968, 1985, 70, False),
],
"fair": [ # RECORD FAIR: hip hop / jazz-funk
("Hip Hop", True, 1979, 2005, 100, False),
("Jazz-Funk", False, 1968, 1995, 70, False),
],
"club": [ # CLUB NIGHT: house / techno / jungle, 12"s only
("House", False, 1985, 2005, 70, True),
("Techno", False, 1985, 2005, 70, True),
("Jungle", False, 1990, 2000, 50, True),
],
}
GRAIL_FRACTION = 0.08 # top of each ranked chunk = certified classic
def q(sql):
r = subprocess.run([PSQL, "-d", "discogs_full", "--csv", "-t", "-c", sql],
capture_output=True, text=True, check=True)
import csv, io
return list(csv.reader(io.StringIO(r.stdout)))
def fetch(tag, is_genre, ylo, yhi, limit, twelve):
join = ("JOIN master_genre mg ON mg.master_id=m.id AND mg.genre=" if is_genre
else "JOIN master_style ms ON ms.master_id=m.id AND ms.style=")
twelve_sql = ("""AND EXISTS (SELECT 1 FROM release_format rf
WHERE rf.release_id=m.main_release AND rf.descriptions LIKE '%12"%')"""
if twelve else "")
rows = q(f"""
SELECT m.title, m.year, COUNT(r.id) AS presses,
(SELECT ma.artist_name FROM master_artist ma WHERE ma.master_id=m.id
ORDER BY ma.position NULLS LAST LIMIT 1),
(SELECT rl.label_name FROM release_label rl WHERE rl.release_id=m.main_release LIMIT 1)
FROM master m {join}'{tag}'
JOIN release r ON r.master_id=m.id
WHERE m.year BETWEEN {ylo} AND {yhi} {twelve_sql}
GROUP BY m.id ORDER BY presses DESC LIMIT {limit}""")
grails = max(1, int(len(rows) * GRAIL_FRACTION))
out = []
for i, r in enumerate(rows):
if len(r) < 5 or not r[0] or not r[3]:
continue
out.append({
"a": r[3], "t": r[0],
"l": r[4] or "White Label",
"y": int(r[1]) if r[1] else None,
**({"g": 1} if i < grails else {}),
})
return out
pools = {}
for pool, chunks in POOLS.items():
seen = set()
tunes = []
for chunk in chunks:
for tune in fetch(*chunk):
key = (tune["a"].lower(), tune["t"].lower())
if key in seen:
continue
seen.add(key)
tunes.append(tune)
pools[pool] = tunes
print(f"{pool}: {len(tunes)} tunes ({sum(1 for t in tunes if t.get('g'))} grails)")
OUT.write_text(json.dumps(pools, ensure_ascii=False, separators=(",", ":")))
print(f"wrote {OUT} ({OUT.stat().st_size // 1024}KB, {sum(len(v) for v in pools.values())} total)")