refactor: defer provider episode count to async load

This commit is contained in:
2026-06-22 21:09:02 +02:00
committed by Milas Holsting
parent 01564ffd52
commit e333ae36e8
5 changed files with 90 additions and 29 deletions

View File

@@ -112,6 +112,13 @@ just dev
The development server runs on `http://localhost:3000` by default. `just dev` uses Air to rebuild
the Go server and frontend assets when relevant files change.
Playback proxying requires a local `PLAYBACK_PROXY_SECRET` so the server can mint stream and
subtitle proxy tokens. Generate a strong value and add it to `.env` before using playback:
```bash
printf 'PLAYBACK_PROXY_SECRET=%s\n' "$(openssl rand -base64 32)" >> .env
```
Create a local user with:
```bash
@@ -137,15 +144,15 @@ go run ./cmd/user <username> <password>
Configuration is loaded from environment variables, and a local `.env` file is read automatically.
| Variable | Default | Purpose |
| --------------------------- | --------------- | ------------------------------------------------------------------- |
| `PORT` | `3000` | HTTP port for the server. |
| `DATABASE_FILE` | `mal.db` | SQLite database path. |
| `GIN_MODE` | release default | Gin runtime mode. |
| `MAL_CORS_ALLOW_ALL` | disabled | Allows any origin when set to `1`; intended for local/proxy setups. |
| `PLAYBACK_PROXY_SECRET` | empty | Enables signed playback proxy tokens when set. |
| `EPISODE_AVAILABILITY_MODE` | `auto` | Episode availability strategy: `auto`, `legacy`, or `jikan`. |
| `MAL_JIKAN_TRACE` | disabled | Enables optional Jikan client tracing when truthy. |
| Variable | Default | Purpose |
| --------------------------- | --------------- | -------------------------------------------------------------------------- |
| `PORT` | `3000` | HTTP port for the server. |
| `DATABASE_FILE` | `mal.db` | SQLite database path. |
| `GIN_MODE` | release default | Gin runtime mode. |
| `MAL_CORS_ALLOW_ALL` | disabled | Allows any origin when set to `1`; intended for local/proxy setups. |
| `PLAYBACK_PROXY_SECRET` | empty | Secret used to mint playback proxy tokens; required for playback proxying. |
| `EPISODE_AVAILABILITY_MODE` | `auto` | Episode availability strategy: `auto`, `legacy`, or `jikan`. |
| `MAL_JIKAN_TRACE` | disabled | Enables optional Jikan client tracing when truthy. |
</details>

View File

@@ -109,6 +109,16 @@ func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime
return animeEpisodeCountDisplay{}
}
func animeInitialEpisodeCount(anime domain.Anime, now time.Time) animeEpisodeCountDisplay {
if anime.Episodes > 0 {
return animeEpisodeCountDisplay{Count: anime.Episodes, Label: "Total episodes"}
}
if count := releasedEpisodeCount(anime, now); count > 0 {
return animeEpisodeCountDisplay{Count: count, Label: "Estimated aired episodes"}
}
return animeEpisodeCountDisplay{}
}
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
hasKnownSub := false
for _, episode := range episodes {
@@ -193,7 +203,7 @@ func (h *AnimeHandler) HandleAnimeDetails(c *gin.Context) {
}
}
episodesCount := h.animeEpisodeCount(c.Request.Context(), anime, time.Now())
episodesCount := animeInitialEpisodeCount(anime, time.Now())
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
"Anime": anime,
@@ -255,6 +265,12 @@ func (h *AnimeHandler) loadAnimeDetailsSection(ctx context.Context, id int, sect
case "statistics":
data, err := h.svc.GetStatistics(ctx, id)
return data, "anime_statistics", err
case "episode-count":
anime, err := h.svc.GetAnimeByID(ctx, id)
if err != nil {
return nil, "", err
}
return h.animeEpisodeCount(ctx, anime, time.Now()), "anime_episode_count", nil
case "themes":
data, err := h.svc.GetThemes(ctx, id)
return data, "anime_themes", err

View File

@@ -175,6 +175,28 @@ func TestAnimeEpisodeCountFallsBackToMetadata(t *testing.T) {
}
}
func TestAnimeInitialEpisodeCountDoesNotCallEpisodeService(t *testing.T) {
episodeSvc := &stubEpisodeService{
episodes: domain.CanonicalEpisodeList{
Episodes: []domain.CanonicalEpisode{{Number: 1}, {Number: 2}, {Number: 3}},
},
}
got := animeInitialEpisodeCount(domain.Anime{Anime: jikan.Anime{
MalID: 59970,
Airing: true,
Episodes: 12,
Aired: jikan.Aired{From: "2026-04-03T00:00:00+00:00"},
}}, time.Date(2026, time.June, 21, 0, 0, 0, 0, time.UTC))
if got.Count != 12 || got.Label != "Total episodes" {
t.Fatalf("animeInitialEpisodeCount() = %+v, want count=12 label=%q", got, "Total episodes")
}
if episodeSvc.called != 0 {
t.Fatalf("GetCanonicalEpisodes() calls = %d, want 0", episodeSvc.called)
}
}
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
tests := []struct {
name string

View File

@@ -31,25 +31,7 @@
</div>
{{end}}
{{if $anime.Type}}<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">{{$anime.Type}}</span>{{end}}
{{if .EpisodesCount}}
{{if eq .EpisodesCountLabel "Total episodes"}}
<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">
<span class="text-foreground">{{.EpisodesCount}}</span> total eps
</span>
{{else if $anime.Episodes}}
<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">
<span class="text-foreground">{{.EpisodesCount}}</span> aired <span class="text-foreground-muted">/</span> <span class="text-foreground">{{$anime.Episodes}}</span> total eps
</span>
{{else}}
<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">
<span class="text-foreground">{{.EpisodesCount}}</span> aired eps
</span>
{{end}}
{{else if $anime.Episodes}}
<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">
<span class="text-foreground">{{$anime.Episodes}}</span> total eps
</span>
{{end}}
{{template "anime_episode_count_loading" dict "AnimeID" $anime.MalID "Count" .EpisodesCount "Label" .EpisodesCountLabel}}
{{if $anime.Status}}<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">{{$anime.Status}}</span>{{end}}
{{if $anime.Season}}<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">{{$anime.Premiered}}</span>{{end}}
{{if $anime.ShortRating}}<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">{{$anime.ShortRating}}</span>{{end}}

View File

@@ -0,0 +1,34 @@
{{define "anime_episode_count"}}
{{if .Items.Count}}
<span class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden">
<span class="text-foreground">{{.Items.Count}}</span>
{{if eq .Items.Label "Total episodes"}}
total eps
{{else if eq .Items.Label "Available episodes"}}
available eps
{{else if eq .Items.Label "Listed episodes"}}
listed eps
{{else}}
aired eps
{{end}}
</span>
{{end}}
{{end}}
{{define "anime_episode_count_loading"}}
{{if .Count}}
<span
hx-get="/anime/{{.AnimeID}}?section=episode-count"
hx-trigger="load"
hx-swap="outerHTML"
class="flex min-w-0 items-center gap-1.5 before:mr-1 before:block before:size-0.75 before:shrink-0 before:rounded-full before:bg-current before:opacity-65 first:before:hidden"
>
<span class="text-foreground">{{.Count}}</span>
{{if eq .Label "Total episodes"}}
total eps
{{else}}
aired eps
{{end}}
</span>
{{end}}
{{end}}