This commit is contained in:
parent
44827ef205
commit
776cf139fa
7 changed files with 64 additions and 1 deletions
|
@ -25,4 +25,7 @@ COPY --from=frontend-build /app/dist /opt/dist
|
||||||
ENV PG_DIRECTORY=/work
|
ENV PG_DIRECTORY=/work
|
||||||
ENV PG_UPLOADS_DIRECTORY=/uploads
|
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"]
|
CMD ["uv", "run", "uvicorn", "--app-dir", "/opt/src", "main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
|
|
|
@ -40,6 +40,7 @@ export type PodcastEpisodePublic = {
|
||||||
file_hash: string;
|
file_hash: string;
|
||||||
file_size: number;
|
file_size: number;
|
||||||
publish_date?: string;
|
publish_date?: string;
|
||||||
|
request_count?: number;
|
||||||
id: string;
|
id: string;
|
||||||
podcast_id: string;
|
podcast_id: string;
|
||||||
description_html: string | null;
|
description_html: string | null;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createMutation, createQuery, useQueryClient, } from "@tanstack/solid-query";
|
import { createMutation, createQuery, useQueryClient, } from "@tanstack/solid-query";
|
||||||
import AdminLayout from "../../admin-layout";
|
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 { A, useNavigate, useParams } from "@solidjs/router";
|
||||||
import { deletePodcastMutation, readEpisodesOptions, readPodcastOptions, readPodcastQueryKey, readPodcastsQueryKey, updatePodcastMutation } from "../../client/@tanstack/solid-query.gen";
|
import { deletePodcastMutation, readEpisodesOptions, readPodcastOptions, readPodcastQueryKey, readPodcastsQueryKey, updatePodcastMutation } from "../../client/@tanstack/solid-query.gen";
|
||||||
import Error from "../../components/error";
|
import Error from "../../components/error";
|
||||||
|
@ -11,6 +11,12 @@ import Upload from "../../components/upload";
|
||||||
import UploadImage from "../../components/upload-image";
|
import UploadImage from "../../components/upload-image";
|
||||||
import { PodcastPublic } from "../../client";
|
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() {
|
export default function AdminPodcast() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const navigate = useNavigate();
|
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 (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<ErrorBoundary fallback={(err, reset) => <Error message={err} reset={reset} />}>
|
<ErrorBoundary fallback={(err, reset) => <Error message={err} reset={reset} />}>
|
||||||
|
@ -123,6 +136,7 @@ export default function AdminPodcast() {
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Published</th>
|
<th>Published</th>
|
||||||
<th>Duration</th>
|
<th>Duration</th>
|
||||||
|
<th>Requests</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -133,6 +147,12 @@ export default function AdminPodcast() {
|
||||||
<td>{episode.name}</td>
|
<td>{episode.name}</td>
|
||||||
<td>{episode.publish_date ? (new Date(episode.publish_date)).toLocaleString() : "?"}</td>
|
<td>{episode.publish_date ? (new Date(episode.publish_date)).toLocaleString() : "?"}</td>
|
||||||
<td>{episode.duration ? `${(episode.duration / 60).toFixed(0)}min` : "?"}</td>
|
<td>{episode.duration ? `${(episode.duration / 60).toFixed(0)}min` : "?"}</td>
|
||||||
|
<td>
|
||||||
|
{episode.request_count ? <>
|
||||||
|
<div class="radial-progress me-1.5" style={`--value:${(calculateEpisodeRequestPercent(episode.request_count, totalRequestCount()) * 100).toFixed(1)}; --size:1rem; --thickness:0.2rem;`} aria-valuenow={(calculateEpisodeRequestPercent(episode.request_count, totalRequestCount()) * 100).toFixed(1)} role="progressbar"></div>
|
||||||
|
<span>{episode.request_count.toLocaleString()} ({(calculateEpisodeRequestPercent(episode.request_count, totalRequestCount()) * 100).toFixed(1)}%)</span>
|
||||||
|
</> : "-"}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<A href={`/admin/${params.podcastId}/${episode.id}`} class="btn btn-ghost btn-xs"><Pencil class="size-4" /> Edit</A>
|
<A href={`/admin/${params.podcastId}/${episode.id}`} class="btn btn-ghost btn-xs"><Pencil class="size-4" /> Edit</A>
|
||||||
</td>
|
</td>
|
||||||
|
|
5
docker-entrypoint.sh
Normal file
5
docker-entrypoint.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -x
|
||||||
|
|
||||||
|
uv run alembic upgrade head || exit 1
|
||||||
|
exec "$@"
|
|
@ -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")
|
|
@ -482,6 +482,10 @@ def get_episode_or_cover(session: SessionDep, podcast_id: str, filename: str):
|
||||||
if episode is None:
|
if episode is None:
|
||||||
raise HTTPException(status_code=404, detail="Episode or podcast not found")
|
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")
|
return FileResponse(settings.directory / f"{episode.id}.m4a")
|
||||||
|
|
||||||
elif (
|
elif (
|
||||||
|
|
|
@ -60,6 +60,7 @@ class PodcastEpisodeBase(SQLModel):
|
||||||
publish_date: datetime = Field(
|
publish_date: datetime = Field(
|
||||||
default_factory=lambda: datetime.now(timezone.utc), nullable=False
|
default_factory=lambda: datetime.now(timezone.utc), nullable=False
|
||||||
)
|
)
|
||||||
|
request_count: int = Field(default=0, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
class PodcastEpisode(PodcastEpisodeBase, table=True):
|
class PodcastEpisode(PodcastEpisodeBase, table=True):
|
||||||
|
|
Loading…
Reference in a new issue