summaryrefslogtreecommitdiff
path: root/Biz
diff options
context:
space:
mode:
Diffstat (limited to 'Biz')
-rw-r--r--Biz/PodcastItLater/Episode.py39
-rw-r--r--Biz/PodcastItLater/Web.py24
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."""