package anime import ( "fmt" "seanime/internal/api/anilist" "seanime/internal/api/metadata" "seanime/internal/hook" "seanime/internal/util/limiter" "sort" "time" "github.com/samber/lo" lop "github.com/samber/lo/parallel" "github.com/sourcegraph/conc/pool" ) type ( MissingEpisodes struct { Episodes []*Episode `json:"episodes"` SilencedEpisodes []*Episode `json:"silencedEpisodes"` } NewMissingEpisodesOptions struct { AnimeCollection *anilist.AnimeCollection LocalFiles []*LocalFile SilencedMediaIds []int MetadataProvider metadata.Provider } ) func NewMissingEpisodes(opts *NewMissingEpisodesOptions) *MissingEpisodes { missing := new(MissingEpisodes) reqEvent := new(MissingEpisodesRequestedEvent) reqEvent.AnimeCollection = opts.AnimeCollection reqEvent.LocalFiles = opts.LocalFiles reqEvent.SilencedMediaIds = opts.SilencedMediaIds reqEvent.MissingEpisodes = missing err := hook.GlobalHookManager.OnMissingEpisodesRequested().Trigger(reqEvent) if err != nil { return nil } opts.AnimeCollection = reqEvent.AnimeCollection // Override the anime collection opts.LocalFiles = reqEvent.LocalFiles // Override the local files opts.SilencedMediaIds = reqEvent.SilencedMediaIds // Override the silenced media IDs missing = reqEvent.MissingEpisodes // Default prevented by hook, return the missing episodes if reqEvent.DefaultPrevented { event := new(MissingEpisodesEvent) event.MissingEpisodes = missing err = hook.GlobalHookManager.OnMissingEpisodes().Trigger(event) if err != nil { return nil } return event.MissingEpisodes } groupedLfs := GroupLocalFilesByMediaID(opts.LocalFiles) rateLimiter := limiter.NewLimiter(time.Second, 20) p := pool.NewWithResults[[]*EntryDownloadEpisode]() for mId, lfs := range groupedLfs { p.Go(func() []*EntryDownloadEpisode { entry, found := opts.AnimeCollection.GetListEntryFromAnimeId(mId) if !found { return nil } // Skip if the status is nil, dropped or completed if entry.Status == nil || *entry.Status == anilist.MediaListStatusDropped || *entry.Status == anilist.MediaListStatusCompleted { return nil } latestLf, found := FindLatestLocalFileFromGroup(lfs) if !found { return nil } //If the latest local file is the same or higher than the current episode count, skip if entry.Media.GetCurrentEpisodeCount() == -1 || entry.Media.GetCurrentEpisodeCount() <= latestLf.GetEpisodeNumber() { return nil } rateLimiter.Wait() // Fetch anime metadata animeMetadata, err := opts.MetadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, entry.Media.ID) if err != nil { return nil } // Get download info downloadInfo, err := NewEntryDownloadInfo(&NewEntryDownloadInfoOptions{ LocalFiles: lfs, AnimeMetadata: animeMetadata, Media: entry.Media, Progress: entry.Progress, Status: entry.Status, MetadataProvider: opts.MetadataProvider, }) if err != nil { return nil } episodes := downloadInfo.EpisodesToDownload sort.Slice(episodes, func(i, j int) bool { return episodes[i].Episode.GetEpisodeNumber() < episodes[j].Episode.GetEpisodeNumber() }) // If there are more than 1 episode to download, modify the name of the first episode if len(episodes) > 1 { episodes = episodes[:1] // keep the first episode if episodes[0].Episode != nil { episodes[0].Episode.DisplayTitle = episodes[0].Episode.DisplayTitle + fmt.Sprintf(" & %d more", len(downloadInfo.EpisodesToDownload)-1) } } return episodes }) } epsToDownload := p.Wait() epsToDownload = lo.Filter(epsToDownload, func(item []*EntryDownloadEpisode, _ int) bool { return item != nil }) // Flatten flattenedEpsToDownload := lo.Flatten(epsToDownload) eps := lop.Map(flattenedEpsToDownload, func(item *EntryDownloadEpisode, _ int) *Episode { return item.Episode }) // Sort sort.Slice(eps, func(i, j int) bool { return eps[i].GetEpisodeNumber() < eps[j].GetEpisodeNumber() }) sort.Slice(eps, func(i, j int) bool { return eps[i].BaseAnime.ID < eps[j].BaseAnime.ID }) missing.Episodes = lo.Filter(eps, func(item *Episode, _ int) bool { return !lo.Contains(opts.SilencedMediaIds, item.BaseAnime.ID) }) missing.SilencedEpisodes = lo.Filter(eps, func(item *Episode, _ int) bool { return lo.Contains(opts.SilencedMediaIds, item.BaseAnime.ID) }) // Event event := new(MissingEpisodesEvent) event.MissingEpisodes = missing err = hook.GlobalHookManager.OnMissingEpisodes().Trigger(event) if err != nil { return nil } return event.MissingEpisodes }