refactor: defer provider episode count to async load
This commit is contained in:
25
README.md
25
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 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.
|
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:
|
Create a local user with:
|
||||||
|
|
||||||
```bash
|
```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.
|
Configuration is loaded from environment variables, and a local `.env` file is read automatically.
|
||||||
|
|
||||||
| Variable | Default | Purpose |
|
| Variable | Default | Purpose |
|
||||||
| --------------------------- | --------------- | ------------------------------------------------------------------- |
|
| --------------------------- | --------------- | -------------------------------------------------------------------------- |
|
||||||
| `PORT` | `3000` | HTTP port for the server. |
|
| `PORT` | `3000` | HTTP port for the server. |
|
||||||
| `DATABASE_FILE` | `mal.db` | SQLite database path. |
|
| `DATABASE_FILE` | `mal.db` | SQLite database path. |
|
||||||
| `GIN_MODE` | release default | Gin runtime mode. |
|
| `GIN_MODE` | release default | Gin runtime mode. |
|
||||||
| `MAL_CORS_ALLOW_ALL` | disabled | Allows any origin when set to `1`; intended for local/proxy setups. |
|
| `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`. |
|
| `EPISODE_AVAILABILITY_MODE` | `auto` | Episode availability strategy: `auto`, `legacy`, or `jikan`. |
|
||||||
| `MAL_JIKAN_TRACE` | disabled | Enables optional Jikan client tracing when truthy. |
|
| `MAL_JIKAN_TRACE` | disabled | Enables optional Jikan client tracing when truthy. |
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,16 @@ func (h *AnimeHandler) animeEpisodeCount(ctx context.Context, anime domain.Anime
|
|||||||
return animeEpisodeCountDisplay{}
|
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 {
|
func animeAudioAvailabilityLabel(episodes []domain.CanonicalEpisode) string {
|
||||||
hasKnownSub := false
|
hasKnownSub := false
|
||||||
for _, episode := range episodes {
|
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{
|
c.HTML(http.StatusOK, "anime.gohtml", gin.H{
|
||||||
"Anime": anime,
|
"Anime": anime,
|
||||||
@@ -255,6 +265,12 @@ func (h *AnimeHandler) loadAnimeDetailsSection(ctx context.Context, id int, sect
|
|||||||
case "statistics":
|
case "statistics":
|
||||||
data, err := h.svc.GetStatistics(ctx, id)
|
data, err := h.svc.GetStatistics(ctx, id)
|
||||||
return data, "anime_statistics", err
|
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":
|
case "themes":
|
||||||
data, err := h.svc.GetThemes(ctx, id)
|
data, err := h.svc.GetThemes(ctx, id)
|
||||||
return data, "anime_themes", err
|
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) {
|
func TestAnimeAudioAvailabilityLabel(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -31,25 +31,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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 $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}}
|
{{template "anime_episode_count_loading" dict "AnimeID" $anime.MalID "Count" .EpisodesCount "Label" .EpisodesCountLabel}}
|
||||||
{{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}}
|
|
||||||
{{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.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.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}}
|
{{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