package anilist import ( "context" "github.com/samber/lo" "seanime/internal/util" "seanime/internal/util/limiter" "seanime/internal/util/result" "sync" ) type ( CompleteAnimeRelationTree struct { *result.Map[int, *CompleteAnime] } FetchMediaTreeRelation = string ) const ( FetchMediaTreeSequels FetchMediaTreeRelation = "sequels" FetchMediaTreePrequels FetchMediaTreeRelation = "prequels" FetchMediaTreeAll FetchMediaTreeRelation = "all" ) // NewCompleteAnimeRelationTree returns a new result.Map[int, *CompleteAnime]. // It is used to store the results of FetchMediaTree or FetchMediaTree calls. func NewCompleteAnimeRelationTree() *CompleteAnimeRelationTree { return &CompleteAnimeRelationTree{result.NewResultMap[int, *CompleteAnime]()} } func (m *BaseAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) { if m == nil { return nil } defer util.HandlePanicInModuleWithError("anilist/BaseAnime.FetchMediaTree", &err) rl.Wait() res, err := anilistClient.CompleteAnimeByID(context.Background(), &m.ID) if err != nil { return err } return res.GetMedia().FetchMediaTree(rel, anilistClient, rl, tree, cache) } // FetchMediaTree populates the CompleteAnimeRelationTree with the given media's sequels and prequels. // It also takes a CompleteAnimeCache to store the fetched media in and avoid duplicate fetches. // It also takes a limiter.Limiter to limit the number of requests made to the AniList API. func (m *CompleteAnime) FetchMediaTree(rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) (err error) { if m == nil { return nil } defer util.HandlePanicInModuleWithError("anilist/CompleteAnime.FetchMediaTree", &err) if tree.Has(m.ID) { cache.Set(m.ID, m) return nil } cache.Set(m.ID, m) tree.Set(m.ID, m) if m.Relations == nil { return nil } // Get all edges edges := m.GetRelations().GetEdges() // Filter edges edges = lo.Filter(edges, func(_edge *CompleteAnime_Relations_Edges, _ int) bool { return (*_edge.RelationType == MediaRelationSequel || *_edge.RelationType == MediaRelationPrequel) && *_edge.GetNode().Status != MediaStatusNotYetReleased && _edge.IsBroadRelationFormat() && !tree.Has(_edge.GetNode().ID) }) if len(edges) == 0 { return nil } doneCh := make(chan struct{}) processEdges(edges, rel, anilistClient, rl, tree, cache, doneCh) for { select { case <-doneCh: return nil default: } } } // processEdges fetches the next node(s) for each edge in parallel. func processEdges(edges []*CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache, doneCh chan struct{}) { var wg sync.WaitGroup wg.Add(len(edges)) for i, item := range edges { go func(edge *CompleteAnime_Relations_Edges, _ int) { defer wg.Done() if edge == nil { return } processEdge(edge, rel, anilistClient, rl, tree, cache) }(item, i) } wg.Wait() go func() { close(doneCh) }() } func processEdge(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation, anilistClient AnilistClient, rl *limiter.Limiter, tree *CompleteAnimeRelationTree, cache *CompleteAnimeCache) { defer util.HandlePanicInModuleThen("anilist/processEdge", func() {}) cacheV, ok := cache.Get(edge.GetNode().ID) edgeCompleteAnime := cacheV if !ok { rl.Wait() // Fetch the next node res, err := anilistClient.CompleteAnimeByID(context.Background(), &edge.GetNode().ID) if err == nil { edgeCompleteAnime = res.GetMedia() cache.Set(edgeCompleteAnime.ID, edgeCompleteAnime) } } if edgeCompleteAnime == nil { return } // Get the relation type to fetch for the next node edgeRel := getEdgeRelation(edge, rel) // Fetch the next node(s) err := edgeCompleteAnime.FetchMediaTree(edgeRel, anilistClient, rl, tree, cache) if err != nil { return } } // getEdgeRelation returns the relation to fetch for the next node based on the current edge and the relation to fetch. // If the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreePrequels for prequels and FetchMediaTreeSequels for sequels. // // For example, if the current node is a sequel and the relation to fetch is FetchMediaTreeAll, it will return FetchMediaTreeSequels so that // only sequels are fetched for the next node. func getEdgeRelation(edge *CompleteAnime_Relations_Edges, rel FetchMediaTreeRelation) FetchMediaTreeRelation { if rel == FetchMediaTreeAll { if *edge.RelationType == MediaRelationPrequel { return FetchMediaTreePrequels } if *edge.RelationType == MediaRelationSequel { return FetchMediaTreeSequels } } return rel }