Compare commits

..

5 commits

Author SHA1 Message Date
c9c2218e14 stream: show a random Navidrome-style background image
Some checks failed
continuous-integration/drone/push Build is failing
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:35:21 +08:00
9ca500a3e0 stream: listen on IPv6 as well as IPv4
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:35:21 +08:00
6d2bc963db stream: add a synthwave favicon
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:35:21 +08:00
f065a0eda9 ingest: resolve bandcamp label sources down to individual tracks
A yt-dlp source pointing at a label/artist page flat-extracts to a mix of
/track/ and /album/ URLs. The provider used each verbatim as a locator, so an
/album/ URL was handed to the fetcher as if it were a track: yt-dlp then
(mis)downloaded the whole album into one file and tagged it from the
playlist-level info, which carries the album title and no artist — surfacing as
"Unknown artist" on the stream.

Drill picked container entries down to a single track before emitting a
locator, bounded by a small depth so nested containers (label -> album ->
track) resolve while a real track (which flat-extracts to just itself) is the
base case. Locators are now always downloadable tracks, so the existing
tag/fetch path and anti-repeat keying work as intended.

Also make guess_metadata trust the explicit artist tag over the "Artist -
Title" title split: some label uploads double the artist into the title
("Artist - Artist - Title"), which the blind last-" - " split mis-parsed. When
that artist prefixes the title we peel it off (repeatedly), falling back to the
split only when there is no artist tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:35:21 +08:00
32fae3dbba stream: play a scheduled jingle right after key times of day
Adds special jingle folders (midi, gouter, bisous) that take priority
over the default jingle rotation once per day, briefly after 11h00,
15h00, 16h30 and 21h00.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:35:21 +08:00
2 changed files with 43 additions and 2 deletions

View file

@ -14,6 +14,19 @@
background: radial-gradient(circle at 30% 20%, #2a2140, #0d0b14 70%);
color: #f2f0f7;
}
/* Fond façon écran de connexion Navidrome : une image aléatoire tirée de
leur galerie, posée derrière la carte avec un voile sombre pour garder
le texte lisible. Le dégradé du body reste visible tant que l'image
n'est pas chargée (ou en cas d'échec réseau). */
.bg {
position: fixed; inset: 0; z-index: -1; background-size: cover;
background-position: center; opacity: 0; transition: opacity .8s ease;
}
.bg.loaded { opacity: 1; }
.bg::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(circle at 30% 20%, rgba(20,16,34,.72), rgba(13,11,20,.9) 75%);
}
.card {
width: min(90vw, 420px); padding: 2.5rem 2rem;
background: rgba(255,255,255,.04); border: 1px solid rgba(255,255,255,.08);
@ -82,6 +95,7 @@
</style>
</head>
<body>
<div class="bg" id="bg"></div>
<main class="card">
<div class="logo" id="stationName"></div>
<div class="np-label"><span class="dot"></span><span id="npLabel">Préchargement</span></div>
@ -107,6 +121,33 @@
document.getElementById("stationName").textContent = "◈ " + STATION_NAME;
document.getElementById("pageTitle").textContent = STATION_NAME;
// Fond aléatoire repris de l'écran de connexion Navidrome : on récupère la
// liste de leur galerie (index.yml, CORS ouvert), on tire un nom au hasard
// et on sert la version .webp depuis leur CDN — même logique que Navidrome,
// qui retire l'extension du nom listé. On précharge l'image avant de
// l'afficher pour éviter tout flash, et on ignore silencieusement les
// échecs (le dégradé du body reste alors le fond).
(async () => {
const BASE = "https://www.navidrome.org/images/";
try {
const r = await fetch(BASE + "index.yml", { cache: "no-store" });
const names = (await r.text())
.split("\n")
.map((l) => l.replace(/^\s*-\s*/, "").trim())
.filter(Boolean);
if (!names.length) return;
const name = names[Math.floor(Math.random() * names.length)];
const url = BASE + name.replace(/\.[^.]+$/, "") + ".webp";
const img = new Image();
img.onload = () => {
const bg = document.getElementById("bg");
bg.style.backgroundImage = `url("${url}")`;
bg.classList.add("loaded");
};
img.src = url;
} catch (e) { /* pas de fond : on garde le dégradé */ }
})();
const titleEl = document.getElementById("title");
const artistEl = document.getElementById("artist");
const npLabel = document.getElementById("npLabel");

View file

@ -96,8 +96,8 @@ jingles_midi = playlist(
jingles_gouter = playlist(
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/gouter"
)
jingles_bisous = playlist(
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/bisous"
jingles_moment = playlist(
mode="randomize", reload_mode="watch", check_next=audio_only, "/jingles/moment"
)
# Fenêtre (en minutes) après l'heure cible pendant laquelle le jingle spécial