feat(shop): country-scoped Apple ids in audio — AU-preferred music.apple.com link + clean preview
audio_sync mirrors apple_id ({cc:album_id}) + apple_song_id (kept raw, country preserved).
/shop/audio parses it: AU>US>first → music.apple.com/{cc}/album/{id}, per-track preview array
→ first clean 30s url. Kiosk Listen uses apple_preview + an Apple Music ↗ link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7e9d5028b0
commit
aa56e874ad
@ -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 {"<cc>":"<album_id>"} (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):
|
||||
|
||||
@ -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 {"<cc>": "<album_id>"} (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"]
|
||||
|
||||
|
||||
|
||||
@ -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=`<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>`;
|
||||
const d=await fetch('/shop/audio/'+rid).then(r=>r.json()).catch(()=>({})), v=d.videos||[];
|
||||
let html='';
|
||||
if(d.apple_preview){
|
||||
html=`<button class="listenBtn" id="lbtn" onclick="togglePreview('${esc(d.apple_preview)}')">▶ 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;
|
||||
if(yt) html=`<button class="listenBtn" onclick="playYT('${yt}')">▶ Watch / listen</button>`;
|
||||
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+=` <a href="${esc(d.apple.url)}" target="_blank" style="color:var(--mut);font-size:18px;margin-left:14px"> Apple Music ↗</a>`;
|
||||
z.innerHTML=html;
|
||||
}
|
||||
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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user