#!/usr/bin/env python3
"""Reusable: build a keyboard-driven track-triage UI from the SoundCloud source cache.
Regenerate anytime: python3 build_triage_ui.py  ->  triage.html"""
import json, re, unicodedata
SRC="sources"
def norm(s):
    s=(s or "").lower()
    s=''.join(c for c in unicodedata.normalize('NFKD',s) if not unicodedata.combining(c))
    s=re.sub(r'\(.*?\)','',s); s=re.sub(r'\[.*?\]','',s)
    s=re.sub(r'(feat|ft|version|remix|instrumental|toma|take|wip|live).*','',s)
    s=re.sub(r'[^a-z0-9]+',' ',s).strip()
    return s
dsp=[r["trackName"] for r in json.load(open(f"{SRC}/itunes_songs.json"))["results"] if r.get("wrapperType")=="track"]
dsp+=[a["title"] for a in json.load(open(f"{SRC}/deezer_albums.json"))["data"]]
dn={norm(t) for t in dsp if norm(t)}
def on(t):
    n=norm(t); return n in dn or any(n and (n in d or d in n) for d in dn)
REMIX=re.compile(r'\b(remix|cover|\bx\b|mason|princess superstar|cardi|wap)\b',re.I)
VOICE=re.compile(r'(macron|philippe|oudea|castera|cast[ée]ra)',re.I)
TEST =re.compile(r'(wip|toma uno|\bdemo\b|prototype|premi[eè]re prise|take five|beta|\btest\b)',re.I)
sc=[json.loads(l) for l in open(f"{SRC}/soundcloud.jsonl") if l.strip()]
tracks=[]
for t in sc:
    title=t.get("title",""); dur=t.get("duration") or 0
    if on(title): a="released"
    elif dur>=720: a="set"
    elif VOICE.search(title): a="voice"
    elif REMIX.search(title): a="remix"
    elif TEST.search(title): a="test"
    else: a=""
    tracks.append({"t":title,"u":t.get("webpage_url") or t.get("url") or "",
                   "y":(t.get("upload_date") or "")[:4] or "?","p":t.get("view_count") or 0,
                   "m":round(dur/60,1),"a":a})
# Triage UNIQUE tracks, not whole sets — sets are containers, split separately (see task #18).
SETS=[t for t in tracks if t["a"]=="set"]
tracks=[t for t in tracks if t["a"]!="set"]
tracks.sort(key=lambda x:-x["p"])
print(f"(excluded {len(SETS)} live-sets — they get split into tracks separately)")

