refactor: defer provider episode count to async load
This commit is contained in:
11
README.md
11
README.md
@@ -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
|
||||
@@ -138,12 +145,12 @@ 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. |
|
||||
| `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. |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}
|
||||
|
||||
34
templates/components/anime_episode_count.gohtml
Normal file
34
templates/components/anime_episode_count.gohtml
Normal 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}}
|
||||
Reference in New Issue
Block a user