Le serveur garde la trace de ce qu'il entend, et repère la rotation (2.4.0)
Le balayage ne servait qu'à trouver un morceau guetté, puis oubliait tout. Il note désormais chaque titre annoncé par les stations, même sans guet en cours : un onglet Écoutes montre l'historique, et la rotation — passages et stations distinctes, par titre et par artiste — sur les 100 dernières remontées. Pour que l'historique soit fiable, un tour va maintenant jusqu'au bout après une trouvaille (l'enregistrement part aussitôt, le début du morceau n'est pas perdu), et la liste des stations est mise en cache une demi-heure au lieu d'interroger l'annuaire à chaque tour. Essayé sur de vraies radios : une station alternait le vrai titre et un titre de repli, ce qui doublait les passages. Un titre déjà relevé sur la même station dans les dix minutes est donc ignoré. Première moitié de la reconnaissance des morceaux ; l'écoute continue et l'empreinte sonore suivront. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
53f5fc1148
commit
7810bef8a3
12 files changed
+380
-20
No files matched your search
@@ -66,6 +66,7 @@
|
||||
.tech { width:100%; border-collapse:collapse; }
|
||||
.tech td { padding:9px 4px; border-bottom:1px solid var(--line); vertical-align:top; }
|
||||
.tech td:first-child { color:var(--muted); width:38%; }
|
||||
h3.section { font-size:15px; margin:20px 0 10px; }
|
||||
.log h4 { margin:0 0 4px; font-size:15px; }
|
||||
</style>
|
||||
</head>
|
||||
@@ -80,6 +81,7 @@
|
||||
<div class="tabs">
|
||||
<button onclick="show('search')" aria-selected="true" id="t-search">🔎 Rechercher</button>
|
||||
<button onclick="show('watch')" aria-selected="false" id="t-watch">📡 Guets</button>
|
||||
<button onclick="show('heard')" aria-selected="false" id="t-heard">📈 Écoutes</button>
|
||||
<button onclick="show('queue')" aria-selected="false" id="t-queue">⏳ File</button>
|
||||
<button onclick="show('library')" aria-selected="false" id="t-library">📚 Bibliothèque</button>
|
||||
<button onclick="show('devices')" aria-selected="false" id="t-devices">📱 Appareils</button>
|
||||
@@ -106,6 +108,24 @@
|
||||
<div id="watches"></div>
|
||||
</section>
|
||||
|
||||
<section id="p-heard" hidden>
|
||||
<div class="row">
|
||||
<select id="rotationWindow" onchange="loadHeard()">
|
||||
<option value="50">50 dernières remontées</option>
|
||||
<option value="100" selected>100 dernières remontées</option>
|
||||
<option value="200">200 dernières remontées</option>
|
||||
<option value="500">500 dernières remontées</option>
|
||||
</select>
|
||||
<input type="text" id="heardQuery" placeholder="Chercher dans l'historique"
|
||||
onkeydown="if(event.key==='Enter')loadHeard()">
|
||||
<button class="ghost" onclick="loadHeard()">Actualiser</button>
|
||||
</div>
|
||||
<h3 class="section">🔥 En rotation</h3>
|
||||
<div id="rotation"></div>
|
||||
<h3 class="section">🕘 Historique</h3>
|
||||
<div id="heard"></div>
|
||||
</section>
|
||||
|
||||
<section id="p-queue" hidden><div id="queue"></div></section>
|
||||
<section id="p-library" hidden><div id="library"></div></section>
|
||||
<section id="p-settings" hidden>
|
||||
@@ -162,13 +182,14 @@ async function api(path, options = {}) {
|
||||
}
|
||||
|
||||
function show(name) {
|
||||
for (const tab of ['search', 'watch', 'queue', 'library', 'devices', 'settings', 'about']) {
|
||||
for (const tab of ['search', 'watch', 'heard', 'queue', 'library', 'devices', 'settings', 'about']) {
|
||||
$('p-' + tab).hidden = tab !== name;
|
||||
$('t-' + tab).setAttribute('aria-selected', String(tab === name));
|
||||
}
|
||||
if (name === 'queue') loadQueue();
|
||||
if (name === 'library') loadLibrary();
|
||||
if (name === 'watch') loadWatches();
|
||||
if (name === 'heard') loadHeard();
|
||||
if (name === 'devices') loadDevices();
|
||||
if (name === 'settings') loadServerInfo();
|
||||
if (name === 'about') loadAbout();
|
||||
@@ -271,7 +292,8 @@ async function loadWatches() {
|
||||
try {
|
||||
const { watches, recording, sweeps, last_sweep: last } = await api('/watches');
|
||||
const scan = last && last.stations
|
||||
? `${sweeps} tour(s) · dernier : ${last.stations} stations, ${last.titles} titres lus`
|
||||
? `${sweeps} tour(s) · dernier : ${last.stations} stations, ${last.titles} titres lus, ` +
|
||||
`${last.heard || 0} nouveau(x) dans l'historique`
|
||||
: 'en attente d\'un guet';
|
||||
$('watches').innerHTML =
|
||||
`<div class="card"><div class="grow"><b>Balayage continu</b><small>${esc(scan)}</small></div>
|
||||
@@ -306,6 +328,44 @@ async function sweep() {
|
||||
loadWatches();
|
||||
}
|
||||
|
||||
const clock = (ms) => new Date(ms).toLocaleString('fr-FR',
|
||||
{ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||||
|
||||
async function loadHeard() {
|
||||
const last = $('rotationWindow').value;
|
||||
const query = $('heardQuery').value.trim();
|
||||
try {
|
||||
const [rot, list] = await Promise.all([
|
||||
api('/rotation?last=' + last + '&top=15'),
|
||||
api('/heard?limit=100' + (query ? '&q=' + encodeURIComponent(query) : '')),
|
||||
]);
|
||||
const label = (t) => t.artist ? `${esc(t.artist)} — ${esc(t.title)}` : esc(t.title);
|
||||
const stations = (n) => n.length > 2 ? `${n.length} stations` : esc(n.join(', '));
|
||||
$('rotation').innerHTML = rot.tracks.length
|
||||
? `<div class="empty" style="padding:0 0 10px;text-align:left">${rot.window} remontée(s) analysée(s)</div>` +
|
||||
rot.tracks.map((t) => `
|
||||
<div class="card"><div class="grow"><b>${label(t)}</b>
|
||||
<small>${stations(t.stations)} · dernier passage ${clock(t.last_heard)}</small></div>
|
||||
<span class="pill ${t.plays > 1 ? 'ok' : ''}">×${t.plays}</span></div>`).join('') +
|
||||
(rot.artists.length ? `<h3 class="section">🎤 Artistes</h3>` + rot.artists.map((a) => `
|
||||
<div class="card"><div class="grow"><b>${esc(a.artist)}</b>
|
||||
<small>${a.tracks} titre(s) · ${a.stations.length} station(s)</small></div>
|
||||
<span class="pill ${a.plays > 1 ? 'ok' : ''}">×${a.plays}</span></div>`).join('') : '')
|
||||
: `<div class="empty">${list.history
|
||||
? 'Rien d\'entendu pour l\'instant — le premier tour de balayage peut prendre une minute.'
|
||||
: 'L\'historique est désactivé (NOXELY_HISTORY=0).'}</div>`;
|
||||
$('heard').innerHTML = list.items.length
|
||||
? `<div class="empty" style="padding:0 0 10px;text-align:left">${list.total} ligne(s) au total</div>` +
|
||||
list.items.map((h) => `
|
||||
<div class="card"><div class="grow"><b>${h.artist ? esc(h.artist) + ' — ' : ''}${esc(h.title)}</b>
|
||||
<small>${esc(h.station)} · ${clock(h.heard_at)}</small></div></div>`).join('')
|
||||
: '<div class="empty">Aucun résultat</div>';
|
||||
} catch (e) {
|
||||
$('rotation').innerHTML = `<div class="empty">${esc(String(e))}</div>`;
|
||||
$('heard').innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadServerInfo() {
|
||||
try {
|
||||
const health = await fetch('/api/health').then((r) => r.json());
|
||||
|
||||
Reference in new issue
Block a user