From 9e0f200ca7b289fe1039eca4478d5f33518b308d Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 2 May 2026 17:00:23 +0200 Subject: [PATCH] feat: implement episode image fallback for banned youtube icons --- api/playback/handler.go | 4 ++ integrations/jikan/episodes.go | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/api/playback/handler.go b/api/playback/handler.go index 1bdb11e..412746c 100644 --- a/api/playback/handler.go +++ b/api/playback/handler.go @@ -63,6 +63,10 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) { } } + for i := range episodes.Data { + episodes.Data[i].Images.Jpg.ImageURL = episodes.Data[i].GetFallbackImage(id) + } + sort.Slice(episodes.Data, func(i, j int) bool { return episodes.Data[i].MalID < episodes.Data[j].MalID }) diff --git a/integrations/jikan/episodes.go b/integrations/jikan/episodes.go index 884639d..4e5ec2b 100644 --- a/integrations/jikan/episodes.go +++ b/integrations/jikan/episodes.go @@ -3,9 +3,79 @@ package jikan import ( "context" "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" "time" ) +const bannedImageURL = "https://myanimelist.net/images/icon-banned-youtube.png" + +var httpClient = &http.Client{Timeout: 10 * time.Second} + +func (e *Episode) GetFallbackImage(animeID int) string { + if e.Images == nil || e.Images.Jpg.ImageURL != bannedImageURL { + return e.Images.Jpg.ImageURL + } + + episodeNum := 1 + if e.Episode != "" { + episodeNum, _ = strconv.Atoi(e.Episode) + } + + episodeURL := fmt.Sprintf("https://myanimelist.net/anime/%d/episode/%d", animeID, episodeNum) + fallbackURL := scrapeAnimeImageFromEpisodePage(episodeURL, episodeNum) + if fallbackURL != "" { + return fallbackURL + } + + return e.Images.Jpg.ImageURL +} + +func scrapeAnimeImageFromEpisodePage(episodeURL string, episodeNum int) string { + req, err := http.NewRequest("GET", episodeURL, nil) + if err != nil { + return "" + } + // Setting User-Agent is important for MAL + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + + resp, err := httpClient.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return "" + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "" + } + + html := string(body) + + // Look for the JSON data in MAL.episodeVideo.aroundVideos + // We extract the object {} containing "episode_number":X + episodeStr := strconv.Itoa(episodeNum) + objPattern := regexp.MustCompile(`\{[^{}]*"episode_number":\s*` + episodeStr + `[^{}]*\}`) + match := objPattern.FindString(html) + if match != "" { + thumbRe := regexp.MustCompile(`"thumbnail":\s*"([^"]+)"`) + thumbMatch := thumbRe.FindStringSubmatch(match) + if len(thumbMatch) > 1 { + // Unescape backslashes in URL + return strings.ReplaceAll(thumbMatch[1], `\/`, `/`) + } + } + + return "" +} + func (c *Client) GetEpisodes(ctx context.Context, animeID int, page int) (EpisodesResponse, error) { if page < 1 { page = 1