feat: parallelize episode image fallback scraping
This commit is contained in:
@@ -8,10 +8,11 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"mal/integrations/jikan"
|
"mal/integrations/jikan"
|
||||||
ctxpkg "mal/internal/context"
|
ctxpkg "mal/internal/context"
|
||||||
"mal/internal/db"
|
database "mal/internal/db"
|
||||||
"mal/templates"
|
"mal/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,9 +64,18 @@ func (h *Handler) HandleWatchPage(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
for i := range episodes.Data {
|
for i := range episodes.Data {
|
||||||
episodes.Data[i].Images.Jpg.ImageURL = episodes.Data[i].GetFallbackImage(id)
|
if episodes.Data[i].Images == nil {
|
||||||
|
episodes.Data[i].Images = &jikan.EpisodeImages{}
|
||||||
}
|
}
|
||||||
|
wg.Add(1)
|
||||||
|
go func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
episodes.Data[idx].Images.Jpg.ImageURL = episodes.Data[idx].GetFallbackImage(id)
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
sort.Slice(episodes.Data, func(i, j int) bool {
|
sort.Slice(episodes.Data, func(i, j int) bool {
|
||||||
return episodes.Data[i].MalID < episodes.Data[j].MalID
|
return episodes.Data[i].MalID < episodes.Data[j].MalID
|
||||||
|
|||||||
@@ -38,16 +38,16 @@ func (e *Episode) GetFallbackImage(animeID int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always trigger scraping if we encounter the banned icon OR the generic placeholder
|
// Always trigger scraping if we encounter the banned icon OR the generic placeholder
|
||||||
if imageUrl != bannedImageURL && imageUrl != placeholderImageURL && imageUrl != "" {
|
// OR if there is no image URL at all
|
||||||
return imageUrl
|
if imageUrl == bannedImageURL || imageUrl == placeholderImageURL || imageUrl == "" {
|
||||||
}
|
// MAL URLs usually follow this format, and it redirects to the slug version
|
||||||
|
episodeURL := fmt.Sprintf("https://myanimelist.net/anime/%d/_/episode/%d", animeID, episodeNum)
|
||||||
episodeURL := fmt.Sprintf("https://myanimelist.net/anime/%d/episode/%d", animeID, episodeNum)
|
|
||||||
fallbackURL := scrapeAnimeImageFromEpisodePage(episodeURL, episodeNum)
|
fallbackURL := scrapeAnimeImageFromEpisodePage(episodeURL, episodeNum)
|
||||||
|
|
||||||
if fallbackURL != "" {
|
if fallbackURL != "" {
|
||||||
return fallbackURL
|
return fallbackURL
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return imageUrl
|
return imageUrl
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,9 @@ func scrapeAnimeImageFromEpisodePage(episodeURL string, episodeNum int) string {
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Log the status code for debugging
|
||||||
if resp.StatusCode != 200 {
|
if resp.StatusCode != 200 {
|
||||||
|
// fmt.Printf("[DEBUG] Failed to fetch %s: Status %d\n", episodeURL, resp.StatusCode)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,11 +79,18 @@ func scrapeAnimeImageFromEpisodePage(episodeURL string, episodeNum int) string {
|
|||||||
|
|
||||||
html := string(body)
|
html := string(body)
|
||||||
|
|
||||||
// Look for the JSON data in MAL.episodeVideo.aroundVideos
|
// MAL sometimes redirects to a URL with a slug.
|
||||||
|
// The JSON object is very likely to be present in the full page.
|
||||||
// We extract the object {} containing "episode_number":X
|
// We extract the object {} containing "episode_number":X
|
||||||
episodeStr := strconv.Itoa(episodeNum)
|
episodeStr := strconv.Itoa(episodeNum)
|
||||||
objPattern := regexp.MustCompile(`\{[^{}]*"episode_number":\s*` + episodeStr + `[^{}]*\}`)
|
objPattern := regexp.MustCompile(`\{[^{}]*"episode_number":\s*` + episodeStr + `[^{}]*\}`)
|
||||||
match := objPattern.FindString(html)
|
match := objPattern.FindString(html)
|
||||||
|
if match == "" {
|
||||||
|
// Try a broader search if the strict one fails
|
||||||
|
objPattern = regexp.MustCompile(`\{[^}]*"episode_number":\s*` + episodeStr + `[^}]*\}`)
|
||||||
|
match = objPattern.FindString(html)
|
||||||
|
}
|
||||||
|
|
||||||
if match != "" {
|
if match != "" {
|
||||||
thumbRe := regexp.MustCompile(`"thumbnail":\s*"([^"]+)"`)
|
thumbRe := regexp.MustCompile(`"thumbnail":\s*"([^"]+)"`)
|
||||||
thumbMatch := thumbRe.FindStringSubmatch(match)
|
thumbMatch := thumbRe.FindStringSubmatch(match)
|
||||||
|
|||||||
@@ -166,17 +166,19 @@ type TopAnimeResponse struct {
|
|||||||
Pagination Pagination `json:"pagination"`
|
Pagination Pagination `json:"pagination"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EpisodeImages struct {
|
||||||
|
Jpg struct {
|
||||||
|
ImageURL string `json:"image_url"`
|
||||||
|
} `json:"jpg"`
|
||||||
|
}
|
||||||
|
|
||||||
type Episode struct {
|
type Episode struct {
|
||||||
MalID int `json:"mal_id"`
|
MalID int `json:"mal_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Episode string `json:"episode"`
|
Episode string `json:"episode"`
|
||||||
Filler bool `json:"filler"`
|
Filler bool `json:"filler"`
|
||||||
Recap bool `json:"recap"`
|
Recap bool `json:"recap"`
|
||||||
Images *struct {
|
Images *EpisodeImages `json:"images,omitempty"`
|
||||||
Jpg struct {
|
|
||||||
ImageURL string `json:"image_url"`
|
|
||||||
} `json:"jpg"`
|
|
||||||
} `json:"images,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EpisodesResponse struct {
|
type EpisodesResponse struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user