summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.tasks/tasks.jsonl4
-rw-r--r--Biz/PodcastItLater/Admin.py291
-rw-r--r--Biz/PodcastItLater/Core.py109
-rw-r--r--Biz/PodcastItLater/UI.py14
-rw-r--r--Biz/PodcastItLater/Web.py132
5 files changed, 542 insertions, 8 deletions
diff --git a/.tasks/tasks.jsonl b/.tasks/tasks.jsonl
index 132d31a..f505eff 100644
--- a/.tasks/tasks.jsonl
+++ b/.tasks/tasks.jsonl
@@ -85,7 +85,7 @@
{"taskCreatedAt":"2025-11-14T18:19:45.999699368Z","taskDependencies":[],"taskId":"t-1a1EwRH","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Test in emacs and narrow terminals","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:18.766470937Z"}
{"taskCreatedAt":"2025-11-14T18:19:46.028016768Z","taskDependencies":[],"taskId":"t-1a1EEer","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Handle edge cases and polish UX","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:18.827147429Z"}
{"taskCreatedAt":"2025-11-14T18:19:46.056655181Z","taskDependencies":[],"taskId":"t-1a1ELGl","taskNamespace":null,"taskParent":"t-19ZF6A8","taskStatus":"Done","taskTitle":"Update documentation","taskType":"WorkTask","taskUpdatedAt":"2025-11-14T19:02:47.319855049Z"}
-{"taskCreatedAt":"2025-11-16T04:06:48.014952363Z","taskDependencies":[],"taskId":"t-ga8V8O","taskNamespace":null,"taskParent":null,"taskStatus":"Open","taskTitle":"PodcastItLater: Public Feed, Metrics & Audio Improvements","taskType":"Epic","taskUpdatedAt":"2025-11-16T04:06:48.014952363Z"}
+{"taskCreatedAt":"2025-11-16T04:06:48.014952363Z","taskDependencies":[],"taskId":"t-ga8V8O","taskNamespace":null,"taskParent":null,"taskStatus":"Done","taskTitle":"PodcastItLater: Public Feed, Metrics & Audio Improvements","taskType":"Epic","taskUpdatedAt":"2025-11-16T08:57:42.45932002Z"}
{"taskCreatedAt":"2025-11-16T04:06:57.071621037Z","taskDependencies":[],"taskId":"t-gaKVc7","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Add database migrations for new columns (is_public, user_episodes table, episode_metrics table, original_url_hash)","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:27.336080901Z"}
{"taskCreatedAt":"2025-11-16T04:06:57.609993104Z","taskDependencies":[],"taskId":"t-gaNbfx","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Implement URL hashing and normalization function for episode deduplication","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:27.896576613Z"}
{"taskCreatedAt":"2025-11-16T04:06:58.132246645Z","taskDependencies":[],"taskId":"t-gaPn6Z","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Add Core.py database functions for public episodes (mark_public, unmark_public, get_public_episodes)","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:09:28.463907822Z"}
@@ -109,4 +109,4 @@
{"taskCreatedAt":"2025-11-16T04:07:35.447349966Z","taskDependencies":[],"taskId":"t-gdlWtu","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Write tests for episode deduplication","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.461656748Z"}
{"taskCreatedAt":"2025-11-16T04:07:35.995113703Z","taskDependencies":[],"taskId":"t-gdoeYo","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Write tests for metrics tracking","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.513956262Z"}
{"taskCreatedAt":"2025-11-16T04:07:36.52315156Z","taskDependencies":[],"taskId":"t-gdqsl7","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Write tests for audio intro/outro generation","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:46:43.574397661Z"}
-{"taskCreatedAt":"2025-11-16T04:07:37.059671738Z","taskDependencies":[],"taskId":"t-gdsHUA","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Open","taskTitle":"Create admin metrics dashboard view","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T04:07:37.059671738Z"}
+{"taskCreatedAt":"2025-11-16T04:07:37.059671738Z","taskDependencies":[],"taskId":"t-gdsHUA","taskNamespace":"Biz/PodcastItLater.hs","taskParent":"t-ga8V8O","taskStatus":"Done","taskTitle":"Create admin metrics dashboard view","taskType":"WorkTask","taskUpdatedAt":"2025-11-16T08:57:35.681938898Z"}
diff --git a/Biz/PodcastItLater/Admin.py b/Biz/PodcastItLater/Admin.py
index 8e12fc7..10a8e58 100644
--- a/Biz/PodcastItLater/Admin.py
+++ b/Biz/PodcastItLater/Admin.py
@@ -31,6 +31,270 @@ from ludic.web.responses import Response
from typing import override
+class MetricsAttrs(Attrs):
+ """Attributes for Metrics component."""
+
+ metrics: dict[str, typing.Any]
+ user: dict[str, typing.Any] | None
+
+
+class MetricCardAttrs(Attrs):
+ """Attributes for MetricCard component."""
+
+ title: str
+ value: int
+ icon: str
+
+
+class MetricCard(Component[AnyChildren, MetricCardAttrs]):
+ """Display a single metric card."""
+
+ @override
+ def render(self) -> html.div:
+ title = self.attrs["title"]
+ value = self.attrs["value"]
+ icon = self.attrs.get("icon", "bi-bar-chart")
+
+ return html.div(
+ html.div(
+ html.div(
+ html.i(classes=["bi", icon, "text-primary", "fs-2"]),
+ classes=["col-auto"],
+ ),
+ html.div(
+ html.h6(title, classes=["text-muted", "mb-1"]),
+ html.h3(str(value), classes=["mb-0"]),
+ classes=["col"],
+ ),
+ classes=["row", "align-items-center"],
+ ),
+ classes=["card-body"],
+ )
+
+
+class TopEpisodesTableAttrs(Attrs):
+ """Attributes for TopEpisodesTable component."""
+
+ episodes: list[dict[str, typing.Any]]
+ metric_name: str
+ count_key: str
+
+
+class TopEpisodesTable(Component[AnyChildren, TopEpisodesTableAttrs]):
+ """Display a table of top episodes by a metric."""
+
+ @override
+ def render(self) -> html.div:
+ episodes = self.attrs["episodes"]
+ metric_name = self.attrs["metric_name"]
+ count_key = self.attrs["count_key"]
+
+ if not episodes:
+ return html.div(
+ html.p(
+ "No data yet",
+ classes=["text-muted", "text-center", "py-3"],
+ ),
+ classes=["card-body"],
+ )
+
+ return html.div(
+ html.div(
+ html.table(
+ html.thead(
+ html.tr(
+ html.th("#", classes=["text-muted"]),
+ html.th("Title"),
+ html.th("Author", classes=["text-muted"]),
+ html.th(
+ metric_name,
+ classes=["text-end", "text-muted"],
+ ),
+ ),
+ classes=["table-light"],
+ ),
+ html.tbody(
+ *[
+ html.tr(
+ html.td(
+ str(idx + 1),
+ classes=["text-muted"],
+ ),
+ html.td(
+ TruncatedText(
+ text=episode["title"],
+ max_length=Core.TITLE_TRUNCATE_LENGTH,
+ ),
+ ),
+ html.td(
+ episode.get("author") or "-",
+ classes=["text-muted"],
+ ),
+ html.td(
+ str(episode[count_key]),
+ classes=["text-end"],
+ ),
+ )
+ for idx, episode in enumerate(episodes)
+ ],
+ ),
+ classes=["table", "table-hover", "mb-0"],
+ ),
+ classes=["table-responsive"],
+ ),
+ classes=["card-body", "p-0"],
+ )
+
+
+class MetricsDashboard(Component[AnyChildren, MetricsAttrs]):
+ """Admin metrics dashboard showing aggregate statistics."""
+
+ @override
+ def render(self) -> UI.PageLayout:
+ metrics = self.attrs["metrics"]
+ user = self.attrs.get("user")
+
+ return UI.PageLayout(
+ html.div(
+ html.h2(
+ html.i(classes=["bi", "bi-graph-up", "me-2"]),
+ "Episode Metrics",
+ classes=["mb-4"],
+ ),
+ # Summary cards
+ html.div(
+ html.div(
+ html.div(
+ MetricCard(
+ title="Total Episodes",
+ value=metrics["total_episodes"],
+ icon="bi-collection",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-md-3"],
+ ),
+ html.div(
+ html.div(
+ MetricCard(
+ title="Total Plays",
+ value=metrics["total_plays"],
+ icon="bi-play-circle",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-md-3"],
+ ),
+ html.div(
+ html.div(
+ MetricCard(
+ title="Total Downloads",
+ value=metrics["total_downloads"],
+ icon="bi-download",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-md-3"],
+ ),
+ html.div(
+ html.div(
+ MetricCard(
+ title="Total Adds",
+ value=metrics["total_adds"],
+ icon="bi-plus-circle",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-md-3"],
+ ),
+ classes=["row", "g-3", "mb-4"],
+ ),
+ # Top episodes tables
+ html.div(
+ html.div(
+ html.div(
+ html.div(
+ html.h5(
+ html.i(
+ classes=[
+ "bi",
+ "bi-play-circle-fill",
+ "me-2",
+ ],
+ ),
+ "Most Played",
+ classes=["card-title", "mb-0"],
+ ),
+ classes=["card-header", "bg-white"],
+ ),
+ TopEpisodesTable(
+ episodes=metrics["most_played"],
+ metric_name="Plays",
+ count_key="play_count",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-lg-4"],
+ ),
+ html.div(
+ html.div(
+ html.div(
+ html.h5(
+ html.i(
+ classes=[
+ "bi",
+ "bi-download",
+ "me-2",
+ ],
+ ),
+ "Most Downloaded",
+ classes=["card-title", "mb-0"],
+ ),
+ classes=["card-header", "bg-white"],
+ ),
+ TopEpisodesTable(
+ episodes=metrics["most_downloaded"],
+ metric_name="Downloads",
+ count_key="download_count",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-lg-4"],
+ ),
+ html.div(
+ html.div(
+ html.div(
+ html.h5(
+ html.i(
+ classes=[
+ "bi",
+ "bi-plus-circle-fill",
+ "me-2",
+ ],
+ ),
+ "Most Added to Feeds",
+ classes=["card-title", "mb-0"],
+ ),
+ classes=["card-header", "bg-white"],
+ ),
+ TopEpisodesTable(
+ episodes=metrics["most_added"],
+ metric_name="Adds",
+ count_key="add_count",
+ ),
+ classes=["card", "shadow-sm"],
+ ),
+ classes=["col-lg-4"],
+ ),
+ classes=["row", "g-3"],
+ ),
+ ),
+ user=user,
+ current_page="admin-metrics",
+ error=None,
+ )
+
+
class AdminUsersAttrs(Attrs):
"""Attributes for AdminUsers component."""
@@ -703,6 +967,33 @@ def toggle_episode_public(request: Request, episode_id: int) -> Response:
)
+def admin_metrics(request: Request) -> MetricsDashboard | Response:
+ """Admin metrics dashboard showing episode statistics."""
+ # Check if user is logged in and is admin
+ user_id = request.session.get("user_id")
+ if not user_id:
+ return Response(
+ "",
+ status_code=302,
+ headers={"Location": "/"},
+ )
+
+ user = Core.Database.get_user_by_id(
+ user_id,
+ )
+ if not user or not Core.is_admin(user):
+ return Response(
+ "",
+ status_code=302,
+ headers={"Location": "/?error=forbidden"},
+ )
+
+ # Get metrics data
+ metrics = Core.Database.get_metrics_summary()
+
+ return MetricsDashboard(metrics=metrics, user=user)
+
+
def main() -> None:
"""Admin tests are currently in Web."""
if "test" in sys.argv:
diff --git a/Biz/PodcastItLater/Core.py b/Biz/PodcastItLater/Core.py
index c625e11..40b50ea 100644
--- a/Biz/PodcastItLater/Core.py
+++ b/Biz/PodcastItLater/Core.py
@@ -1088,6 +1088,98 @@ class Database: # noqa: PLR0904
return dict(row) if row is not None else None
@staticmethod
+ def get_metrics_summary() -> dict[str, Any]:
+ """Get aggregate metrics summary for admin dashboard.
+
+ Returns:
+ dict with keys:
+ - total_episodes: Total number of episodes
+ - total_plays: Total play events
+ - total_downloads: Total download events
+ - total_adds: Total add events
+ - most_played: List of top 10 most played episodes
+ - most_downloaded: List of top 10 most downloaded episodes
+ - most_added: List of top 10 most added episodes
+ """
+ with Database.get_connection() as conn:
+ cursor = conn.cursor()
+
+ # Get total episodes
+ cursor.execute("SELECT COUNT(*) as count FROM episodes")
+ total_episodes = cursor.fetchone()["count"]
+
+ # Get event counts
+ cursor.execute(
+ "SELECT COUNT(*) as count FROM episode_metrics "
+ "WHERE event_type = 'played'",
+ )
+ total_plays = cursor.fetchone()["count"]
+
+ cursor.execute(
+ "SELECT COUNT(*) as count FROM episode_metrics "
+ "WHERE event_type = 'downloaded'",
+ )
+ total_downloads = cursor.fetchone()["count"]
+
+ cursor.execute(
+ "SELECT COUNT(*) as count FROM episode_metrics "
+ "WHERE event_type = 'added'",
+ )
+ total_adds = cursor.fetchone()["count"]
+
+ # Get most played episodes
+ cursor.execute(
+ """
+ SELECT e.id, e.title, e.author, COUNT(*) as play_count
+ FROM episode_metrics em
+ JOIN episodes e ON em.episode_id = e.id
+ WHERE em.event_type = 'played'
+ GROUP BY em.episode_id
+ ORDER BY play_count DESC
+ LIMIT 10
+ """,
+ )
+ most_played = [dict(row) for row in cursor.fetchall()]
+
+ # Get most downloaded episodes
+ cursor.execute(
+ """
+ SELECT e.id, e.title, e.author, COUNT(*) as download_count
+ FROM episode_metrics em
+ JOIN episodes e ON em.episode_id = e.id
+ WHERE em.event_type = 'downloaded'
+ GROUP BY em.episode_id
+ ORDER BY download_count DESC
+ LIMIT 10
+ """,
+ )
+ most_downloaded = [dict(row) for row in cursor.fetchall()]
+
+ # Get most added episodes
+ cursor.execute(
+ """
+ SELECT e.id, e.title, e.author, COUNT(*) as add_count
+ FROM episode_metrics em
+ JOIN episodes e ON em.episode_id = e.id
+ WHERE em.event_type = 'added'
+ GROUP BY em.episode_id
+ ORDER BY add_count DESC
+ LIMIT 10
+ """,
+ )
+ most_added = [dict(row) for row in cursor.fetchall()]
+
+ return {
+ "total_episodes": total_episodes,
+ "total_plays": total_plays,
+ "total_downloads": total_downloads,
+ "total_adds": total_adds,
+ "most_played": most_played,
+ "most_downloaded": most_downloaded,
+ "most_added": most_added,
+ }
+
+ @staticmethod
def track_episode_event(
episode_id: int,
event_type: str,
@@ -1130,6 +1222,23 @@ class Database: # noqa: PLR0904
return {row["event_type"]: row["count"] for row in rows}
@staticmethod
+ def get_episode_metric_events(episode_id: int) -> list[dict[str, Any]]:
+ """Get raw metric events for an episode (for testing)."""
+ with Database.get_connection() as conn:
+ cursor = conn.cursor()
+ cursor.execute(
+ """
+ SELECT id, episode_id, user_id, event_type, created_at
+ FROM episode_metrics
+ WHERE episode_id = ?
+ ORDER BY created_at DESC
+ """,
+ (episode_id,),
+ )
+ rows = cursor.fetchall()
+ return [dict(row) for row in rows]
+
+ @staticmethod
def set_user_stripe_customer(user_id: int, customer_id: str) -> None:
"""Link Stripe customer ID to user."""
with Database.get_connection() as conn:
diff --git a/Biz/PodcastItLater/UI.py b/Biz/PodcastItLater/UI.py
index 57d0f33..009fdbe 100644
--- a/Biz/PodcastItLater/UI.py
+++ b/Biz/PodcastItLater/UI.py
@@ -263,6 +263,20 @@ class PageLayout(Component[AnyChildren, PageLayoutAttrs]):
classes=["dropdown-item"],
),
),
+ html.li(
+ html.a(
+ html.i(
+ classes=[
+ "bi",
+ "bi-graph-up",
+ "me-2",
+ ],
+ ),
+ "Metrics",
+ href="/admin/metrics",
+ classes=["dropdown-item"],
+ ),
+ ),
classes=["dropdown-menu"],
aria_labelledby="adminDropdown",
),
diff --git a/Biz/PodcastItLater/Web.py b/Biz/PodcastItLater/Web.py
index b41f31d..60818e9 100644
--- a/Biz/PodcastItLater/Web.py
+++ b/Biz/PodcastItLater/Web.py
@@ -1611,6 +1611,7 @@ def cancel_queue_item(request: Request, job_id: int) -> Response:
app.delete("/queue/{job_id}")(Admin.delete_queue_item)
app.get("/admin/users")(Admin.admin_users)
+app.get("/admin/metrics")(Admin.admin_metrics)
app.post("/admin/users/{user_id}/status")(Admin.update_user_status)
app.post("/admin/episode/{episode_id}/toggle-public")(
Admin.toggle_episode_public,
@@ -2145,6 +2146,124 @@ class TestAdminInterface(BaseWebTest):
self.assertIn("PROCESSING: 1", response.text)
+class TestMetricsDashboard(BaseWebTest):
+ """Test metrics dashboard functionality."""
+
+ def setUp(self) -> None:
+ """Set up test client with logged-in admin user."""
+ super().setUp()
+
+ # Create and login admin user
+ self.user_id, _ = Core.Database.create_user(
+ "ben@bensima.com",
+ )
+ Core.Database.update_user_status(
+ self.user_id,
+ "active",
+ )
+ self.client.post("/login", data={"email": "ben@bensima.com"})
+
+ def test_metrics_page_requires_admin(self) -> None:
+ """Verify non-admin users cannot access metrics."""
+ # Create non-admin user
+ user_id, _ = Core.Database.create_user("user@example.com")
+ Core.Database.update_user_status(user_id, "active")
+
+ # Login as non-admin
+ self.client.get("/logout")
+ self.client.post("/login", data={"email": "user@example.com"})
+
+ # Try to access metrics
+ response = self.client.get("/admin/metrics")
+
+ # Should redirect
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.headers["Location"], "/?error=forbidden")
+
+ def test_metrics_page_requires_login(self) -> None:
+ """Verify unauthenticated users are redirected."""
+ self.client.get("/logout")
+
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 302)
+ self.assertEqual(response.headers["Location"], "/")
+
+ def test_metrics_displays_summary(self) -> None:
+ """Verify metrics summary is displayed."""
+ # Create test episode
+ episode_id = Core.Database.create_episode(
+ title="Test Episode",
+ audio_url="http://example.com/audio.mp3",
+ content_length=1000,
+ duration=300,
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode_id)
+
+ # Track some events
+ Core.Database.track_episode_metric(episode_id, "played")
+ Core.Database.track_episode_metric(episode_id, "played")
+ Core.Database.track_episode_metric(episode_id, "downloaded")
+ Core.Database.track_episode_metric(episode_id, "added", self.user_id)
+
+ # Get metrics page
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Episode Metrics", response.text)
+ self.assertIn("Total Episodes", response.text)
+ self.assertIn("Total Plays", response.text)
+ self.assertIn("Total Downloads", response.text)
+ self.assertIn("Total Adds", response.text)
+
+ def test_metrics_shows_top_episodes(self) -> None:
+ """Verify top episodes tables are displayed."""
+ # Create test episodes
+ episode1 = Core.Database.create_episode(
+ title="Popular Episode",
+ audio_url="http://example.com/popular.mp3",
+ content_length=1000,
+ duration=300,
+ author="Test Author",
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode1)
+
+ episode2 = Core.Database.create_episode(
+ title="Less Popular Episode",
+ audio_url="http://example.com/less.mp3",
+ content_length=1000,
+ duration=300,
+ )
+ Core.Database.add_episode_to_user(self.user_id, episode2)
+
+ # Track events - more for episode1
+ for _ in range(5):
+ Core.Database.track_episode_metric(episode1, "played")
+ for _ in range(2):
+ Core.Database.track_episode_metric(episode2, "played")
+
+ for _ in range(3):
+ Core.Database.track_episode_metric(episode1, "downloaded")
+ Core.Database.track_episode_metric(episode2, "downloaded")
+
+ # Get metrics page
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Most Played", response.text)
+ self.assertIn("Most Downloaded", response.text)
+ self.assertIn("Popular Episode", response.text)
+
+ def test_metrics_empty_state(self) -> None:
+ """Verify metrics page works with no data."""
+ response = self.client.get("/admin/metrics")
+
+ self.assertEqual(response.status_code, 200)
+ self.assertIn("Episode Metrics", response.text)
+ # Should show 0 for counts
+ self.assertIn("Total Episodes", response.text)
+
+
class TestJobCancellation(BaseWebTest):
"""Test job cancellation functionality."""
@@ -2499,10 +2618,11 @@ class TestEpisodeDeduplication(BaseWebTest):
for url in similar_urls:
url_hash = Core.hash_url(url)
- episode = Core.Database.find_episode_by_url_hash(url_hash)
+ episode = Core.Database.get_episode_by_url_hash(url_hash)
self.assertIsNotNone(episode)
- self.assertEqual(episode["id"], self.episode_id) # type: ignore[index]
+ if episode is not None:
+ self.assertEqual(episode["id"], self.episode_id)
def test_add_existing_episode_to_user_feed(self) -> None:
"""Should add existing episode to new user's feed."""
@@ -2551,7 +2671,7 @@ class TestMetricsTracking(BaseWebTest):
)
# Verify metric was recorded
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "added")
self.assertEqual(metrics[0]["user_id"], self.user_id)
@@ -2564,7 +2684,7 @@ class TestMetricsTracking(BaseWebTest):
self.user_id,
)
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "played")
@@ -2576,7 +2696,7 @@ class TestMetricsTracking(BaseWebTest):
user_id=None,
)
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
self.assertEqual(len(metrics), 1)
self.assertEqual(metrics[0]["event_type"], "played")
self.assertIsNone(metrics[0]["user_id"])
@@ -2594,7 +2714,7 @@ class TestMetricsTracking(BaseWebTest):
self.assertEqual(response.status_code, 200)
# Verify metric was recorded
- metrics = Core.Database.get_episode_metrics(self.episode_id)
+ metrics = Core.Database.get_episode_metric_events(self.episode_id)
played_metrics = [m for m in metrics if m["event_type"] == "played"]
self.assertGreater(len(played_metrics), 0)