From 776cf139fafceed8056a146aea408e8e638acfe0 Mon Sep 17 00:00:00 2001 From: Jake Walker Date: Mon, 28 Jul 2025 22:12:08 +0100 Subject: [PATCH] add request counting --- Dockerfile | 3 ++ client/src/client/types.gen.ts | 1 + client/src/routes/admin/podcast.tsx | 22 +++++++++++++- docker-entrypoint.sh | 5 ++++ ...8d2db9cd60_add_request_count_to_episode.py | 29 +++++++++++++++++++ src/main.py | 4 +++ src/models.py | 1 + 7 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docker-entrypoint.sh create mode 100644 migrations/versions/d58d2db9cd60_add_request_count_to_episode.py diff --git a/Dockerfile b/Dockerfile index 0aa3398..2243bb9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,4 +25,7 @@ COPY --from=frontend-build /app/dist /opt/dist ENV PG_DIRECTORY=/work ENV PG_UPLOADS_DIRECTORY=/uploads +RUN chmod a+x /opt/docker-entrypoint.sh + +ENTRYPOINT ["/opt/docker-entrypoint.sh"] CMD ["uv", "run", "uvicorn", "--app-dir", "/opt/src", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/client/src/client/types.gen.ts b/client/src/client/types.gen.ts index 5ab7dd0..31fe54e 100644 --- a/client/src/client/types.gen.ts +++ b/client/src/client/types.gen.ts @@ -40,6 +40,7 @@ export type PodcastEpisodePublic = { file_hash: string; file_size: number; publish_date?: string; + request_count?: number; id: string; podcast_id: string; description_html: string | null; diff --git a/client/src/routes/admin/podcast.tsx b/client/src/routes/admin/podcast.tsx index 9d8cfa1..e5b47de 100644 --- a/client/src/routes/admin/podcast.tsx +++ b/client/src/routes/admin/podcast.tsx @@ -1,6 +1,6 @@ import { createMutation, createQuery, useQueryClient, } from "@tanstack/solid-query"; import AdminLayout from "../../admin-layout"; -import { createEffect, createSignal, ErrorBoundary, For, Match, Show, Suspense, Switch } from "solid-js"; +import { createEffect, createMemo, createSignal, ErrorBoundary, For, Match, Show, Suspense, Switch } from "solid-js"; import { A, useNavigate, useParams } from "@solidjs/router"; import { deletePodcastMutation, readEpisodesOptions, readPodcastOptions, readPodcastQueryKey, readPodcastsQueryKey, updatePodcastMutation } from "../../client/@tanstack/solid-query.gen"; import Error from "../../components/error"; @@ -11,6 +11,12 @@ import Upload from "../../components/upload"; import UploadImage from "../../components/upload-image"; import { PodcastPublic } from "../../client"; +function calculateEpisodeRequestPercent(episodeRequestCount?: number, totalRequestCount?: number): number { + const totalClipped = (totalRequestCount === 0 ? 1 : totalRequestCount) ?? 1; + const percent = (episodeRequestCount ?? 0) / totalClipped; + return percent; +} + export default function AdminPodcast() { const params = useParams(); const navigate = useNavigate(); @@ -79,6 +85,13 @@ export default function AdminPodcast() { }); } + const totalRequestCount = createMemo(() => { + if (episodeQuery.isSuccess && episodeQuery.data && Array.isArray(episodeQuery.data)) { + return episodeQuery.data.reduce((sum, episode) => sum + (episode.request_count ?? 0), 0); + } + return 0; + }); + return ( }> @@ -123,6 +136,7 @@ export default function AdminPodcast() { Name Published Duration + Requests Actions @@ -133,6 +147,12 @@ export default function AdminPodcast() { {episode.name} {episode.publish_date ? (new Date(episode.publish_date)).toLocaleString() : "?"} {episode.duration ? `${(episode.duration / 60).toFixed(0)}min` : "?"} + + {episode.request_count ? <> +
+ {episode.request_count.toLocaleString()} ({(calculateEpisodeRequestPercent(episode.request_count, totalRequestCount()) * 100).toFixed(1)}%) + : "-"} + Edit diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..17e8d40 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -x + +uv run alembic upgrade head || exit 1 +exec "$@" diff --git a/migrations/versions/d58d2db9cd60_add_request_count_to_episode.py b/migrations/versions/d58d2db9cd60_add_request_count_to_episode.py new file mode 100644 index 0000000..7554a89 --- /dev/null +++ b/migrations/versions/d58d2db9cd60_add_request_count_to_episode.py @@ -0,0 +1,29 @@ +"""add request_count to episode + +Revision ID: d58d2db9cd60 +Revises: 9efcecc1e58d +Create Date: 2025-07-28 21:59:25.547437 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "d58d2db9cd60" +down_revision: Union[str, None] = "9efcecc1e58d" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + "podcastepisode", + sa.Column("request_count", sa.Integer(), nullable=False, server_default="0"), + ) + + +def downgrade() -> None: + op.drop_column("podcastepisode", "request_count") diff --git a/src/main.py b/src/main.py index 65de47d..7b95c91 100644 --- a/src/main.py +++ b/src/main.py @@ -482,6 +482,10 @@ def get_episode_or_cover(session: SessionDep, podcast_id: str, filename: str): if episode is None: raise HTTPException(status_code=404, detail="Episode or podcast not found") + # increment request count + episode.request_count += 1 + session.add(episode) + return FileResponse(settings.directory / f"{episode.id}.m4a") elif ( diff --git a/src/models.py b/src/models.py index 9e7f30d..ccf97d1 100644 --- a/src/models.py +++ b/src/models.py @@ -60,6 +60,7 @@ class PodcastEpisodeBase(SQLModel): publish_date: datetime = Field( default_factory=lambda: datetime.now(timezone.utc), nullable=False ) + request_count: int = Field(default=0, nullable=False) class PodcastEpisode(PodcastEpisodeBase, table=True):