package handlers import ( "errors" "seanime/internal/api/anilist" "seanime/internal/database/db_bridge" "seanime/internal/library/anime" "seanime/internal/torrentstream" "seanime/internal/util" "seanime/internal/util/result" "time" "github.com/labstack/echo/v4" ) // HandleGetLibraryCollection // // @summary returns the main local anime collection. // @desc This creates a new LibraryCollection struct and returns it. // @desc This is used to get the main anime collection of the user. // @desc It uses the cached Anilist anime collection for the GET method. // @desc It refreshes the AniList anime collection if the POST method is used. // @route /api/v1/library/collection [GET,POST] // @returns anime.LibraryCollection func (h *Handler) HandleGetLibraryCollection(c echo.Context) error { animeCollection, err := h.App.GetAnimeCollection(false) if err != nil { return h.RespondWithError(c, err) } if animeCollection == nil { return h.RespondWithData(c, &anime.LibraryCollection{}) } originalAnimeCollection := animeCollection var lfs []*anime.LocalFile nakamaLibrary, fromNakama := h.App.NakamaManager.GetHostAnimeLibrary() if fromNakama { // Save the original anime collection to restore it later originalAnimeCollection = animeCollection.Copy() lfs = nakamaLibrary.LocalFiles // Merge missing media entries into the collection currentMediaIds := make(map[int]struct{}) for _, list := range animeCollection.MediaListCollection.GetLists() { for _, entry := range list.GetEntries() { currentMediaIds[entry.GetMedia().GetID()] = struct{}{} } } nakamaMediaIds := make(map[int]struct{}) for _, lf := range lfs { if lf.MediaId > 0 { nakamaMediaIds[lf.MediaId] = struct{}{} } } missingMediaIds := make(map[int]struct{}) for _, lf := range lfs { if lf.MediaId > 0 { if _, ok := currentMediaIds[lf.MediaId]; !ok { missingMediaIds[lf.MediaId] = struct{}{} } } } for _, list := range nakamaLibrary.AnimeCollection.MediaListCollection.GetLists() { for _, entry := range list.GetEntries() { if _, ok := missingMediaIds[entry.GetMedia().GetID()]; ok { // create a new entry with blank list data newEntry := &anilist.AnimeListEntry{ ID: entry.GetID(), Media: entry.GetMedia(), Status: &[]anilist.MediaListStatus{anilist.MediaListStatusPlanning}[0], } animeCollection.MediaListCollection.AddEntryToList(newEntry, anilist.MediaListStatusPlanning) } } } } else { lfs, _, err = db_bridge.GetLocalFiles(h.App.Database) if err != nil { return h.RespondWithError(c, err) } } libraryCollection, err := anime.NewLibraryCollection(c.Request().Context(), &anime.NewLibraryCollectionOptions{ AnimeCollection: animeCollection, Platform: h.App.AnilistPlatform, LocalFiles: lfs, MetadataProvider: h.App.MetadataProvider, }) if err != nil { return h.RespondWithError(c, err) } // Restore the original anime collection if it was modified if fromNakama { *animeCollection = *originalAnimeCollection } if !fromNakama { if (h.App.SecondarySettings.Torrentstream != nil && h.App.SecondarySettings.Torrentstream.Enabled && h.App.SecondarySettings.Torrentstream.IncludeInLibrary) || (h.App.Settings.GetLibrary() != nil && h.App.Settings.GetLibrary().EnableOnlinestream && h.App.Settings.GetLibrary().IncludeOnlineStreamingInLibrary) || (h.App.SecondarySettings.Debrid != nil && h.App.SecondarySettings.Debrid.Enabled && h.App.SecondarySettings.Debrid.IncludeDebridStreamInLibrary) { h.App.TorrentstreamRepository.HydrateStreamCollection(&torrentstream.HydrateStreamCollectionOptions{ AnimeCollection: animeCollection, LibraryCollection: libraryCollection, MetadataProvider: h.App.MetadataProvider, }) } } // Add and remove necessary metadata when hydrating from Nakama if fromNakama { for _, ep := range libraryCollection.ContinueWatchingList { ep.IsNakamaEpisode = true } for _, list := range libraryCollection.Lists { for _, entry := range list.Entries { if entry.EntryLibraryData == nil { continue } entry.NakamaEntryLibraryData = &anime.NakamaEntryLibraryData{ UnwatchedCount: entry.EntryLibraryData.UnwatchedCount, MainFileCount: entry.EntryLibraryData.MainFileCount, } entry.EntryLibraryData = nil } } } // Hydrate total library size if libraryCollection != nil && libraryCollection.Stats != nil { libraryCollection.Stats.TotalSize = util.Bytes(h.App.TotalLibrarySize) } return h.RespondWithData(c, libraryCollection) } //---------------------------------------------------------------------------------------------------------------------------------------------------- var animeScheduleCache = result.NewCache[int, []*anime.ScheduleItem]() // HandleGetAnimeCollectionSchedule // // @summary returns anime collection schedule // @desc This is used by the "Schedule" page to display the anime schedule. // @route /api/v1/library/schedule [GET] // @returns []anime.ScheduleItem func (h *Handler) HandleGetAnimeCollectionSchedule(c echo.Context) error { // Invalidate the cache when the Anilist collection is refreshed h.App.AddOnRefreshAnilistCollectionFunc("HandleGetAnimeCollectionSchedule", func() { animeScheduleCache.Clear() }) if ret, ok := animeScheduleCache.Get(1); ok { return h.RespondWithData(c, ret) } animeSchedule, err := h.App.AnilistPlatform.GetAnimeAiringSchedule(c.Request().Context()) if err != nil { return h.RespondWithError(c, err) } animeCollection, err := h.App.GetAnimeCollection(false) if err != nil { return h.RespondWithError(c, err) } ret := anime.GetScheduleItems(animeSchedule, animeCollection) animeScheduleCache.SetT(1, ret, 1*time.Hour) return h.RespondWithData(c, ret) } // HandleAddUnknownMedia // // @summary adds the given media to the user's AniList planning collections // @desc Since media not found in the user's AniList collection are not displayed in the library, this route is used to add them. // @desc The response is ignored in the frontend, the client should just refetch the entire library collection. // @route /api/v1/library/unknown-media [POST] // @returns anilist.AnimeCollection func (h *Handler) HandleAddUnknownMedia(c echo.Context) error { type body struct { MediaIds []int `json:"mediaIds"` } b := new(body) if err := c.Bind(b); err != nil { return h.RespondWithError(c, err) } // Add non-added media entries to AniList collection if err := h.App.AnilistPlatform.AddMediaToCollection(c.Request().Context(), b.MediaIds); err != nil { return h.RespondWithError(c, errors.New("error: Anilist responded with an error, this is most likely a rate limit issue")) } // Bypass the cache animeCollection, err := h.App.GetAnimeCollection(true) if err != nil { return h.RespondWithError(c, errors.New("error: Anilist responded with an error, wait one minute before refreshing")) } return h.RespondWithData(c, animeCollection) }