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:
tuxgyverandClaude Sonnet 5 committed 2026-09-20 17:59:32 +02:00
1 parent 53f5fc1148
commit 7810bef8a3
12 files changed
+380 -20

No files matched your search

+65
View File
@@ -61,9 +61,26 @@ CREATE TABLE IF NOT EXISTS watches (
found_on TEXT,
found_title TEXT
);
CREATE TABLE IF NOT EXISTS heard (
id INTEGER PRIMARY KEY AUTOINCREMENT,
heard_at INTEGER NOT NULL,
station_id TEXT NOT NULL,
station TEXT NOT NULL,
raw TEXT NOT NULL,
artist TEXT NOT NULL DEFAULT '',
title TEXT NOT NULL,
method TEXT NOT NULL DEFAULT 'icy'
);
CREATE INDEX IF NOT EXISTS heard_station ON heard(station_id, id);
CREATE INDEX IF NOT EXISTS heard_at ON heard(heard_at);
"""
#: Délai avant qu'une même station puisse rejouer un même titre, pour l'historique.
REPEAT_WINDOW_MS = 10 * 60 * 1000
@dataclass
class Media:
id: str
@@ -228,3 +245,51 @@ class Storage:
def delete_watch(self, watch_id: str) -> None:
self._db.execute("DELETE FROM watches WHERE id = ?", (watch_id,))
self._db.commit()
# --- Historique des écoutes ------------------------------------------
def add_heard(self, *, station_id: str, station: str, raw: str, artist: str, title: str,
method: str = "icy") -> bool:
"""Note un titre entendu, sauf si cette station vient déjà de l'annoncer.
Un balayage revient sur la même station toutes les quelques dizaines de secondes, pour
un morceau de plusieurs minutes : sans garde, chaque passage ajouterait une ligne et la
rotation compterait dix fois ce qui n'a été diffusé qu'une. Certaines stations alternent
en plus le vrai titre et un titre de repli ; on ignore donc tout titre déjà relevé sur la
même station dans les `REPEAT_WINDOW_MS` — une station ne rejoue pas un morceau si tôt.
"""
recent = self._db.execute(
"SELECT raw, heard_at FROM heard WHERE station_id = ? ORDER BY id DESC LIMIT 20",
(station_id,),
).fetchall()
if recent and recent[0]["raw"] == raw:
return False
horizon = now() - REPEAT_WINDOW_MS
if any(r["raw"] == raw and r["heard_at"] > horizon for r in recent):
return False
self._db.execute(
"INSERT INTO heard (heard_at,station_id,station,raw,artist,title,method) "
"VALUES (?,?,?,?,?,?,?)",
(now(), station_id, station, raw, artist, title, method),
)
self._db.commit()
return True
def heard(self, limit: int = 100, query: str | None = None) -> list[dict]:
"""Les dernières remontées, la plus récente d'abord."""
sql = "SELECT * FROM heard"
params: list = []
if query:
sql += " WHERE instr(lower(raw), ?) > 0"
params.append(query.lower())
sql += " ORDER BY id DESC LIMIT ?"
params.append(limit)
return [dict(r) for r in self._db.execute(sql, params)]
def heard_count(self) -> int:
return self._db.execute("SELECT COUNT(*) FROM heard").fetchone()[0]
def prune_heard(self, older_than_ms: int) -> int:
cursor = self._db.execute("DELETE FROM heard WHERE heard_at < ?", (older_than_ms,))
self._db.commit()
return cursor.rowcount