diff --git a/api/playback/allanime_client.go b/api/playback/allanime_client.go index 9ecc599..87b94ab 100644 --- a/api/playback/allanime_client.go +++ b/api/playback/allanime_client.go @@ -107,6 +107,12 @@ type searchResult struct { Name string } +type AvailableEpisodes struct { + Sub int + Dub int + Raw int +} + type allAnimeClient struct { httpClient *http.Client extractor *providerExtractor @@ -487,6 +493,47 @@ func (c *allAnimeClient) GetEpisodes(ctx context.Context, showID string, mode st return episodes, nil } +func (c *allAnimeClient) GetAvailableEpisodes(ctx context.Context, showID string) (AvailableEpisodes, error) { + graphqlQuery := `query($showId: String!) { + show(_id: $showId) { + availableEpisodesDetail + } + }` + + result, err := c.graphqlRequest(ctx, graphqlQuery, map[string]any{"showId": showID}) + if err != nil { + return AvailableEpisodes{}, err + } + + data, ok := result["data"].(map[string]any) + if !ok { + return AvailableEpisodes{}, fmt.Errorf("invalid response") + } + + show, ok := data["show"].(map[string]any) + if !ok || show == nil { + return AvailableEpisodes{}, fmt.Errorf("show not found") + } + + detail, ok := show["availableEpisodesDetail"].(map[string]any) + if !ok { + return AvailableEpisodes{}, fmt.Errorf("invalid detail") + } + + var count AvailableEpisodes + if sub, ok := detail["sub"].([]any); ok { + count.Sub = len(sub) + } + if dub, ok := detail["dub"].([]any); ok { + count.Dub = len(dub) + } + if raw, ok := detail["raw"].([]any); ok { + count.Raw = len(raw) + } + + return count, nil +} + func buildStreamSource(url, sourceType, provider string) StreamSource { return StreamSource{ URL: url, diff --git a/api/playback/handler.go b/api/playback/handler.go index 0eda327..953f380 100644 --- a/api/playback/handler.go +++ b/api/playback/handler.go @@ -2,6 +2,7 @@ package playback import ( "encoding/json" + "fmt" "io" "log" "net/http" @@ -107,6 +108,31 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) { log.Printf("watch data error: %v", err) } + // Update episodes list if fallback has more + if watchData.FallbackEpisodes != nil { + maxCount := 0 + for _, count := range watchData.FallbackEpisodes { + if count > maxCount { + maxCount = count + } + } + + if maxCount > len(episodes.Data) { + // Add dummy episodes for the ones Jikan is missing + start := len(episodes.Data) + 1 + for i := start; i <= maxCount; i++ { + dummy := jikan.Episode{ + MalID: i, + Episode: fmt.Sprintf("Episode %d", i), + Title: fmt.Sprintf("Episode %d", i), + Images: &jikan.EpisodeImages{}, + } + dummy.Images.Jpg.ImageURL = dummy.GetFallbackImage(id) + episodes.Data = append(episodes.Data, dummy) + } + } + } + if err := templates.GetRenderer().ExecuteTemplate(w, "watch.gohtml", map[string]any{ "Anime": anime, "Episodes": episodes.Data, diff --git a/api/playback/service_base.go b/api/playback/service_base.go index 769f2c1..667642e 100644 --- a/api/playback/service_base.go +++ b/api/playback/service_base.go @@ -59,10 +59,11 @@ type playbackDataCacheItem struct { } type playbackBaseData struct { - Title string - AvailableModes []string - ModeSources map[string]ModeSource - Segments []SkipSegment + Title string + AvailableModes []string + ModeSources map[string]ModeSource + Segments []SkipSegment + FallbackEpisodes map[string]int } type modeSourceResult struct { @@ -140,6 +141,13 @@ func (s *Service) BuildWatchPageData(ctx context.Context, malID int, titleCandid return WatchPageData{}, errors.New("no direct playable sources available") } + fallbackEpisodes := make(map[string]int) + if counts, err := s.allAnimeClient.GetAvailableEpisodes(ctx, showID); err == nil { + fallbackEpisodes["sub"] = counts.Sub + fallbackEpisodes["dub"] = counts.Dub + fallbackEpisodes["raw"] = counts.Raw + } + watchTitle := strings.TrimSpace(resolvedTitle) if watchTitle == "" { watchTitle = firstNonEmptyTitle(titleCandidates) @@ -149,10 +157,11 @@ func (s *Service) BuildWatchPageData(ctx context.Context, malID int, titleCandid } baseData = playbackBaseData{ - Title: watchTitle, - AvailableModes: availableModes(modeSources), - ModeSources: modeSources, - Segments: segments, + Title: watchTitle, + AvailableModes: availableModes(modeSources), + ModeSources: modeSources, + Segments: segments, + FallbackEpisodes: fallbackEpisodes, } s.setPlaybackBaseDataCache(cacheKey, baseData) @@ -189,6 +198,7 @@ func (s *Service) BuildWatchPageData(ctx context.Context, malID int, titleCandid AvailableModes: cloneSlice(baseData.AvailableModes), ModeSources: clientModeSources, Segments: cloneSlice(segments), + FallbackEpisodes: baseData.FallbackEpisodes, }, nil } @@ -342,10 +352,11 @@ func (s *Service) fetchPlaybackSourcesAndSegments(ctx context.Context, showID st func clonePlaybackBaseData(data playbackBaseData) playbackBaseData { return playbackBaseData{ - Title: data.Title, - AvailableModes: cloneSlice(data.AvailableModes), - ModeSources: cloneModeSources(data.ModeSources), - Segments: cloneSlice(data.Segments), + Title: data.Title, + AvailableModes: cloneSlice(data.AvailableModes), + ModeSources: cloneModeSources(data.ModeSources), + Segments: cloneSlice(data.Segments), + FallbackEpisodes: data.FallbackEpisodes, } } diff --git a/api/playback/types.go b/api/playback/types.go index 56d7a52..12d8119 100644 --- a/api/playback/types.go +++ b/api/playback/types.go @@ -35,13 +35,14 @@ type SkipSegment struct { } type WatchPageData struct { - MalID int - Title string - CurrentEpisode string - StartTimeSeconds float64 - CurrentStatus string - InitialMode string - AvailableModes []string - ModeSources map[string]ModeSource - Segments []SkipSegment + MalID int + Title string + CurrentEpisode string + StartTimeSeconds float64 + CurrentStatus string + InitialMode string + AvailableModes []string + ModeSources map[string]ModeSource + Segments []SkipSegment + FallbackEpisodes map[string]int } diff --git a/go.mod b/go.mod index c143143..2c8ba9e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( github.com/PuerkitoBio/goquery v1.11.0 + github.com/go-chi/chi/v5 v5.2.5 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.40 diff --git a/go.sum b/go.sum index fcdd556..9223d51 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/templates/watch.gohtml b/templates/watch.gohtml index 2864993..38ec0d9 100644 --- a/templates/watch.gohtml +++ b/templates/watch.gohtml @@ -15,6 +15,12 @@

No episodes found for this anime.

{{else}} + {{$totalEps := $anime.Episodes}} + {{$fallbackSub := .WatchData.FallbackEpisodes.sub}} + {{if gt $fallbackSub $totalEps}} + {{$totalEps = $fallbackSub}} + {{end}} +
{{range $episodes}} {{$isCurrent := eq (printf "%v" .MalID) $currentEpID}} @@ -27,6 +33,11 @@
{{end}} + {{if gt .MalID $anime.Episodes}} +
+ Latest +
+ {{end}}