diff --git a/app/shop_routes.py b/app/shop_routes.py index 60bf419..cab5d4c 100644 --- a/app/shop_routes.py +++ b/app/shop_routes.py @@ -146,24 +146,52 @@ async def shop_release(release_id: int, db=Depends(get_db)): return {"release": dict(dr) if dr else None, "tracks": tracks, "copies": copies} +def _apple_id(raw): + """apple_id is country-scoped {"":""} (some rows double-JSON-encoded). Prefer AU + (this is an AU store), then US, then whatever country is present → a real music.apple.com link.""" + if not raw: + return None + try: + v = json.loads(raw) + if isinstance(v, str): + v = json.loads(v) + except (ValueError, TypeError): + return None + if not isinstance(v, dict) or not v: + return None + cc = "au" if "au" in v else ("us" if "us" in v else next(iter(v))) + aid = v.get(cc) + return {"country": cc, "album_id": aid, + "url": f"https://music.apple.com/{cc}/album/{aid}" if aid else None} + + @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.""" + """Playable / embeddable previews — Apple 30s preview + country-scoped Apple link, Beatport/Bandcamp, 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 + SELECT apple_id, apple_url, apple_preview_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() - audio = dict(a) if a else None - if audio and isinstance(audio.get("apple_preview_url"), str) and audio["apple_preview_url"].lstrip().startswith("["): - try: - arr = json.loads(audio["apple_preview_url"]) - audio["apple_preview_url"] = next((u for u in arr if u), None) - except (ValueError, TypeError): - audio["apple_preview_url"] = None 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": audio, "videos": vids} + if not a: + return {"apple": None, "apple_preview": None, "videos": vids} + pv = a["apple_preview_url"] + previews = [] + if isinstance(pv, str) and pv.lstrip().startswith("["): + try: + previews = [u for u in json.loads(pv) if u] + except (ValueError, TypeError): + previews = [] + elif pv: + previews = [pv] + return {"apple": _apple_id(a["apple_id"]), "apple_url": a["apple_url"], + "apple_preview": previews[0] if previews else None, "apple_previews": previews, + "beatport_url": a["beatport_url"], "beatport_embed": a["beatport_embed"], + "beatport_id": a["beatport_id"], "bandcamp_url": a["bandcamp_url"], + "bandcamp_embed": a["bandcamp_embed"], "soundcloud_url": a["soundcloud_url"], + "spotify_id": a["spotify_id"], "videos": vids} class WantIn(BaseModel): diff --git a/audio_sync.py b/audio_sync.py index 0a3c933..46c3acf 100644 --- a/audio_sync.py +++ b/audio_sync.py @@ -21,7 +21,7 @@ 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, + release_id bigint PRIMARY KEY, apple_id text, apple_song_id text, 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); @@ -30,9 +30,11 @@ CREATE TABLE disc_release_video ( """ 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"] +# apple_id = country-scoped {"": ""} (essential: country + the id; single-vs-album = the +# Discogs release itself, via format). apple_song_id = song-level. Both kept RAW so nothing's lost. +A_COLS = ["release_id", "apple_id", "apple_song_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"] diff --git a/site/kiosk.html b/site/kiosk.html index 4a8ff5a..dfffe44 100644 --- a/site/kiosk.html +++ b/site/kiosk.html @@ -189,14 +189,17 @@ async function openDetail(rid){ 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=``; + const d=await fetch('/shop/audio/'+rid).then(r=>r.json()).catch(()=>({})), v=d.videos||[]; + let html=''; + if(d.apple_preview){ + html=``; } 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; + if(yt) html=``; + else if(d.beatport_embed) html=d.beatport_embed; + else if(d.bandcamp_embed) html=d.bandcamp_embed; } + if(d.apple && d.apple.url) html+=` Apple Music ↗`; + z.innerHTML=html; } function resetListenBtn(){ const b=$('#lbtn'); if(b) b.innerHTML='▶ Listen · 30s preview'; } function togglePreview(url){ const au=$('#kAudio'), b=$('#lbtn'); if(!au) return;