diff --git a/ingest/radieo/api.py b/ingest/radieo/api.py index 1c0d2e9..4830701 100644 --- a/ingest/radieo/api.py +++ b/ingest/radieo/api.py @@ -26,10 +26,13 @@ def annotate_uri(path: Path, track: Track) -> str: def esc(value: str) -> str: return value.replace("\\", "\\\\").replace('"', '\\"') - return ( - f'annotate:title="{esc(track.title)}",artist="{esc(track.artist)}"' - f":{path}" - ) + fields = [f'title="{esc(track.title)}"', f'artist="{esc(track.artist)}"'] + # Web page the track was pulled from, so the player can link back to the + # source. Only http(s) locators qualify (yt-dlp tracks); a Subsonic song id + # is opaque and points at no public page. + if track.locator.startswith(("http://", "https://")): + fields.append(f'url="{esc(track.locator)}"') + return f'annotate:{",".join(fields)}:{path}' class IngestServer(ThreadingHTTPServer): diff --git a/stream/index.html b/stream/index.html index 05ec908..8c8d6bb 100644 --- a/stream/index.html +++ b/stream/index.html @@ -25,6 +25,8 @@ color: #7d768f; margin-bottom: .5rem; } .title { font-size: 1.55rem; font-weight: 650; line-height: 1.25; word-wrap: break-word; } + .title a { color: inherit; text-decoration: none; transition: color .15s; } + .title a:hover { color: #9b8cff; text-decoration: underline; } .artist { margin-top: .35rem; font-size: 1.05rem; color: #b8b2c8; } audio { width: 100%; margin-top: 2rem; } .share { display: flex; gap: .5rem; margin-top: 1rem; } @@ -56,16 +58,16 @@ color: #7d768f; margin: 0 0 .4rem; font-weight: 600; } .history ul { list-style: none; margin: 0; padding: 0; } .history li { border-top: 1px solid rgba(255,255,255,.06); } - .history li > :not(a) { padding: .45rem 0; } - .history .h-dl { + .history .h-row { display: flex; align-items: center; gap: .6rem; padding: .45rem 0; - text-decoration: none; color: inherit; } - .history .h-dl > div { flex: 1; min-width: 0; } - .history .h-icon { color: #7d768f; font-size: .95rem; transition: color .15s; } - .history .h-dl:hover .h-icon { color: #9b8cff; } - .history .h-dl:hover .h-title { color: #fff; } + .history .h-meta { flex: 1; min-width: 0; } + .history .h-act { color: #7d768f; font-size: .95rem; text-decoration: none; + transition: color .15s; } + .history .h-act:hover { color: #9b8cff; } .history .h-title { color: #e8e4f2; font-size: .92rem; } + .history .h-title a { color: inherit; text-decoration: none; transition: color .15s; } + .history .h-title a:hover { color: #9b8cff; text-decoration: underline; } .history .h-artist { color: #8b849c; font-size: .8rem; margin-top: .1rem; } .history .empty { color: #6b6480; font-size: .85rem; padding: .45rem 0; } .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; @@ -163,7 +165,13 @@ const m = await r.json(); const t = (m.title || "").trim(); const a = (m.artist || "").trim(); - titleEl.textContent = t || "—"; + // Le titre devient un lien vers la page d'origine (yt-dlp) quand elle + // est connue ; sinon simple texte. + const u = (m.url || "").trim(); + const label = t || "—"; + titleEl.innerHTML = u + ? `${escapeHtml(label)}` + : escapeHtml(label); artistEl.textContent = a; document.title = t ? (a ? `${t} — ${a} · ${STATION_NAME}` : `${t} · ${STATION_NAME}`) : STATION_NAME; } catch (e) { /* keep last known values */ } @@ -188,14 +196,18 @@ const t = escapeHtml((m.title || "").trim() || "—"); const a = (m.artist || "").trim(); const artist = a ? `
${escapeHtml(a)}
` : ""; - const meta = `
${t}
${artist}`; - // Lien de téléchargement si le fichier est encore dans le cache. + // Le titre porte le lien vers la page d'origine (yt-dlp) quand elle + // existe ; le téléchargement reste une action distincte à côté. + const u = (m.url || "").trim(); + const titleHtml = u + ? `${t}` + : t; + const meta = `
${titleHtml}
${artist}
`; const f = (m.file || "").trim(); - if (f) { - const href = "/download?file=" + encodeURIComponent(f); - return `
  • ${meta}
  • `; - } - return `
  • ${meta}
  • `; + const dl = f + ? `` + : ""; + return `
  • ${meta}${dl}
  • `; }).join(""); } catch (e) { /* keep last known values */ } } diff --git a/stream/radio.liq b/stream/radio.liq index 9fb6f32..f567eff 100644 --- a/stream/radio.liq +++ b/stream/radio.liq @@ -127,8 +127,8 @@ radio.on_metadata( now_playing := m # `file` : nom de base du fichier à l'antenne, servant de jeton de # téléchargement (/download?file=…). Vide si la métadonnée manque. - entry = {title=m["title"], artist=m["artist"], file=path.basename(m["filename"])} - head = list.hd(default={title="", artist="", file=""}, history()) + entry = {title=m["title"], artist=m["artist"], url=m["url"], file=path.basename(m["filename"])} + head = list.hd(default={title="", artist="", url="", file=""}, history()) is_dup = head.title == entry.title and head.artist == entry.artist if not is_dup and (entry.title != "" or entry.artist != "") then history := list.prefix(history_max, list.add(entry, history())) @@ -159,7 +159,7 @@ harbor.http.register( port=8000, method="GET", "/nowplaying", fun(_, resp) -> begin m = now_playing() - resp.json({title=m["title"], artist=m["artist"]}) + resp.json({title=m["title"], artist=m["artist"], url=m["url"]}) end )