From c9cdec66806590defded4d3df7e095148d406b9a Mon Sep 17 00:00:00 2001 From: Thomas Hallock Date: Sun, 22 Feb 2026 12:02:03 -0600 Subject: [PATCH] Deduplicate listen events from multiple HA entities When a track plays, multiple HA entities (Cast, WiFi, MA) all fire the automation simultaneously, creating 3x duplicate listen events. Now skips logging if the same track was recorded within the last 60s. Co-Authored-By: Claude Opus 4.6 --- src/haunt_fm/api/history.py | 2 ++ src/haunt_fm/services/history_ingest.py | 20 ++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/haunt_fm/api/history.py b/src/haunt_fm/api/history.py index c493851..2ce2a08 100644 --- a/src/haunt_fm/api/history.py +++ b/src/haunt_fm/api/history.py @@ -36,4 +36,6 @@ async def receive_webhook(payload: WebhookPayload, session: AsyncSession = Depen listened_at=payload.listened_at, raw_payload=payload.model_dump(mode="json"), ) + if event is None: + return {"ok": True, "duplicate": True} return {"ok": True, "track_id": event.track_id, "event_id": event.id} diff --git a/src/haunt_fm/services/history_ingest.py b/src/haunt_fm/services/history_ingest.py index f4f214c..3f66d89 100644 --- a/src/haunt_fm/services/history_ingest.py +++ b/src/haunt_fm/services/history_ingest.py @@ -1,10 +1,13 @@ -from datetime import datetime +import logging +from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from haunt_fm.models.track import ListenEvent, Track +logger = logging.getLogger(__name__) + def make_fingerprint(artist: str, title: str) -> str: return f"{artist.lower().strip()}::{title.lower().strip()}" @@ -47,9 +50,22 @@ async def ingest_listen_event( source: str, listened_at: datetime, raw_payload: dict | None = None, -) -> ListenEvent: +) -> ListenEvent | None: track = await upsert_track(session, title, artist, album) + # Deduplicate: skip if this track was logged within the last 60 seconds. + # Multiple HA entities (Cast, WiFi, MA) fire simultaneously for the same play event. + cutoff = datetime.now(timezone.utc) - timedelta(seconds=60) + recent = await session.execute( + select(ListenEvent) + .where(ListenEvent.track_id == track.id) + .where(ListenEvent.listened_at > cutoff) + .limit(1) + ) + if recent.scalar_one_or_none() is not None: + logger.debug("Skipping duplicate listen event for %s - %s", artist, title) + return None + event = ListenEvent( track_id=track.id, source=source,