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 <noreply@anthropic.com>
This commit is contained in:
2026-02-22 12:02:03 -06:00
parent 448be52f4a
commit c9cdec6680
2 changed files with 20 additions and 2 deletions

View File

@@ -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}

View File

@@ -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,