From 70dbc1ba85fb12a36dbe9b3bd71f9657e132d678 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 2 May 2026 17:21:56 +0200 Subject: [PATCH] feat: use real metadata for fallback episodes --- api/playback/allanime_client.go | 64 +++++++++++++++++++++++++++++---- api/playback/handler.go | 41 +++++++++++++++++---- api/playback/service_base.go | 14 ++++++-- 3 files changed, 103 insertions(+), 16 deletions(-) diff --git a/api/playback/allanime_client.go b/api/playback/allanime_client.go index 87b94ab..abba7a9 100644 --- a/api/playback/allanime_client.go +++ b/api/playback/allanime_client.go @@ -108,9 +108,9 @@ type searchResult struct { } type AvailableEpisodes struct { - Sub int - Dub int - Raw int + Sub []string + Dub []string + Raw []string } type allAnimeClient struct { @@ -497,6 +497,7 @@ func (c *allAnimeClient) GetAvailableEpisodes(ctx context.Context, showID string graphqlQuery := `query($showId: String!) { show(_id: $showId) { availableEpisodesDetail + lastEpisodeInfo } }` @@ -522,18 +523,69 @@ func (c *allAnimeClient) GetAvailableEpisodes(ctx context.Context, showID string var count AvailableEpisodes if sub, ok := detail["sub"].([]any); ok { - count.Sub = len(sub) + for _, s := range sub { + if str, ok := s.(string); ok { + count.Sub = append(count.Sub, str) + } + } } if dub, ok := detail["dub"].([]any); ok { - count.Dub = len(dub) + for _, s := range dub { + if str, ok := s.(string); ok { + count.Dub = append(count.Dub, str) + } + } } if raw, ok := detail["raw"].([]any); ok { - count.Raw = len(raw) + for _, s := range raw { + if str, ok := s.(string); ok { + count.Raw = append(count.Raw, str) + } + } } return count, nil } +func (c *allAnimeClient) GetEpisodeMetadata(ctx context.Context, showID string, episode string) (map[string]any, error) { + // First try to get it from the standard episode query which often contains metadata + // but we'll use a specific query that includes images and titles if available + graphqlQuery := `query($showId: String!, $episodeString: String!, $translationType: VaildTranslationTypeEnumType!) { + episode(showId: $showId, episodeString: $episodeString, translationType: $translationType) { + notes + episodeInfo { + notes + thumbnails + vidinfors { + vidPath + } + } + } + }` + + // We'll try SUB by default for metadata + result, err := c.graphqlRequest(ctx, graphqlQuery, map[string]any{ + "showId": showID, + "episodeString": episode, + "translationType": "sub", + }) + if err != nil { + return nil, err + } + + data, ok := result["data"].(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid response") + } + + ep, ok := data["episode"].(map[string]any) + if !ok { + return nil, fmt.Errorf("episode not found") + } + + return ep, 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 953f380..58c8deb 100644 --- a/api/playback/handler.go +++ b/api/playback/handler.go @@ -118,17 +118,44 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) { } if maxCount > len(episodes.Data) { - // Add dummy episodes for the ones Jikan is missing + // Fetch metadata for the missing episodes start := len(episodes.Data) + 1 for i := start; i <= maxCount; i++ { - dummy := jikan.Episode{ + epStr := strconv.Itoa(i) + meta, err := h.svc.GetEpisodeMetadata(r.Context(), id, epStr) + + title := fmt.Sprintf("Episode %d", i) + imgURL := "" + + if err == nil && meta != nil { + if info, ok := meta["episodeInfo"].(map[string]any); ok { + if thumbs, ok := info["thumbnails"].([]any); ok && len(thumbs) > 0 { + if firstThumb, ok := thumbs[0].(string); ok { + imgURL = firstThumb + } + } + } + if notes, ok := meta["notes"].(string); ok && notes != "" { + title = notes + } + } + + if imgURL == "" { + // Last resort fallback + tmpEp := jikan.Episode{MalID: i} + imgURL = tmpEp.GetFallbackImage(id) + } + + episodes.Data = append(episodes.Data, 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) + Title: title, + Images: &jikan.EpisodeImages{ + Jpg: struct { + ImageURL string `json:"image_url"` + }{ImageURL: imgURL}, + }, + }) } } } diff --git a/api/playback/service_base.go b/api/playback/service_base.go index 667642e..ba83a6c 100644 --- a/api/playback/service_base.go +++ b/api/playback/service_base.go @@ -143,9 +143,9 @@ func (s *Service) BuildWatchPageData(ctx context.Context, malID int, titleCandid 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 + fallbackEpisodes["sub"] = len(counts.Sub) + fallbackEpisodes["dub"] = len(counts.Dub) + fallbackEpisodes["raw"] = len(counts.Raw) } watchTitle := strings.TrimSpace(resolvedTitle) @@ -360,6 +360,14 @@ func clonePlaybackBaseData(data playbackBaseData) playbackBaseData { } } +func (s *Service) GetEpisodeMetadata(ctx context.Context, malID int, episode string) (map[string]any, error) { + showID, _, err := s.resolveShowCached(ctx, malID, nil) + if err != nil { + return nil, err + } + return s.allAnimeClient.GetEpisodeMetadata(ctx, showID, episode) +} + func cloneSlice[T any](items []T) []T { if items == nil { return []T{}