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
@@ -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
|
||||
Reference in new issue
Block a user