diff options
| author | Ben Sima <ben@bsima.me> | 2025-11-16 03:30:44 -0500 |
|---|---|---|
| committer | Ben Sima <ben@bsima.me> | 2025-11-16 03:30:44 -0500 |
| commit | 081f0759b37452bb1319c4f5f88a1d451a5177a9 (patch) | |
| tree | 69457c921a9a82a67a1d19e6b79d713d76e820cc | |
| parent | 3b917a87e12c8ef97bc52afd3903ac22c082f7b1 (diff) | |
Add metrics tracking endpoint and JavaScript for play events
- Added POST /episode/{id}/track endpoint to track play/download
events - Added JavaScript to audio player to track first play event -
JavaScript sends fetch request to tracking endpoint on play - Tracks
user_id if logged in, otherwise anonymous - Added main() function to
Episode.py for test compatibility
Tasks completed: t-gcdFSb, t-gcfTnG
| -rw-r--r-- | Biz/PodcastItLater/Episode.py | 39 | ||||
| -rw-r--r-- | Biz/PodcastItLater/Web.py | 24 |
2 files changed, 63 insertions, 0 deletions
diff --git a/Biz/PodcastItLater/Episode.py b/Biz/PodcastItLater/Episode.py index a516f65..b7c302c 100644 --- a/Biz/PodcastItLater/Episode.py +++ b/Biz/PodcastItLater/Episode.py @@ -9,6 +9,7 @@ share functionality, and signup prompts for non-authenticated users. # : dep ludic import Biz.PodcastItLater.UI as UI import ludic.html as html +import sys import typing from ludic.attrs import Attrs from ludic.components import Component @@ -21,6 +22,7 @@ class EpisodePlayerAttrs(Attrs): audio_url: str title: str + episode_id: int class EpisodePlayer(Component[AnyChildren, EpisodePlayerAttrs]): @@ -29,6 +31,7 @@ class EpisodePlayer(Component[AnyChildren, EpisodePlayerAttrs]): @override def render(self) -> html.div: audio_url = self.attrs["audio_url"] + episode_id = self.attrs["episode_id"] return html.div( html.div( @@ -43,9 +46,38 @@ class EpisodePlayer(Component[AnyChildren, EpisodePlayerAttrs]): "Your browser does not support the audio element.", controls=True, preload="metadata", + id=f"audio-player-{episode_id}", classes=["w-100"], style={"max-width": "100%"}, ), + # JavaScript to track play events + html.script( + f""" + (function() {{ + var player = document.getElementById( + 'audio-player-{episode_id}' + ); + var hasTrackedPlay = false; + + player.addEventListener('play', function() {{ + // Track first play only + if (!hasTrackedPlay) {{ + hasTrackedPlay = true; + + // Send play event to server + fetch('/episode/{episode_id}/track', {{ + method: 'POST', + headers: {{ + 'Content-Type': + 'application/x-www-form-urlencoded' + }}, + body: 'event_type=played' + }}); + }} + }}); + }})(); + """, + ), classes=["card-body"], ), classes=["card", "mb-4"], @@ -304,6 +336,7 @@ class EpisodeDetailPage(Component[AnyChildren, EpisodeDetailPageAttrs]): EpisodePlayer( audio_url=episode["audio_url"], title=episode["title"], + episode_id=episode["id"], ), # Share button ShareButton(share_url=share_url), @@ -354,3 +387,9 @@ class EpisodeDetailPage(Component[AnyChildren, EpisodeDetailPageAttrs]): page_title=page_title, meta_tags=meta_tags, ) + + +def main() -> None: + """Episode module has no tests currently.""" + if "test" in sys.argv: + sys.exit(0) diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py index 7c85e0b..a706eb5 100644 --- a/Biz/PodcastItLater/Web.py +++ b/Biz/PodcastItLater/Web.py @@ -1659,6 +1659,30 @@ def add_episode_to_feed(request: Request, episode_id: int) -> Response: ) +@app.post("/episode/{episode_id}/track") +def track_episode( + request: Request, + episode_id: int, + data: FormData, +) -> Response: + """Track an episode metric event (play, download).""" + # Get event type from form data + event_type_raw = data.get("event_type", "") + event_type = event_type_raw if isinstance(event_type_raw, str) else "" + + # Validate event type + if event_type not in {"played", "downloaded"}: + return Response("Invalid event type", status_code=400) + + # Get user ID if logged in (None for anonymous) + user_id = request.session.get("user_id") + + # Track the event + Core.Database.track_episode_metric(episode_id, event_type, user_id) + + return Response("", status_code=200) + + class BaseWebTest(Test.TestCase): """Base class for web tests with database setup.""" |
