#!/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), ], "warehouse": [ # WAREHOUSE RAVE: acid / minimal / tech house, 12"s only ("Acid House", False, 1987, 1995, 50, True), ("Tech House", False, 1992, 2005, 50, True), ("Minimal", False, 1992, 2005, 50, True), ], "festival": [ # FESTIVAL GROUNDS: trance / big beat / prog, 12"s only ("Trance", False, 1992, 2005, 60, True), ("Big Beat", False, 1994, 2002, 40, True), ("Progressive House", False, 1992, 2005, 50, True), ], "archive": [ # THE ARCHIVE: the deep stuff, 12"s only ("Drum n Bass", False, 1992, 2005, 60, True), ("Hardcore", False, 1990, 1997, 50, True), ("Breakbeat", False, 1988, 2005, 50, True), ("Electro", False, 1982, 2005, 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)")