TPL=r'''<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>L'Armada · Track Triage</title>
<style>
:root{--abyss:#06121c;--deep:#0b2233;--ink:#dff1f5;--mute:#8fb3c0;--line:#1d4358;--foam:#7fe3d4}
*{box-sizing:border-box}body{margin:0;font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:var(--ink);background:linear-gradient(180deg,var(--abyss),#02161f);height:100vh;overflow:hidden}
.app{display:grid;grid-template-columns:320px 1fr;height:100vh}
.side{border-right:1px solid var(--line);overflow:auto;padding:10px}
.side h1{font-size:14px;letter-spacing:1px;color:var(--foam);margin:6px 8px 4px}
.prog{font-size:12px;color:var(--mute);margin:0 8px 8px}
.bar{height:6px;background:#0a2c3c;border-radius:4px;margin:0 8px 10px;overflow:hidden}
.bar i{display:block;height:100%;background:var(--foam);transition:width .3s}
.row{display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:7px;cursor:pointer;font-size:13px}
.row:hover{background:#0e2a3a}.row.cur{background:#12384c;outline:1px solid var(--foam)}
.dot{width:9px;height:9px;border-radius:50%;flex:0 0 auto;background:#26485a}
.row .ti{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
.row .yr{color:var(--mute);font-size:11px}
.main{display:flex;flex-direction:column;padding:22px 26px;overflow:auto}
.crumbs{color:var(--mute);font-size:12px}
.title{font-size:26px;font-weight:700;margin:4px 0 2px}
.meta{color:var(--mute);font-size:13px;margin-bottom:12px}
.auto{display:inline-block;font-size:11px;border:1px solid var(--line);border-radius:6px;padding:1px 7px;color:var(--mute);margin-left:8px}
iframe{width:100%;height:120px;border:0;border-radius:10px;background:#0a2c3c;margin-bottom:16px}
.tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px;max-width:760px}
.tier{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:10px;padding:9px 11px;cursor:pointer;background:var(--deep);transition:.12s}
.tier:hover{border-color:var(--foam)}.tier.sel{outline:2px solid var(--foam);background:#103246}
.kbd{font-weight:800;width:22px;height:22px;border-radius:5px;display:grid;place-items:center;background:#0a2c3c;color:var(--ink);font-size:12px;flex:0 0 auto}
.tier .lab{font-size:13px}.tier .ds{font-size:11px;color:var(--mute)}
.note{margin-top:14px;max-width:760px}
.note input{width:100%;background:#0a2c3c;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:9px 11px;font:inherit}
.hint{color:var(--mute);font-size:12px;margin-top:14px;max-width:760px}
.hint kbd{background:#0a2c3c;border:1px solid var(--line);border-radius:4px;padding:0 5px}
.tools{margin-top:auto;padding-top:16px;display:flex;gap:10px}
button{background:transparent;border:1px solid var(--line);color:var(--foam);border-radius:8px;padding:7px 13px;cursor:pointer;font:inherit}
button:hover{border-color:var(--foam)}
</style></head><body>
<div class="app">
  <div class="side">
    <h1>⛵ TRACK TRIAGE</h1>
    <div class="prog" id="prog"></div><div class="bar"><i id="barfill"></i></div>
    <div id="list"></div>
  </div>
  <div class="main">
    <div class="crumbs" id="crumbs"></div>
    <div class="title" id="title"></div>
    <div class="meta" id="meta"></div>
    <iframe id="player" allow="autoplay"></iframe>
    <div class="tiers" id="tiers"></div>
    <div class="note"><input id="note" placeholder="note (optional) — what's sampled, who's feat., etc."></div>
    <div class="hint">Keys: <kbd>←</kbd><kbd>→</kbd> navigate · letter = tag &amp; advance · <kbd>Space</kbd> play/pause · <kbd>n</kbd> note · <kbd>Backspace</kbd> clear · auto-tags are dim until you confirm.</div>
    <div class="tools">
      <button onclick="expJSON()">⬇ Export JSON</button>
      <button onclick="expCSV()">⬇ Export CSV</button>
      <button onclick="if(confirm('Reset all annotations?')){localStorage.removeItem(KEY);location.reload()}">Reset</button>
    </div>
  </div>
</div>
<script src="https://w.soundcloud.com/player/api.js"></script>
<script>
const TRACKS=/*__DATA__*/;
const TIERS=[
 {k:'o',id:'original',lab:'Original',c:'#46d39a',ds:'✅ release'},
 {k:'l',id:'light',lab:'Light sampling',c:'#7fe3d4',ds:'✅ low-risk'},
 {k:'h',id:'heavy',lab:'Heavy sampling',c:'#ffc24d',ds:'🟡 clear first'},
 {k:'r',id:'remix',lab:'Remix / Cover',c:'#ff7a6b',ds:'🔴 live-only'},
 {k:'v',id:'voice',lab:'Voice / Satire',c:'#ff6b6b',ds:'🔴 live-only'},
 {k:'s',id:'set',lab:'Live-set',c:'#8fb3c0',ds:'🔴 live-only'},
 {k:'t',id:'test',lab:'Test / Sketch',c:'#6a8290',ds:'— shelf'},
 {k:'x',id:'released',lab:'Released',c:'#4a6b7a',ds:'skip'},
];
const TBK=Object.fromEntries(TIERS.map(t=>[t.k,t.id]));
const TBI=Object.fromEntries(TIERS.map(t=>[t.id,t]));
const KEY='armada_triage_v1';
let state=JSON.parse(localStorage.getItem(KEY)||'null');
if(!state){state={};TRACKS.forEach(t=>{if(t.a)state[t.u]={tier:t.a,note:'',auto:true};});}
let i=0,widget=null;
const $=id=>document.getElementById(id);
function save(){localStorage.setItem(KEY,JSON.stringify(state));}
function done(){return TRACKS.filter(t=>state[t.u]&&!state[t.u].auto).length;}
function loadPlayer(u){
  const f=$('player');
  f.src='https://w.soundcloud.com/player/?url='+encodeURIComponent(u)+'&color=%2339c2b0&auto_play=false&show_comments=false&visual=false&buying=false&sharing=false&download=false';
  try{widget=SC.Widget(f);}catch(e){widget=null;}
}
function renderList(){
  const L=$('list');L.innerHTML='';
  TRACKS.forEach((t,n)=>{
    const a=state[t.u];const col=a?TBI[a.tier].c:'#26485a';
    const d=document.createElement('div');d.className='row'+(n===i?' cur':'');
    d.innerHTML='<span class="dot" style="background:'+col+(a&&a.auto?';opacity:.45':'')+'"></span><span class="ti">'+t.t+'</span><span class="yr">'+t.y+'</span>';
    d.onclick=()=>{i=n;render();};L.appendChild(d);
  });
}
function render(){
  const t=TRACKS[i];const a=state[t.u];
  $('crumbs').textContent=(i+1)+' / '+TRACKS.length+(t.a?'   · auto-guess: '+TBI[t.a].lab:'');
  $('title').textContent=t.t;
  $('meta').innerHTML=t.y+' · '+t.p+' plays · '+t.m+' min'+(a?' <span class="auto">'+(a.auto?'auto: ':'you: ')+TBI[a.tier].lab+'</span>':'');
  const T=$('tiers');T.innerHTML='';
  TIERS.forEach(tr=>{
    const sel=a&&a.tier===tr.id;
    const e=document.createElement('div');e.className='tier'+(sel?' sel':'');
    e.style.borderLeft='4px solid '+tr.c;
    e.innerHTML='<span class="kbd">'+tr.k.toUpperCase()+'</span><span><div class="lab">'+tr.lab+'</div><div class="ds">'+tr.ds+'</div></span>';
    e.onclick=()=>tag(tr.id);T.appendChild(e);
  });
  $('note').value=a?a.note||'':'';
  const d=done();$('prog').textContent=d+' / '+TRACKS.length+' confirmed ('+(TRACKS.length-d)+' to go)';
  $('barfill').style.width=(100*d/TRACKS.length)+'%';
  loadPlayer(t.u);renderList();
  document.querySelector('.row.cur')?.scrollIntoView({block:'nearest'});
}
function tag(id){const t=TRACKS[i];state[t.u]={tier:id,note:($('note').value||''),auto:false};save();nextTodo();}
function nextTodo(){let j=i;for(let n=1;n<=TRACKS.length;n++){const k=(i+n)%TRACKS.length;const a=state[TRACKS[k].u];if(!a||a.auto){j=k;break;}}i=(j===i)?Math.min(i+1,TRACKS.length-1):j;render();}
function go(d){i=Math.max(0,Math.min(TRACKS.length-1,i+d));render();}
document.addEventListener('keydown',e=>{
  if(e.target.id==='note'){if(e.key==='Enter'||e.key==='Escape'){state[TRACKS[i].u]=state[TRACKS[i].u]||{tier:'',auto:false};state[TRACKS[i].u].note=$('note').value;save();e.target.blur();}return;}
  if(e.key==='ArrowRight'){go(1);e.preventDefault();}
  else if(e.key==='ArrowLeft'){go(-1);e.preventDefault();}
  else if(e.key===' '){if(widget)widget.toggle();e.preventDefault();}
  else if(e.key==='n'){$('note').focus();e.preventDefault();}
  else if(e.key==='Backspace'){delete state[TRACKS[i].u];save();render();e.preventDefault();}
  else if(TBK[e.key.toLowerCase()]){tag(TBK[e.key.toLowerCase()]);e.preventDefault();}
});
function dl(name,txt,type){const b=new Blob([txt],{type});const u=URL.createObjectURL(b);const a=document.createElement('a');a.href=u;a.download=name;a.click();URL.revokeObjectURL(u);}
function expJSON(){const o=TRACKS.map(t=>({title:t.t,url:t.u,year:t.y,plays:t.p,min:t.m,tier:state[t.u]?.tier||'',dsp:state[t.u]?TBI[state[t.u].tier].ds:'',confirmed:state[t.u]?!state[t.u].auto:false,note:state[t.u]?.note||''}));dl('triage-export.json',JSON.stringify(o,null,2),'application/json');}
function expCSV(){const rows=[['title','url','tier','dsp','confirmed','note']];TRACKS.forEach(t=>{const a=state[t.u];rows.push([t.t,t.u,a?.tier||'',a?TBI[a.tier].ds:'',a?(!a.auto):'',a?.note||''].map(x=>'"'+String(x).replace(/"/g,'""')+'"'));});dl('triage-export.csv',rows.map(r=>r.join(',')).join('\n'),'text/csv');}
render();
</script></body></html>'''
open("triage.html","w").write(TPL.replace("/*__DATA__*/", json.dumps(tracks, ensure_ascii=False)))
print("triage.html written:", len(tracks), "tracks")
auto=sum(1 for t in tracks if t["a"])
print(f"pre-tagged (auto): {auto}  |  to classify by ear: {len(tracks)-auto}")
