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
}

1
go.mod
View File

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

2
go.sum
View File

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

View File

@@ -15,6 +15,12 @@
<p class="text-lg">No episodes found for this anime.</p>
</div>
{{else}}
{{$totalEps := $anime.Episodes}}
{{$fallbackSub := .WatchData.FallbackEpisodes.sub}}
{{if gt $fallbackSub $totalEps}}
{{$totalEps = $fallbackSub}}
{{end}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{{range $episodes}}
{{$isCurrent := eq (printf "%v" .MalID) $currentEpID}}
@@ -27,6 +33,11 @@
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
</div>
{{end}}
{{if gt .MalID $anime.Episodes}}
<div class="absolute left-2 top-2 rounded bg-red-600 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-white shadow-sm">
Latest
</div>
{{end}}
<div class="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
<div class="bg-accent flex h-12 w-12 items-center justify-center rounded-full text-white shadow-lg">
<svg class="ml-1 h-6 w-6" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z" /></svg>