feat: add deep fallback for latest anime episodes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
1
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
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user