diff --git a/app/shop_routes.py b/app/shop_routes.py index 245101c..0609f85 100644 --- a/app/shop_routes.py +++ b/app/shop_routes.py @@ -146,6 +146,19 @@ async def shop_release(release_id: int, db=Depends(get_db)): return {"release": dict(dr) if dr else None, "tracks": tracks, "copies": copies} +@router.get("/audio/{release_id}") +async def shop_audio(release_id: int, db=Depends(get_db)): + """Playable / embeddable previews for a release — Apple 30s preview, Beatport/Bandcamp embeds, YouTube.""" + a = (await db.execute(text(""" + SELECT apple_preview_url, apple_url, beatport_id, beatport_url, beatport_embed, beatport_genre, + spotify_id, bandcamp_url, bandcamp_embed, soundcloud_url + FROM disc_release_audio WHERE release_id = :r"""), {"r": release_id})).mappings().first() + vids = [dict(r) for r in (await db.execute(text(""" + SELECT title, uri, duration FROM disc_release_video WHERE release_id = :r AND uri <> '' LIMIT 8 + """), {"r": release_id})).mappings()] + return {"audio": dict(a) if a else None, "videos": vids} + + class WantIn(BaseModel): email: str artist: str = "" diff --git a/audio_sync.py b/audio_sync.py new file mode 100644 index 0000000..0a3c933 --- /dev/null +++ b/audio_sync.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Mirror WowPlatter's audio/video link tables → RecordGod Postgres (for kiosk/storefront previews). + + python3 audio_sync.py | ssh -C root@100.94.195.115 \ + 'docker exec -i recordgod-db psql -U recordgod -d recordgod -q -v ON_ERROR_STOP=1' + +disc_release_audio = one wide row per release (apple_preview_url = a direct 30s preview, beatport_embed, +bandcamp, spotify…); disc_release_video = YouTube clips per release/track. All rows mirrored — the JOIN to +disc_release naturally filters to what the shop stocks. Re-run after a fresh Beatport/Apple enrich. +""" +import re +import pathlib +import sys + +import pymysql + +WP_CONFIG = "/opt/homebrew/var/www/monsterrobot.localsite/wp-config.php" +SOCK = "/opt/homebrew/var/mysql/mysql.sock" + +SCHEMA = """ +SET synchronous_commit = off; +DROP TABLE IF EXISTS disc_release_audio, disc_release_video CASCADE; +CREATE TABLE disc_release_audio ( + release_id bigint PRIMARY KEY, apple_url text, apple_preview_url text, + beatport_id text, beatport_url text, beatport_embed text, beatport_genre text, + spotify_id text, tidal_id text, deezer_id text, + bandcamp_url text, bandcamp_embed text, soundcloud_url text, allmusic_url text, local_url text); +CREATE TABLE disc_release_video ( + release_id bigint, track_id text, title text, uri text, embed int, duration int); +""" +INDEXES = "CREATE INDEX ON disc_release_video (release_id);\nANALYZE disc_release_audio;\n" + +A_COLS = ["release_id", "apple_url", "apple_preview_url", "beatport_id", "beatport_url", "beatport_embed", + "beatport_genre", "spotify_id", "tidal_id", "deezer_id", "bandcamp_url", "bandcamp_embed", + "soundcloud_url", "allmusic_url", "local_url"] +V_COLS = ["release_id", "track_id", "title", "uri", "embed", "duration"] + + +def creds(): + t = pathlib.Path(WP_CONFIG).read_text() + g = lambda k: re.search(rf"'{k}',\s*'([^']*)'", t).group(1) + return g("DB_NAME"), g("DB_USER"), g("DB_PASSWORD") + + +def cp(v): + if v is None or v == "": + return r"\N" + return (str(v).replace("\\", "\\\\").replace("\t", "\\t") + .replace("\n", "\\n").replace("\r", "\\r")) + + +def copy_block(out, table, cols, rows): + out.write(f"COPY {table} ({','.join(cols)}) FROM stdin;\n") + for r in rows: + out.write("\t".join(cp(r[c]) for c in cols) + "\n") + out.write("\\.\n\n") + print(f" {table:22} {len(rows):>7,}", file=sys.stderr) + + +def main(): + name, user, pw = creds() + my = pymysql.connect(unix_socket=SOCK, user=user, password=pw, database=name, + cursorclass=pymysql.cursors.DictCursor) + c = my.cursor() + P = "wp_rmp_disc_" + out = sys.stdout + out.write(SCHEMA) + print("emitting audio/video:", file=sys.stderr) + c.execute(f"SELECT {','.join(A_COLS)} FROM {P}release_audio") + copy_block(out, "disc_release_audio", A_COLS, c.fetchall()) + c.execute(f"SELECT {','.join(V_COLS)} FROM {P}release_video WHERE uri IS NOT NULL AND uri <> ''") + copy_block(out, "disc_release_video", V_COLS, c.fetchall()) + out.write(INDEXES) + print("done", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/site/kiosk.html b/site/kiosk.html index 5713171..4a8ff5a 100644 --- a/site/kiosk.html +++ b/site/kiosk.html @@ -63,6 +63,10 @@ #detail .tracks{flex:1;overflow-y:auto;border-top:1px solid var(--line);padding-top:14px} #detail .tk{display:flex;justify-content:space-between;font-size:21px;padding:7px 0;border-bottom:1px solid #1c1c22;color:#d6d6de} #detail .tk .n{color:var(--mut);width:54px} + #dListen{margin-bottom:16px} + .listenBtn{background:var(--primary);color:#11070c;border:0;border-radius:14px;padding:16px 30px;font:700 26px var(--font),system-ui;display:inline-flex;align-items:center;gap:10px;cursor:pointer} + .listenBtn .muted{color:#5a1030;font-weight:500;font-size:18px} + #dListen iframe{width:100%;height:240px;border:0;border-radius:14px} .closeX{position:absolute;top:30px;right:36px;font-size:40px;color:var(--mut);z-index:40;width:70px;height:70px;display:flex;align-items:center;justify-content:center;background:var(--panel);border-radius:50%} .bigprice{font-size:46px;font-weight:800;color:var(--primary)} @@ -101,6 +105,7 @@

+
@@ -130,7 +135,7 @@ async function buildWall(){ /* idle: return to attract after 75s of no touch */ function resetIdle(){ clearTimeout(IDLE); IDLE=setTimeout(goAttract, 75000); } -function goAttract(){ clearTimeout(IDLE); show('attract'); $('#q').value=''; } +function goAttract(){ clearTimeout(IDLE); stopAudio(); show('attract'); $('#q').value=''; } function enterBrowse(){ show('browse'); resetIdle(); load(''); setTimeout(()=>$('#q').focus(),100); } function show(id){ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('on')); $('#'+id).classList.add('on'); } @@ -179,8 +184,26 @@ async function openDetail(rid){ if(c0 && c0.product_url){ qb.style.display='flex'; $('#dQrPrice').textContent=money(c0.price); try{ new QRCode(qel,{text:c0.product_url,width:120,height:120,correctLevel:QRCode.CorrectLevel.M}); }catch(e){ qb.style.display='none'; } } else qb.style.display='none'; + loadAudio(r.id); } -function closeDetail(){ show('browse'); resetIdle(); } +function ytId(uri){ const m=String(uri||'').match(/(?:v=|youtu\.be\/|embed\/)([\w-]{11})/); return m?m[1]:(/^[\w-]{11}$/.test(uri||'')?uri:null); } +async function loadAudio(rid){ + const z=$('#dListen'); z.innerHTML=''; + const d=await fetch('/shop/audio/'+rid).then(r=>r.json()).catch(()=>({})), a=d.audio||{}, v=d.videos||[]; + if(a.apple_preview_url){ + z.innerHTML=``; + } else { const yt=v.map(x=>ytId(x.uri)).find(Boolean); + if(yt) z.innerHTML=``; + else if(a.beatport_embed) z.innerHTML=a.beatport_embed; + else if(a.bandcamp_embed) z.innerHTML=a.bandcamp_embed; + } +} +function resetListenBtn(){ const b=$('#lbtn'); if(b) b.innerHTML='▶ Listen · 30s preview'; } +function togglePreview(url){ const au=$('#kAudio'), b=$('#lbtn'); if(!au) return; + if(au.paused){ if(au.src!==url) au.src=url; au.play(); b.innerHTML='⏸ Pause'; } else { au.pause(); resetListenBtn(); } } +function playYT(id){ $('#dListen').innerHTML=``; } +function stopAudio(){ const a=$('#kAudio'); if(a) a.pause(); document.querySelectorAll('#dListen iframe').forEach(f=>f.remove()); } +function closeDetail(){ stopAudio(); show('browse'); resetIdle(); } boot();