feat: add deep fallback for latest anime episodes

This commit is contained in:
2026-05-02 17:20:38 +02:00
committed by Mikkel Elvers
parent 8fb7b1b72f
commit dd301384c5
7 changed files with 120 additions and 21 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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
}