feat(shop): wire audio/video previews — kiosk Listen (Apple 30s / YouTube / Beatport)
audio_sync.py mirrors WowPlatter disc_release_audio (22.5k; apple_preview_url, beatport_embed,
bandcamp…) + disc_release_video (231k YouTube) into Postgres. /shop/audio/{id} returns playable
sources. Kiosk detail gets a ▶ Listen: Apple 30s preview (instant <audio>), else YouTube embed,
else Beatport/Bandcamp embed; stops on close/idle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bab0826fea
commit
16c85184cd
@ -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 = ""
|
||||
|
||||
78
audio_sync.py
Normal file
78
audio_sync.py
Normal file
@ -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()
|
||||
@ -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)}
|
||||
</style>
|
||||
@ -101,6 +105,7 @@
|
||||
<h1 id="dTitle"></h1>
|
||||
<div class="artist" id="dArtist"></div>
|
||||
<div class="meta" id="dMeta"></div>
|
||||
<div id="dListen"></div>
|
||||
<div class="locate" id="dLocate"></div>
|
||||
<div class="tracks" id="dTracks"></div>
|
||||
</div>
|
||||
@ -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=`<button class="listenBtn" id="lbtn" onclick="togglePreview('${esc(a.apple_preview_url)}')">▶ Listen <span class="muted">· 30s preview</span></button><audio id="kAudio" onended="resetListenBtn()"></audio>`;
|
||||
} else { const yt=v.map(x=>ytId(x.uri)).find(Boolean);
|
||||
if(yt) z.innerHTML=`<button class="listenBtn" onclick="playYT('${yt}')">▶ Watch / listen</button>`;
|
||||
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 <span class="muted">· 30s preview</span>'; }
|
||||
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=`<iframe src="https://www.youtube.com/embed/${id}?autoplay=1&rel=0" allow="autoplay" allowfullscreen></iframe>`; }
|
||||
function stopAudio(){ const a=$('#kAudio'); if(a) a.pause(); document.querySelectorAll('#dListen iframe').forEach(f=>f.remove()); }
|
||||
function closeDetail(){ stopAudio(); show('browse'); resetIdle(); }
|
||||
|
||||
boot();
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user