diff --git a/README.md b/README.md index 21936e6..5d9a9ad 100644 --- a/README.md +++ b/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 @@ -137,15 +144,15 @@ go run ./cmd/user 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. | diff --git a/internal/anime/details_handler.go b/internal/anime/details_handler.go index 7b01f04..b55a767 100644 --- a/internal/anime/details_handler.go +++ b/internal/anime/details_handler.go @@ -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 diff --git a/internal/anime/handler_test.go b/internal/anime/handler_test.go index e70ea85..06d8bc1 100644 --- a/internal/anime/handler_test.go +++ b/internal/anime/handler_test.go @@ -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 diff --git a/templates/anime.gohtml b/templates/anime.gohtml index 7f1bac7..5b3e300 100644 --- a/templates/anime.gohtml +++ b/templates/anime.gohtml @@ -31,25 +31,7 @@ {{end}} {{if $anime.Type}}{{$anime.Type}}{{end}} - {{if .EpisodesCount}} - {{if eq .EpisodesCountLabel "Total episodes"}} - - {{.EpisodesCount}} total eps - - {{else if $anime.Episodes}} - - {{.EpisodesCount}} aired / {{$anime.Episodes}} total eps - - {{else}} - - {{.EpisodesCount}} aired eps - - {{end}} - {{else if $anime.Episodes}} - - {{$anime.Episodes}} total eps - - {{end}} + {{template "anime_episode_count_loading" dict "AnimeID" $anime.MalID "Count" .EpisodesCount "Label" .EpisodesCountLabel}} {{if $anime.Status}}{{$anime.Status}}{{end}} {{if $anime.Season}}{{$anime.Premiered}}{{end}} {{if $anime.ShortRating}}{{$anime.ShortRating}}{{end}} diff --git a/templates/components/anime_episode_count.gohtml b/templates/components/anime_episode_count.gohtml new file mode 100644 index 0000000..cdb26b1 --- /dev/null +++ b/templates/components/anime_episode_count.gohtml @@ -0,0 +1,34 @@ +{{define "anime_episode_count"}} +{{if .Items.Count}} + + {{.Items.Count}} + {{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}} + +{{end}} +{{end}} + +{{define "anime_episode_count_loading"}} +{{if .Count}} + + {{.Count}} + {{if eq .Label "Total episodes"}} + total eps + {{else}} + aired eps + {{end}} + +{{end}} +{{end}}