node build fixed
This commit is contained in:
@@ -0,0 +1,768 @@
|
||||
package anilist_platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/util/limiter"
|
||||
"seanime/internal/util/result"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
type (
|
||||
AnilistPlatform struct {
|
||||
logger *zerolog.Logger
|
||||
username mo.Option[string]
|
||||
anilistClient anilist.AnilistClient
|
||||
animeCollection mo.Option[*anilist.AnimeCollection]
|
||||
rawAnimeCollection mo.Option[*anilist.AnimeCollection]
|
||||
mangaCollection mo.Option[*anilist.MangaCollection]
|
||||
rawMangaCollection mo.Option[*anilist.MangaCollection]
|
||||
isOffline bool
|
||||
offlinePlatformEnabled bool
|
||||
baseAnimeCache *result.BoundedCache[int, *anilist.BaseAnime]
|
||||
}
|
||||
)
|
||||
|
||||
func NewAnilistPlatform(anilistClient anilist.AnilistClient, logger *zerolog.Logger) platform.Platform {
|
||||
ap := &AnilistPlatform{
|
||||
anilistClient: anilistClient,
|
||||
logger: logger,
|
||||
username: mo.None[string](),
|
||||
animeCollection: mo.None[*anilist.AnimeCollection](),
|
||||
rawAnimeCollection: mo.None[*anilist.AnimeCollection](),
|
||||
mangaCollection: mo.None[*anilist.MangaCollection](),
|
||||
rawMangaCollection: mo.None[*anilist.MangaCollection](),
|
||||
baseAnimeCache: result.NewBoundedCache[int, *anilist.BaseAnime](50),
|
||||
}
|
||||
|
||||
return ap
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) clearCache() {
|
||||
ap.baseAnimeCache.Clear()
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) SetUsername(username string) {
|
||||
// Set the username for the AnilistPlatform
|
||||
if username == "" {
|
||||
ap.username = mo.Some[string]("")
|
||||
return
|
||||
}
|
||||
|
||||
ap.username = mo.Some(username)
|
||||
return
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) SetAnilistClient(client anilist.AnilistClient) {
|
||||
// Set the AnilistClient for the AnilistPlatform
|
||||
ap.anilistClient = client
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) UpdateEntry(ctx context.Context, mediaID int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
ap.logger.Trace().Msg("anilist platform: Updating entry")
|
||||
|
||||
event := new(PreUpdateEntryEvent)
|
||||
event.MediaID = &mediaID
|
||||
event.Status = status
|
||||
event.ScoreRaw = scoreRaw
|
||||
event.Progress = progress
|
||||
event.StartedAt = startedAt
|
||||
event.CompletedAt = completedAt
|
||||
|
||||
err := hook.GlobalHookManager.OnPreUpdateEntry().Trigger(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event.DefaultPrevented {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = ap.anilistClient.UpdateMediaListEntry(ctx, event.MediaID, event.Status, event.ScoreRaw, event.Progress, event.StartedAt, event.CompletedAt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postEvent := new(PostUpdateEntryEvent)
|
||||
postEvent.MediaID = &mediaID
|
||||
|
||||
err = hook.GlobalHookManager.OnPostUpdateEntry().Trigger(postEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) UpdateEntryProgress(ctx context.Context, mediaID int, progress int, totalCount *int) error {
|
||||
ap.logger.Trace().Msg("anilist platform: Updating entry progress")
|
||||
|
||||
event := new(PreUpdateEntryProgressEvent)
|
||||
event.MediaID = &mediaID
|
||||
event.Progress = &progress
|
||||
event.TotalCount = totalCount
|
||||
event.Status = lo.ToPtr(anilist.MediaListStatusCurrent)
|
||||
|
||||
err := hook.GlobalHookManager.OnPreUpdateEntryProgress().Trigger(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event.DefaultPrevented {
|
||||
return nil
|
||||
}
|
||||
|
||||
realTotalCount := 0
|
||||
if totalCount != nil && *totalCount > 0 {
|
||||
realTotalCount = *totalCount
|
||||
}
|
||||
|
||||
// Check if the anime is in the repeating list
|
||||
// If it is, set the status to repeating
|
||||
if ap.rawAnimeCollection.IsPresent() {
|
||||
for _, list := range ap.rawAnimeCollection.MustGet().MediaListCollection.Lists {
|
||||
if list.Status != nil && *list.Status == anilist.MediaListStatusRepeating {
|
||||
if list.Entries != nil {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
*event.Status = anilist.MediaListStatusRepeating
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if realTotalCount > 0 && progress >= realTotalCount {
|
||||
*event.Status = anilist.MediaListStatusCompleted
|
||||
}
|
||||
|
||||
if realTotalCount > 0 && progress > realTotalCount {
|
||||
*event.Progress = realTotalCount
|
||||
}
|
||||
|
||||
_, err = ap.anilistClient.UpdateMediaListEntryProgress(
|
||||
ctx,
|
||||
event.MediaID,
|
||||
event.Progress,
|
||||
event.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postEvent := new(PostUpdateEntryProgressEvent)
|
||||
postEvent.MediaID = &mediaID
|
||||
|
||||
err = hook.GlobalHookManager.OnPostUpdateEntryProgress().Trigger(postEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) UpdateEntryRepeat(ctx context.Context, mediaID int, repeat int) error {
|
||||
ap.logger.Trace().Msg("anilist platform: Updating entry repeat")
|
||||
|
||||
event := new(PreUpdateEntryRepeatEvent)
|
||||
event.MediaID = &mediaID
|
||||
event.Repeat = &repeat
|
||||
|
||||
err := hook.GlobalHookManager.OnPreUpdateEntryRepeat().Trigger(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if event.DefaultPrevented {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = ap.anilistClient.UpdateMediaListEntryRepeat(ctx, event.MediaID, event.Repeat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
postEvent := new(PostUpdateEntryRepeatEvent)
|
||||
postEvent.MediaID = &mediaID
|
||||
|
||||
err = hook.GlobalHookManager.OnPostUpdateEntryRepeat().Trigger(postEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) DeleteEntry(ctx context.Context, mediaID int) error {
|
||||
ap.logger.Trace().Msg("anilist platform: Deleting entry")
|
||||
_, err := ap.anilistClient.DeleteEntry(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnime(ctx context.Context, mediaID int) (*anilist.BaseAnime, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching anime")
|
||||
|
||||
//if cachedAnime, ok := ap.baseAnimeCache.Get(mediaID); ok {
|
||||
// ap.logger.Trace().Msg("anilist platform: Returning anime from cache")
|
||||
// event := new(GetAnimeEvent)
|
||||
// event.Anime = cachedAnime
|
||||
// err := hook.GlobalHookManager.OnGetAnime().Trigger(event)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return event.Anime, nil
|
||||
//}
|
||||
|
||||
ret, err := ap.anilistClient.BaseAnimeByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
media := ret.GetMedia()
|
||||
|
||||
event := new(GetAnimeEvent)
|
||||
event.Anime = media
|
||||
|
||||
err = hook.GlobalHookManager.OnGetAnime().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//ap.baseAnimeCache.SetT(mediaID, event.Anime, time.Minute*30)
|
||||
|
||||
return event.Anime, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeByMalID(ctx context.Context, malID int) (*anilist.BaseAnime, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching anime by MAL ID")
|
||||
ret, err := ap.anilistClient.BaseAnimeByMalID(ctx, &malID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
media := ret.GetMedia()
|
||||
|
||||
event := new(GetAnimeEvent)
|
||||
event.Anime = media
|
||||
|
||||
err = hook.GlobalHookManager.OnGetAnime().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.Anime, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeDetails(ctx context.Context, mediaID int) (*anilist.AnimeDetailsById_Media, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching anime details")
|
||||
ret, err := ap.anilistClient.AnimeDetailsByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
media := ret.GetMedia()
|
||||
|
||||
event := new(GetAnimeDetailsEvent)
|
||||
event.Anime = media
|
||||
|
||||
err = hook.GlobalHookManager.OnGetAnimeDetails().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.Anime, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeWithRelations(ctx context.Context, mediaID int) (*anilist.CompleteAnime, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching anime with relations")
|
||||
ret, err := ap.anilistClient.CompleteAnimeByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetManga(ctx context.Context, mediaID int) (*anilist.BaseManga, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching manga")
|
||||
ret, err := ap.anilistClient.BaseMangaByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
media := ret.GetMedia()
|
||||
|
||||
event := new(GetMangaEvent)
|
||||
event.Manga = media
|
||||
|
||||
err = hook.GlobalHookManager.OnGetManga().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.Manga, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetMangaDetails(ctx context.Context, mediaID int) (*anilist.MangaDetailsById_Media, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching manga details")
|
||||
ret, err := ap.anilistClient.MangaDetailsByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
if !bypassCache && ap.animeCollection.IsPresent() {
|
||||
event := new(GetCachedAnimeCollectionEvent)
|
||||
event.AnimeCollection = ap.animeCollection.MustGet()
|
||||
err := hook.GlobalHookManager.OnGetCachedAnimeCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.AnimeCollection, nil
|
||||
}
|
||||
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshAnimeCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetAnimeCollectionEvent)
|
||||
event.AnimeCollection = ap.animeCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetAnimeCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.AnimeCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetRawAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
if !bypassCache && ap.rawAnimeCollection.IsPresent() {
|
||||
event := new(GetCachedRawAnimeCollectionEvent)
|
||||
event.AnimeCollection = ap.rawAnimeCollection.MustGet()
|
||||
err := hook.GlobalHookManager.OnGetCachedRawAnimeCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.AnimeCollection, nil
|
||||
}
|
||||
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshAnimeCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetRawAnimeCollectionEvent)
|
||||
event.AnimeCollection = ap.rawAnimeCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetRawAnimeCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.AnimeCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) RefreshAnimeCollection(ctx context.Context) (*anilist.AnimeCollection, error) {
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshAnimeCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetAnimeCollectionEvent)
|
||||
event.AnimeCollection = ap.animeCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetAnimeCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event2 := new(GetRawAnimeCollectionEvent)
|
||||
event2.AnimeCollection = ap.rawAnimeCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetRawAnimeCollection().Trigger(event2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.AnimeCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) refreshAnimeCollection(ctx context.Context) error {
|
||||
if ap.username.IsAbsent() {
|
||||
return errors.New("anilist: Username is not set")
|
||||
}
|
||||
|
||||
// Else, get the collection from Anilist
|
||||
collection, err := ap.anilistClient.AnimeCollection(ctx, ap.username.ToPointer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the raw collection to App (retains the lists with no status)
|
||||
collectionCopy := *collection
|
||||
ap.rawAnimeCollection = mo.Some(&collectionCopy)
|
||||
listCollectionCopy := *collection.MediaListCollection
|
||||
ap.rawAnimeCollection.MustGet().MediaListCollection = &listCollectionCopy
|
||||
listsCopy := make([]*anilist.AnimeCollection_MediaListCollection_Lists, len(collection.MediaListCollection.Lists))
|
||||
copy(listsCopy, collection.MediaListCollection.Lists)
|
||||
ap.rawAnimeCollection.MustGet().MediaListCollection.Lists = listsCopy
|
||||
|
||||
// Remove lists with no status (custom lists)
|
||||
collection.MediaListCollection.Lists = lo.Filter(collection.MediaListCollection.Lists, func(list *anilist.AnimeCollection_MediaListCollection_Lists, _ int) bool {
|
||||
return list.Status != nil
|
||||
})
|
||||
|
||||
// Save the collection to App
|
||||
ap.animeCollection = mo.Some(collection)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeCollectionWithRelations(ctx context.Context) (*anilist.AnimeCollectionWithRelations, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching anime collection with relations")
|
||||
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ret, err := ap.anilistClient.AnimeCollectionWithRelations(ctx, ap.username.ToPointer())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
|
||||
if !bypassCache && ap.mangaCollection.IsPresent() {
|
||||
event := new(GetCachedMangaCollectionEvent)
|
||||
event.MangaCollection = ap.mangaCollection.MustGet()
|
||||
err := hook.GlobalHookManager.OnGetCachedMangaCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.MangaCollection, nil
|
||||
}
|
||||
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshMangaCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetMangaCollectionEvent)
|
||||
event.MangaCollection = ap.mangaCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetMangaCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.MangaCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetRawMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching raw manga collection")
|
||||
|
||||
if !bypassCache && ap.rawMangaCollection.IsPresent() {
|
||||
ap.logger.Trace().Msg("anilist platform: Returning raw manga collection from cache")
|
||||
event := new(GetCachedRawMangaCollectionEvent)
|
||||
event.MangaCollection = ap.rawMangaCollection.MustGet()
|
||||
err := hook.GlobalHookManager.OnGetCachedRawMangaCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return event.MangaCollection, nil
|
||||
}
|
||||
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshMangaCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetRawMangaCollectionEvent)
|
||||
event.MangaCollection = ap.rawMangaCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetRawMangaCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.MangaCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) RefreshMangaCollection(ctx context.Context) (*anilist.MangaCollection, error) {
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
err := ap.refreshMangaCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetMangaCollectionEvent)
|
||||
event.MangaCollection = ap.mangaCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetMangaCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event2 := new(GetRawMangaCollectionEvent)
|
||||
event2.MangaCollection = ap.rawMangaCollection.MustGet()
|
||||
|
||||
err = hook.GlobalHookManager.OnGetRawMangaCollection().Trigger(event2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.MangaCollection, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) refreshMangaCollection(ctx context.Context) error {
|
||||
if ap.username.IsAbsent() {
|
||||
return errors.New("anilist: Username is not set")
|
||||
}
|
||||
|
||||
collection, err := ap.anilistClient.MangaCollection(ctx, ap.username.ToPointer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the raw collection to App (retains the lists with no status)
|
||||
collectionCopy := *collection
|
||||
ap.rawMangaCollection = mo.Some(&collectionCopy)
|
||||
listCollectionCopy := *collection.MediaListCollection
|
||||
ap.rawMangaCollection.MustGet().MediaListCollection = &listCollectionCopy
|
||||
listsCopy := make([]*anilist.MangaCollection_MediaListCollection_Lists, len(collection.MediaListCollection.Lists))
|
||||
copy(listsCopy, collection.MediaListCollection.Lists)
|
||||
ap.rawMangaCollection.MustGet().MediaListCollection.Lists = listsCopy
|
||||
|
||||
// Remove lists with no status (custom lists)
|
||||
collection.MediaListCollection.Lists = lo.Filter(collection.MediaListCollection.Lists, func(list *anilist.MangaCollection_MediaListCollection_Lists, _ int) bool {
|
||||
return list.Status != nil
|
||||
})
|
||||
|
||||
// Remove Novels from both collections
|
||||
for _, list := range collection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetFormat() != nil && *entry.GetMedia().GetFormat() == anilist.MediaFormatNovel {
|
||||
list.Entries = lo.Filter(list.Entries, func(e *anilist.MangaCollection_MediaListCollection_Lists_Entries, _ int) bool {
|
||||
return *e.GetMedia().GetFormat() != anilist.MediaFormatNovel
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, list := range ap.rawMangaCollection.MustGet().MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetFormat() != nil && *entry.GetMedia().GetFormat() == anilist.MediaFormatNovel {
|
||||
list.Entries = lo.Filter(list.Entries, func(e *anilist.MangaCollection_MediaListCollection_Lists_Entries, _ int) bool {
|
||||
return *e.GetMedia().GetFormat() != anilist.MediaFormatNovel
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the collection to App
|
||||
ap.mangaCollection = mo.Some(collection)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) AddMediaToCollection(ctx context.Context, mIds []int) error {
|
||||
ap.logger.Trace().Msg("anilist platform: Adding media to collection")
|
||||
if len(mIds) == 0 {
|
||||
ap.logger.Debug().Msg("anilist: No media added to planning list")
|
||||
return nil
|
||||
}
|
||||
|
||||
rateLimiter := limiter.NewLimiter(1*time.Second, 1) // 1 request per second
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
for _, _id := range mIds {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
rateLimiter.Wait()
|
||||
defer wg.Done()
|
||||
_, err := ap.anilistClient.UpdateMediaListEntry(
|
||||
ctx,
|
||||
&id,
|
||||
lo.ToPtr(anilist.MediaListStatusPlanning),
|
||||
lo.ToPtr(0),
|
||||
lo.ToPtr(0),
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
ap.logger.Error().Msg("anilist: An error occurred while adding media to planning list: " + err.Error())
|
||||
}
|
||||
}(_id)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
ap.logger.Debug().Any("count", len(mIds)).Msg("anilist: Media added to planning list")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetStudioDetails(ctx context.Context, studioID int) (*anilist.StudioDetails, error) {
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching studio details")
|
||||
ret, err := ap.anilistClient.StudioDetails(ctx, &studioID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
event := new(GetStudioDetailsEvent)
|
||||
event.Studio = ret
|
||||
|
||||
err = hook.GlobalHookManager.OnGetStudioDetails().Trigger(event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return event.Studio, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnilistClient() anilist.AnilistClient {
|
||||
return ap.anilistClient
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetViewerStats(ctx context.Context) (*anilist.ViewerStats, error) {
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, errors.New("anilist: Username is not set")
|
||||
}
|
||||
|
||||
ap.logger.Trace().Msg("anilist platform: Fetching viewer stats")
|
||||
ret, err := ap.anilistClient.ViewerStats(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (ap *AnilistPlatform) GetAnimeAiringSchedule(ctx context.Context) (*anilist.AnimeAiringSchedule, error) {
|
||||
if ap.username.IsAbsent() {
|
||||
return nil, errors.New("anilist: Username is not set")
|
||||
}
|
||||
|
||||
collection, err := ap.GetAnimeCollection(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mediaIds := make([]*int, 0)
|
||||
for _, list := range collection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
mediaIds = append(mediaIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
}
|
||||
}
|
||||
|
||||
var ret *anilist.AnimeAiringSchedule
|
||||
|
||||
now := time.Now()
|
||||
currentSeason, currentSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindCurrent)
|
||||
previousSeason, previousSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindPrevious)
|
||||
nextSeason, nextSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindNext)
|
||||
|
||||
ret, err = ap.anilistClient.AnimeAiringSchedule(ctx, mediaIds, ¤tSeason, ¤tSeasonYear, &previousSeason, &previousSeasonYear, &nextSeason, &nextSeasonYear)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type animeScheduleMedia interface {
|
||||
GetMedia() []*anilist.AnimeSchedule
|
||||
}
|
||||
|
||||
foundIds := make(map[int]struct{})
|
||||
addIds := func(n animeScheduleMedia) {
|
||||
for _, m := range n.GetMedia() {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
foundIds[m.GetID()] = struct{}{}
|
||||
}
|
||||
}
|
||||
addIds(ret.GetOngoing())
|
||||
addIds(ret.GetOngoingNext())
|
||||
addIds(ret.GetPreceding())
|
||||
addIds(ret.GetUpcoming())
|
||||
addIds(ret.GetUpcomingNext())
|
||||
|
||||
missingIds := make([]*int, 0)
|
||||
for _, list := range collection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if _, found := foundIds[entry.GetMedia().GetID()]; found {
|
||||
continue
|
||||
}
|
||||
endDate := entry.GetMedia().GetEndDate()
|
||||
// Ignore if ended more than 2 months ago
|
||||
if endDate == nil || endDate.GetYear() == nil || endDate.GetMonth() == nil {
|
||||
missingIds = append(missingIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
continue
|
||||
}
|
||||
endTime := time.Date(*endDate.GetYear(), time.Month(*endDate.GetMonth()), 1, 0, 0, 0, 0, time.UTC)
|
||||
if endTime.Before(now.AddDate(0, -2, 0)) {
|
||||
continue
|
||||
}
|
||||
missingIds = append(missingIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingIds) > 0 {
|
||||
retB, err := ap.anilistClient.AnimeAiringScheduleRaw(ctx, missingIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(retB.GetPage().GetMedia()) > 0 {
|
||||
// Add to ongoing next
|
||||
for _, m := range retB.Page.GetMedia() {
|
||||
if ret.OngoingNext == nil {
|
||||
ret.OngoingNext = &anilist.AnimeAiringSchedule_OngoingNext{
|
||||
Media: make([]*anilist.AnimeSchedule, 0),
|
||||
}
|
||||
}
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ret.OngoingNext.Media = append(ret.OngoingNext.Media, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package anilist_platform
|
||||
|
||||
import (
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/hook_resolver"
|
||||
)
|
||||
|
||||
/////////////////////////////
|
||||
// AniList Events
|
||||
/////////////////////////////
|
||||
|
||||
type GetAnimeEvent struct {
|
||||
hook_resolver.Event
|
||||
Anime *anilist.BaseAnime `json:"anime"`
|
||||
}
|
||||
|
||||
type GetAnimeDetailsEvent struct {
|
||||
hook_resolver.Event
|
||||
Anime *anilist.AnimeDetailsById_Media `json:"anime"`
|
||||
}
|
||||
|
||||
type GetMangaEvent struct {
|
||||
hook_resolver.Event
|
||||
Manga *anilist.BaseManga `json:"manga"`
|
||||
}
|
||||
|
||||
type GetMangaDetailsEvent struct {
|
||||
hook_resolver.Event
|
||||
Manga *anilist.MangaDetailsById_Media `json:"manga"`
|
||||
}
|
||||
|
||||
type GetCachedAnimeCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
AnimeCollection *anilist.AnimeCollection `json:"animeCollection"`
|
||||
}
|
||||
|
||||
type GetCachedMangaCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
MangaCollection *anilist.MangaCollection `json:"mangaCollection"`
|
||||
}
|
||||
|
||||
type GetAnimeCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
AnimeCollection *anilist.AnimeCollection `json:"animeCollection"`
|
||||
}
|
||||
|
||||
type GetMangaCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
MangaCollection *anilist.MangaCollection `json:"mangaCollection"`
|
||||
}
|
||||
|
||||
type GetCachedRawAnimeCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
AnimeCollection *anilist.AnimeCollection `json:"animeCollection"`
|
||||
}
|
||||
|
||||
type GetCachedRawMangaCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
MangaCollection *anilist.MangaCollection `json:"mangaCollection"`
|
||||
}
|
||||
|
||||
type GetRawAnimeCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
AnimeCollection *anilist.AnimeCollection `json:"animeCollection"`
|
||||
}
|
||||
|
||||
type GetRawMangaCollectionEvent struct {
|
||||
hook_resolver.Event
|
||||
MangaCollection *anilist.MangaCollection `json:"mangaCollection"`
|
||||
}
|
||||
|
||||
type GetStudioDetailsEvent struct {
|
||||
hook_resolver.Event
|
||||
Studio *anilist.StudioDetails `json:"studio"`
|
||||
}
|
||||
|
||||
// PreUpdateEntryEvent is triggered when an entry is about to be updated.
|
||||
// Prevent default to skip the default update and override the update.
|
||||
type PreUpdateEntryEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
Status *anilist.MediaListStatus `json:"status"`
|
||||
ScoreRaw *int `json:"scoreRaw"`
|
||||
Progress *int `json:"progress"`
|
||||
StartedAt *anilist.FuzzyDateInput `json:"startedAt"`
|
||||
CompletedAt *anilist.FuzzyDateInput `json:"completedAt"`
|
||||
}
|
||||
|
||||
type PostUpdateEntryEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
}
|
||||
|
||||
// PreUpdateEntryProgressEvent is triggered when an entry's progress is about to be updated.
|
||||
// Prevent default to skip the default update and override the update.
|
||||
type PreUpdateEntryProgressEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
Progress *int `json:"progress"`
|
||||
TotalCount *int `json:"totalCount"`
|
||||
// Defaults to anilist.MediaListStatusCurrent
|
||||
Status *anilist.MediaListStatus `json:"status"`
|
||||
}
|
||||
|
||||
type PostUpdateEntryProgressEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
}
|
||||
|
||||
// PreUpdateEntryRepeatEvent is triggered when an entry's repeat is about to be updated.
|
||||
// Prevent default to skip the default update and override the update.
|
||||
type PreUpdateEntryRepeatEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
Repeat *int `json:"repeat"`
|
||||
}
|
||||
|
||||
type PostUpdateEntryRepeatEvent struct {
|
||||
hook_resolver.Event
|
||||
MediaID *int `json:"mediaId"`
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package offline_platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/local"
|
||||
"seanime/internal/platforms/platform"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoLocalAnimeCollection = errors.New("no local anime collection")
|
||||
ErrorNoLocalMangaCollection = errors.New("no local manga collection")
|
||||
// ErrMediaNotFound means the media wasn't found in the local collection
|
||||
ErrMediaNotFound = errors.New("media not found")
|
||||
// ErrActionNotSupported means the action isn't valid on the local platform
|
||||
ErrActionNotSupported = errors.New("action not supported")
|
||||
)
|
||||
|
||||
// OfflinePlatform used when offline.
|
||||
// It provides the same API as the anilist_platform.AnilistPlatform but some methods are no-op.
|
||||
type OfflinePlatform struct {
|
||||
logger *zerolog.Logger
|
||||
localManager local.Manager
|
||||
client anilist.AnilistClient
|
||||
}
|
||||
|
||||
func NewOfflinePlatform(localManager local.Manager, client anilist.AnilistClient, logger *zerolog.Logger) (platform.Platform, error) {
|
||||
ap := &OfflinePlatform{
|
||||
logger: logger,
|
||||
localManager: localManager,
|
||||
client: client,
|
||||
}
|
||||
|
||||
return ap, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (lp *OfflinePlatform) SetUsername(username string) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) SetAnilistClient(client anilist.AnilistClient) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
func rearrangeAnimeCollectionLists(animeCollection *anilist.AnimeCollection) {
|
||||
removedEntries := make([]*anilist.AnimeCollection_MediaListCollection_Lists_Entries, 0)
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
if list.GetStatus() == nil || list.GetEntries() == nil {
|
||||
continue
|
||||
}
|
||||
var indicesToRemove []int
|
||||
for idx, entry := range list.GetEntries() {
|
||||
if entry.GetStatus() == nil {
|
||||
continue
|
||||
}
|
||||
// Mark for removal if status differs
|
||||
if *list.GetStatus() != *entry.GetStatus() {
|
||||
indicesToRemove = append(indicesToRemove, idx)
|
||||
removedEntries = append(removedEntries, entry)
|
||||
}
|
||||
}
|
||||
// Remove entries in reverse order to avoid re-slicing issues
|
||||
for i := len(indicesToRemove) - 1; i >= 0; i-- {
|
||||
idx := indicesToRemove[i]
|
||||
list.Entries = append(list.Entries[:idx], list.Entries[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Add removed entries to the correct list
|
||||
for _, entry := range removedEntries {
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
if list.GetStatus() == nil {
|
||||
continue
|
||||
}
|
||||
if *list.GetStatus() == *entry.GetStatus() {
|
||||
list.Entries = append(list.Entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func rearrangeMangaCollectionLists(mangaCollection *anilist.MangaCollection) {
|
||||
removedEntries := make([]*anilist.MangaCollection_MediaListCollection_Lists_Entries, 0)
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
if list.GetStatus() == nil || list.GetEntries() == nil {
|
||||
continue
|
||||
}
|
||||
var indicesToRemove []int
|
||||
for idx, entry := range list.GetEntries() {
|
||||
if entry.GetStatus() == nil {
|
||||
continue
|
||||
}
|
||||
// Mark for removal if status differs
|
||||
if *list.GetStatus() != *entry.GetStatus() {
|
||||
indicesToRemove = append(indicesToRemove, idx)
|
||||
removedEntries = append(removedEntries, entry)
|
||||
}
|
||||
}
|
||||
// Remove entries in reverse order to avoid re-slicing issues
|
||||
for i := len(indicesToRemove) - 1; i >= 0; i-- {
|
||||
idx := indicesToRemove[i]
|
||||
list.Entries = append(list.Entries[:idx], list.Entries[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Add removed entries to the correct list
|
||||
for _, entry := range removedEntries {
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
if list.GetStatus() == nil {
|
||||
continue
|
||||
}
|
||||
if *list.GetStatus() == *entry.GetStatus() {
|
||||
list.Entries = append(list.Entries, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateEntry updates the entry for the given media ID.
|
||||
// It doesn't add the entry if it doesn't exist.
|
||||
func (lp *OfflinePlatform) UpdateEntry(ctx context.Context, mediaID int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
animeCollection := lp.localManager.GetLocalAnimeCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
if status != nil {
|
||||
entry.Status = status
|
||||
}
|
||||
if scoreRaw != nil {
|
||||
entry.Score = lo.ToPtr(float64(*scoreRaw))
|
||||
}
|
||||
if progress != nil {
|
||||
entry.Progress = progress
|
||||
}
|
||||
if startedAt != nil {
|
||||
entry.StartedAt = &anilist.AnimeCollection_MediaListCollection_Lists_Entries_StartedAt{
|
||||
Year: startedAt.Year,
|
||||
Month: startedAt.Month,
|
||||
Day: startedAt.Day,
|
||||
}
|
||||
}
|
||||
if completedAt != nil {
|
||||
entry.CompletedAt = &anilist.AnimeCollection_MediaListCollection_Lists_Entries_CompletedAt{
|
||||
Year: completedAt.Year,
|
||||
Month: completedAt.Month,
|
||||
Day: completedAt.Day,
|
||||
}
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
rearrangeAnimeCollectionLists(animeCollection)
|
||||
lp.localManager.UpdateLocalAnimeCollection(animeCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
mangaCollection := lp.localManager.GetLocalMangaCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
if status != nil {
|
||||
entry.Status = status
|
||||
}
|
||||
if scoreRaw != nil {
|
||||
entry.Score = lo.ToPtr(float64(*scoreRaw))
|
||||
}
|
||||
if progress != nil {
|
||||
entry.Progress = progress
|
||||
}
|
||||
if startedAt != nil {
|
||||
entry.StartedAt = &anilist.MangaCollection_MediaListCollection_Lists_Entries_StartedAt{
|
||||
Year: startedAt.Year,
|
||||
Month: startedAt.Month,
|
||||
Day: startedAt.Day,
|
||||
}
|
||||
}
|
||||
if completedAt != nil {
|
||||
entry.CompletedAt = &anilist.MangaCollection_MediaListCollection_Lists_Entries_CompletedAt{
|
||||
Year: completedAt.Year,
|
||||
Month: completedAt.Month,
|
||||
Day: completedAt.Day,
|
||||
}
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
rearrangeMangaCollectionLists(mangaCollection)
|
||||
lp.localManager.UpdateLocalMangaCollection(mangaCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) UpdateEntryProgress(ctx context.Context, mediaID int, progress int, totalEpisodes *int) error {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
animeCollection := lp.localManager.GetLocalAnimeCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
entry.Progress = &progress
|
||||
if totalEpisodes != nil {
|
||||
entry.Media.Episodes = totalEpisodes
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
rearrangeAnimeCollectionLists(animeCollection)
|
||||
lp.localManager.UpdateLocalAnimeCollection(animeCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
mangaCollection := lp.localManager.GetLocalMangaCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
entry.Progress = &progress
|
||||
if totalEpisodes != nil {
|
||||
entry.Media.Chapters = totalEpisodes
|
||||
}
|
||||
|
||||
// Save the collection
|
||||
rearrangeMangaCollectionLists(mangaCollection)
|
||||
lp.localManager.UpdateLocalMangaCollection(mangaCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) UpdateEntryRepeat(ctx context.Context, mediaID int, repeat int) error {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
animeCollection := lp.localManager.GetLocalAnimeCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
entry.Repeat = &repeat
|
||||
|
||||
// Save the collection
|
||||
rearrangeAnimeCollectionLists(animeCollection)
|
||||
lp.localManager.UpdateLocalAnimeCollection(animeCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
mangaCollection := lp.localManager.GetLocalMangaCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
// Update the entry
|
||||
entry.Repeat = &repeat
|
||||
|
||||
// Save the collection
|
||||
rearrangeMangaCollectionLists(mangaCollection)
|
||||
lp.localManager.UpdateLocalMangaCollection(mangaCollection)
|
||||
lp.localManager.SetHasLocalChanges(true)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
// DeleteEntry isn't supported for the local platform, always returns an error.
|
||||
func (lp *OfflinePlatform) DeleteEntry(ctx context.Context, mediaID int) error {
|
||||
return ErrActionNotSupported
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnime(ctx context.Context, mediaID int) (*anilist.BaseAnime, error) {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
animeCollection := lp.localManager.GetLocalAnimeCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
return entry.Media, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnimeByMalID(ctx context.Context, malID int) (*anilist.BaseAnime, error) {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
animeCollection := lp.localManager.GetLocalAnimeCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range animeCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetIDMal() != nil && *entry.GetMedia().GetIDMal() == malID {
|
||||
return entry.Media, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMediaNotFound
|
||||
}
|
||||
|
||||
// GetAnimeDetails isn't supported for the local platform, always returns an empty struct.
|
||||
func (lp *OfflinePlatform) GetAnimeDetails(ctx context.Context, mediaID int) (*anilist.AnimeDetailsById_Media, error) {
|
||||
return &anilist.AnimeDetailsById_Media{}, nil
|
||||
}
|
||||
|
||||
// GetAnimeWithRelations isn't supported for the local platform, always returns an error.
|
||||
func (lp *OfflinePlatform) GetAnimeWithRelations(ctx context.Context, mediaID int) (*anilist.CompleteAnime, error) {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetManga(ctx context.Context, mediaID int) (*anilist.BaseManga, error) {
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
mangaCollection := lp.localManager.GetLocalMangaCollection().MustGet()
|
||||
|
||||
// Find the entry
|
||||
for _, list := range mangaCollection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if entry.GetMedia().GetID() == mediaID {
|
||||
return entry.Media, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMediaNotFound
|
||||
}
|
||||
|
||||
// GetMangaDetails isn't supported for the local platform, always returns an empty struct.
|
||||
func (lp *OfflinePlatform) GetMangaDetails(ctx context.Context, mediaID int) (*anilist.MangaDetailsById_Media, error) {
|
||||
return &anilist.MangaDetailsById_Media{}, nil
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
return lp.localManager.GetLocalAnimeCollection().MustGet(), nil
|
||||
} else {
|
||||
return nil, ErrNoLocalAnimeCollection
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetRawAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
if lp.localManager.GetLocalAnimeCollection().IsPresent() {
|
||||
return lp.localManager.GetLocalAnimeCollection().MustGet(), nil
|
||||
} else {
|
||||
return nil, ErrNoLocalAnimeCollection
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshAnimeCollection is a no-op, always returns the local anime collection.
|
||||
func (lp *OfflinePlatform) RefreshAnimeCollection(ctx context.Context) (*anilist.AnimeCollection, error) {
|
||||
animeCollection, ok := lp.localManager.GetLocalAnimeCollection().Get()
|
||||
if !ok {
|
||||
return nil, ErrNoLocalAnimeCollection
|
||||
}
|
||||
|
||||
return animeCollection, nil
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnimeCollectionWithRelations(ctx context.Context) (*anilist.AnimeCollectionWithRelations, error) {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
return lp.localManager.GetLocalMangaCollection().MustGet(), nil
|
||||
} else {
|
||||
return nil, ErrorNoLocalMangaCollection
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetRawMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
if lp.localManager.GetLocalMangaCollection().IsPresent() {
|
||||
return lp.localManager.GetLocalMangaCollection().MustGet(), nil
|
||||
} else {
|
||||
return nil, ErrorNoLocalMangaCollection
|
||||
}
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) RefreshMangaCollection(ctx context.Context) (*anilist.MangaCollection, error) {
|
||||
mangaCollection, ok := lp.localManager.GetLocalMangaCollection().Get()
|
||||
if !ok {
|
||||
return nil, ErrorNoLocalMangaCollection
|
||||
}
|
||||
|
||||
return mangaCollection, nil
|
||||
}
|
||||
|
||||
// AddMediaToCollection isn't supported for the local platform, always returns an error.
|
||||
func (lp *OfflinePlatform) AddMediaToCollection(ctx context.Context, mIds []int) error {
|
||||
return ErrActionNotSupported
|
||||
}
|
||||
|
||||
// GetStudioDetails isn't supported for the local platform, always returns an empty struct
|
||||
func (lp *OfflinePlatform) GetStudioDetails(ctx context.Context, studioID int) (*anilist.StudioDetails, error) {
|
||||
return &anilist.StudioDetails{}, nil
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnilistClient() anilist.AnilistClient {
|
||||
return lp.client
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetViewerStats(ctx context.Context) (*anilist.ViewerStats, error) {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
|
||||
func (lp *OfflinePlatform) GetAnimeAiringSchedule(ctx context.Context) (*anilist.AnimeAiringSchedule, error) {
|
||||
return nil, ErrActionNotSupported
|
||||
}
|
||||
62
seanime-2.9.10/internal/platforms/platform/platform.go
Normal file
62
seanime-2.9.10/internal/platforms/platform/platform.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/api/anilist"
|
||||
)
|
||||
|
||||
type Platform interface {
|
||||
SetUsername(username string)
|
||||
// SetAnilistClient sets the AniList client to use for the platform
|
||||
SetAnilistClient(client anilist.AnilistClient)
|
||||
// UpdateEntry updates the entry for the given media ID
|
||||
UpdateEntry(context context.Context, mediaID int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error
|
||||
// UpdateEntryProgress updates the entry progress for the given media ID
|
||||
UpdateEntryProgress(context context.Context, mediaID int, progress int, totalEpisodes *int) error
|
||||
// UpdateEntryRepeat updates the entry repeat number for the given media ID
|
||||
UpdateEntryRepeat(context context.Context, mediaID int, repeat int) error
|
||||
// DeleteEntry deletes the entry for the given media ID
|
||||
DeleteEntry(context context.Context, mediaID int) error
|
||||
// GetAnime gets the anime for the given media ID
|
||||
GetAnime(context context.Context, mediaID int) (*anilist.BaseAnime, error)
|
||||
// GetAnimeByMalID gets the anime by MAL ID
|
||||
GetAnimeByMalID(context context.Context, malID int) (*anilist.BaseAnime, error)
|
||||
// GetAnimeWithRelations gets the anime with relations for the given media ID
|
||||
// This is used for scanning purposes in order to build the relation tree
|
||||
GetAnimeWithRelations(context context.Context, mediaID int) (*anilist.CompleteAnime, error)
|
||||
// GetAnimeDetails gets the anime details for the given media ID
|
||||
// These details are only fetched by the anime page
|
||||
GetAnimeDetails(context context.Context, mediaID int) (*anilist.AnimeDetailsById_Media, error)
|
||||
// GetManga gets the manga for the given media ID
|
||||
GetManga(context context.Context, mediaID int) (*anilist.BaseManga, error)
|
||||
// GetAnimeCollection gets the anime collection without custom lists
|
||||
// This should not make any API calls and instead should be based on GetRawAnimeCollection
|
||||
GetAnimeCollection(context context.Context, bypassCache bool) (*anilist.AnimeCollection, error)
|
||||
// GetRawAnimeCollection gets the anime collection with custom lists
|
||||
GetRawAnimeCollection(context context.Context, bypassCache bool) (*anilist.AnimeCollection, error)
|
||||
// GetMangaDetails gets the manga details for the given media ID
|
||||
// These details are only fetched by the manga page
|
||||
GetMangaDetails(context context.Context, mediaID int) (*anilist.MangaDetailsById_Media, error)
|
||||
// GetAnimeCollectionWithRelations gets the anime collection with relations
|
||||
// This is used for scanning purposes in order to build the relation tree
|
||||
GetAnimeCollectionWithRelations(context context.Context) (*anilist.AnimeCollectionWithRelations, error)
|
||||
// GetMangaCollection gets the manga collection without custom lists
|
||||
// This should not make any API calls and instead should be based on GetRawMangaCollection
|
||||
GetMangaCollection(context context.Context, bypassCache bool) (*anilist.MangaCollection, error)
|
||||
// GetRawMangaCollection gets the manga collection with custom lists
|
||||
GetRawMangaCollection(context context.Context, bypassCache bool) (*anilist.MangaCollection, error)
|
||||
// AddMediaToCollection adds the media to the collection
|
||||
AddMediaToCollection(context context.Context, mIds []int) error
|
||||
// GetStudioDetails gets the studio details for the given studio ID
|
||||
GetStudioDetails(context context.Context, studioID int) (*anilist.StudioDetails, error)
|
||||
// GetAnilistClient gets the AniList client
|
||||
GetAnilistClient() anilist.AnilistClient
|
||||
// RefreshAnimeCollection refreshes the anime collection
|
||||
RefreshAnimeCollection(context context.Context) (*anilist.AnimeCollection, error)
|
||||
// RefreshMangaCollection refreshes the manga collection
|
||||
RefreshMangaCollection(context context.Context) (*anilist.MangaCollection, error)
|
||||
// GetViewerStats gets the viewer stats
|
||||
GetViewerStats(context context.Context) (*anilist.ViewerStats, error)
|
||||
// GetAnimeAiringSchedule gets the schedule for airing anime in the collection
|
||||
GetAnimeAiringSchedule(context context.Context) (*anilist.AnimeAiringSchedule, error)
|
||||
}
|
||||
515
seanime-2.9.10/internal/platforms/simulated_platform/helpers.go
Normal file
515
seanime-2.9.10/internal/platforms/simulated_platform/helpers.go
Normal file
@@ -0,0 +1,515 @@
|
||||
package simulated_platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"seanime/internal/api/anilist"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// CollectionWrapper provides an ambivalent interface for anime and manga collections
|
||||
type CollectionWrapper struct {
|
||||
platform *SimulatedPlatform
|
||||
isAnime bool
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeCollectionWrapper() *CollectionWrapper {
|
||||
return &CollectionWrapper{platform: sp, isAnime: true}
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetMangaCollectionWrapper() *CollectionWrapper {
|
||||
return &CollectionWrapper{platform: sp, isAnime: false}
|
||||
}
|
||||
|
||||
// AddEntry adds a new entry to the collection
|
||||
func (cw *CollectionWrapper) AddEntry(mediaId int, status anilist.MediaListStatus) error {
|
||||
if cw.isAnime {
|
||||
return cw.addAnimeEntry(mediaId, status)
|
||||
}
|
||||
return cw.addMangaEntry(mediaId, status)
|
||||
}
|
||||
|
||||
// UpdateEntry updates an existing entry in the collection
|
||||
func (cw *CollectionWrapper) UpdateEntry(mediaId int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
if cw.isAnime {
|
||||
return cw.updateAnimeEntry(mediaId, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
return cw.updateMangaEntry(mediaId, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
|
||||
// UpdateEntryProgress updates the progress of an entry
|
||||
func (cw *CollectionWrapper) UpdateEntryProgress(mediaId int, progress int, totalCount *int) error {
|
||||
status := anilist.MediaListStatusCurrent
|
||||
if totalCount != nil && progress >= *totalCount {
|
||||
status = anilist.MediaListStatusCompleted
|
||||
}
|
||||
|
||||
return cw.UpdateEntry(mediaId, &status, nil, &progress, nil, nil)
|
||||
}
|
||||
|
||||
// DeleteEntry removes an entry from the collection
|
||||
func (cw *CollectionWrapper) DeleteEntry(mediaId int, isEntryId ...bool) error {
|
||||
if cw.isAnime {
|
||||
return cw.deleteAnimeEntry(mediaId, isEntryId...)
|
||||
}
|
||||
return cw.deleteMangaEntry(mediaId, isEntryId...)
|
||||
}
|
||||
|
||||
// FindEntry finds an entry by media ID
|
||||
func (cw *CollectionWrapper) FindEntry(mediaId int, isEntryId ...bool) (interface{}, error) {
|
||||
if cw.isAnime {
|
||||
return cw.findAnimeEntry(mediaId, isEntryId...)
|
||||
}
|
||||
return cw.findMangaEntry(mediaId, isEntryId...)
|
||||
}
|
||||
|
||||
// UpdateMediaData updates the media data for an entry
|
||||
func (cw *CollectionWrapper) UpdateMediaData(mediaId int, mediaData interface{}) error {
|
||||
if cw.isAnime {
|
||||
if baseAnime, ok := mediaData.(*anilist.BaseAnime); ok {
|
||||
return cw.updateAnimeMediaData(mediaId, baseAnime)
|
||||
}
|
||||
return errors.New("invalid anime data type")
|
||||
}
|
||||
|
||||
if baseManga, ok := mediaData.(*anilist.BaseManga); ok {
|
||||
return cw.updateMangaMediaData(mediaId, baseManga)
|
||||
}
|
||||
return errors.New("invalid manga data type")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Anime Collection Helper Methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (cw *CollectionWrapper) addAnimeEntry(mediaId int, status anilist.MediaListStatus) error {
|
||||
collection, err := cw.platform.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if entry already exists
|
||||
if _, err := cw.findAnimeEntry(mediaId); err == nil {
|
||||
return errors.New("entry already exists")
|
||||
}
|
||||
|
||||
// Fetch media data
|
||||
mediaResp, err := cw.platform.client.BaseAnimeByID(context.Background(), &mediaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find or create the appropriate list
|
||||
var targetList *anilist.AnimeCollection_MediaListCollection_Lists
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
if list.GetStatus() != nil && *list.GetStatus() == status {
|
||||
targetList = list
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetList == nil {
|
||||
// Create new list
|
||||
targetList = &anilist.AnimeCollection_MediaListCollection_Lists{
|
||||
Status: &status,
|
||||
Name: lo.ToPtr(string(status)),
|
||||
IsCustomList: lo.ToPtr(false),
|
||||
Entries: []*anilist.AnimeCollection_MediaListCollection_Lists_Entries{},
|
||||
}
|
||||
collection.GetMediaListCollection().Lists = append(collection.GetMediaListCollection().Lists, targetList)
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
newEntry := &anilist.AnimeCollection_MediaListCollection_Lists_Entries{
|
||||
ID: int(time.Now().UnixNano()), // Generate unique ID
|
||||
Status: &status,
|
||||
Progress: lo.ToPtr(0),
|
||||
Media: mediaResp.GetMedia(),
|
||||
Score: lo.ToPtr(0.0),
|
||||
Notes: nil,
|
||||
Repeat: lo.ToPtr(0),
|
||||
Private: lo.ToPtr(false),
|
||||
StartedAt: &anilist.AnimeCollection_MediaListCollection_Lists_Entries_StartedAt{},
|
||||
CompletedAt: &anilist.AnimeCollection_MediaListCollection_Lists_Entries_CompletedAt{},
|
||||
}
|
||||
|
||||
targetList.Entries = append(targetList.Entries, newEntry)
|
||||
|
||||
// Save collection
|
||||
cw.platform.localManager.SaveSimulatedAnimeCollection(collection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) updateAnimeEntry(mediaId int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
collection, err := cw.platform.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var foundEntry *anilist.AnimeCollection_MediaListCollection_Lists_Entries
|
||||
var sourceList *anilist.AnimeCollection_MediaListCollection_Lists
|
||||
var entryIndex int
|
||||
|
||||
// Find the entry
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for i, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
foundEntry = entry
|
||||
sourceList = list
|
||||
entryIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundEntry != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundEntry == nil || sourceList == nil {
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
// Update entry fields
|
||||
if progress != nil {
|
||||
foundEntry.Progress = progress
|
||||
}
|
||||
if scoreRaw != nil {
|
||||
foundEntry.Score = lo.ToPtr(float64(*scoreRaw))
|
||||
}
|
||||
if startedAt != nil {
|
||||
foundEntry.StartedAt = &anilist.AnimeCollection_MediaListCollection_Lists_Entries_StartedAt{
|
||||
Year: startedAt.Year,
|
||||
Month: startedAt.Month,
|
||||
Day: startedAt.Day,
|
||||
}
|
||||
}
|
||||
if completedAt != nil {
|
||||
foundEntry.CompletedAt = &anilist.AnimeCollection_MediaListCollection_Lists_Entries_CompletedAt{
|
||||
Year: completedAt.Year,
|
||||
Month: completedAt.Month,
|
||||
Day: completedAt.Day,
|
||||
}
|
||||
}
|
||||
|
||||
// If status changed, move entry to different list
|
||||
if status != nil && foundEntry.GetStatus() != nil && *status != *foundEntry.GetStatus() {
|
||||
foundEntry.Status = status
|
||||
|
||||
// Remove from current list
|
||||
sourceList.Entries = append(sourceList.Entries[:entryIndex], sourceList.Entries[entryIndex+1:]...)
|
||||
|
||||
// Find or create target list
|
||||
var targetList *anilist.AnimeCollection_MediaListCollection_Lists
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
if list.GetStatus() != nil && *list.GetStatus() == *status {
|
||||
targetList = list
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetList == nil {
|
||||
targetList = &anilist.AnimeCollection_MediaListCollection_Lists{
|
||||
Status: status,
|
||||
Name: lo.ToPtr(string(*status)),
|
||||
IsCustomList: lo.ToPtr(false),
|
||||
Entries: []*anilist.AnimeCollection_MediaListCollection_Lists_Entries{},
|
||||
}
|
||||
collection.GetMediaListCollection().Lists = append(collection.GetMediaListCollection().Lists, targetList)
|
||||
}
|
||||
|
||||
targetList.Entries = append(targetList.Entries, foundEntry)
|
||||
}
|
||||
|
||||
cw.platform.localManager.SaveSimulatedAnimeCollection(collection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) deleteAnimeEntry(mediaId int, isEntryId ...bool) error {
|
||||
collection, err := cw.platform.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find and remove entry
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for i, entry := range list.GetEntries() {
|
||||
if len(isEntryId) > 0 && isEntryId[0] {
|
||||
// If isEntryId is true, we assume mediaId is actually the entry ID
|
||||
if entry.GetID() == mediaId {
|
||||
list.Entries = append(list.Entries[:i], list.Entries[i+1:]...)
|
||||
cw.platform.localManager.SaveSimulatedAnimeCollection(collection)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
list.Entries = append(list.Entries[:i], list.Entries[i+1:]...)
|
||||
cw.platform.localManager.SaveSimulatedAnimeCollection(collection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) findAnimeEntry(mediaId int, isEntryId ...bool) (*anilist.AnimeCollection_MediaListCollection_Lists_Entries, error) {
|
||||
collection, err := cw.platform.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if len(isEntryId) > 0 && isEntryId[0] {
|
||||
if entry.GetID() == mediaId {
|
||||
return entry, nil
|
||||
}
|
||||
} else {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) updateAnimeMediaData(mediaId int, mediaData *anilist.BaseAnime) error {
|
||||
collection, err := cw.platform.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
entry.Media = mediaData
|
||||
cw.platform.localManager.SaveSimulatedAnimeCollection(collection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Manga Collection Helper Methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (cw *CollectionWrapper) addMangaEntry(mediaId int, status anilist.MediaListStatus) error {
|
||||
collection, err := cw.platform.getOrCreateMangaCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check if entry already exists
|
||||
if _, err := cw.findMangaEntry(mediaId); err == nil {
|
||||
return errors.New("entry already exists")
|
||||
}
|
||||
|
||||
// Fetch media data
|
||||
mediaResp, err := cw.platform.client.BaseMangaByID(context.Background(), &mediaId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find or create the appropriate list
|
||||
var targetList *anilist.MangaCollection_MediaListCollection_Lists
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
if list.GetStatus() != nil && *list.GetStatus() == status {
|
||||
targetList = list
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetList == nil {
|
||||
// Create new list
|
||||
targetList = &anilist.MangaCollection_MediaListCollection_Lists{
|
||||
Status: &status,
|
||||
Name: lo.ToPtr(string(status)),
|
||||
IsCustomList: lo.ToPtr(false),
|
||||
Entries: []*anilist.MangaCollection_MediaListCollection_Lists_Entries{},
|
||||
}
|
||||
collection.GetMediaListCollection().Lists = append(collection.GetMediaListCollection().Lists, targetList)
|
||||
}
|
||||
|
||||
// Create new entry
|
||||
newEntry := &anilist.MangaCollection_MediaListCollection_Lists_Entries{
|
||||
ID: int(time.Now().UnixNano()),
|
||||
Status: &status,
|
||||
Progress: lo.ToPtr(0),
|
||||
Media: mediaResp.GetMedia(),
|
||||
Score: lo.ToPtr(0.0),
|
||||
Notes: nil,
|
||||
Repeat: lo.ToPtr(0),
|
||||
Private: lo.ToPtr(false),
|
||||
StartedAt: &anilist.MangaCollection_MediaListCollection_Lists_Entries_StartedAt{},
|
||||
CompletedAt: &anilist.MangaCollection_MediaListCollection_Lists_Entries_CompletedAt{},
|
||||
}
|
||||
|
||||
targetList.Entries = append(targetList.Entries, newEntry)
|
||||
|
||||
// Save collection
|
||||
cw.platform.localManager.SaveSimulatedMangaCollection(collection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) updateMangaEntry(mediaId int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
collection, err := cw.platform.getOrCreateMangaCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var foundEntry *anilist.MangaCollection_MediaListCollection_Lists_Entries
|
||||
var sourceList *anilist.MangaCollection_MediaListCollection_Lists
|
||||
var entryIndex int
|
||||
|
||||
// Find the entry
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for i, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
foundEntry = entry
|
||||
sourceList = list
|
||||
entryIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundEntry != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundEntry == nil || sourceList == nil {
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
// Update entry fields
|
||||
if progress != nil {
|
||||
foundEntry.Progress = progress
|
||||
}
|
||||
if scoreRaw != nil {
|
||||
foundEntry.Score = lo.ToPtr(float64(*scoreRaw))
|
||||
}
|
||||
if startedAt != nil {
|
||||
foundEntry.StartedAt = &anilist.MangaCollection_MediaListCollection_Lists_Entries_StartedAt{
|
||||
Year: startedAt.Year,
|
||||
Month: startedAt.Month,
|
||||
Day: startedAt.Day,
|
||||
}
|
||||
}
|
||||
if completedAt != nil {
|
||||
foundEntry.CompletedAt = &anilist.MangaCollection_MediaListCollection_Lists_Entries_CompletedAt{
|
||||
Year: completedAt.Year,
|
||||
Month: completedAt.Month,
|
||||
Day: completedAt.Day,
|
||||
}
|
||||
}
|
||||
|
||||
// If status changed, move entry to different list
|
||||
if status != nil && foundEntry.GetStatus() != nil && *status != *foundEntry.GetStatus() {
|
||||
foundEntry.Status = status
|
||||
|
||||
// Remove from current list
|
||||
sourceList.Entries = append(sourceList.Entries[:entryIndex], sourceList.Entries[entryIndex+1:]...)
|
||||
|
||||
// Find or create target list
|
||||
var targetList *anilist.MangaCollection_MediaListCollection_Lists
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
if list.GetStatus() != nil && *list.GetStatus() == *status {
|
||||
targetList = list
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetList == nil {
|
||||
targetList = &anilist.MangaCollection_MediaListCollection_Lists{
|
||||
Status: status,
|
||||
Name: lo.ToPtr(string(*status)),
|
||||
IsCustomList: lo.ToPtr(false),
|
||||
Entries: []*anilist.MangaCollection_MediaListCollection_Lists_Entries{},
|
||||
}
|
||||
collection.GetMediaListCollection().Lists = append(collection.GetMediaListCollection().Lists, targetList)
|
||||
}
|
||||
|
||||
targetList.Entries = append(targetList.Entries, foundEntry)
|
||||
}
|
||||
|
||||
cw.platform.localManager.SaveSimulatedMangaCollection(collection)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) deleteMangaEntry(mediaId int, isEntryId ...bool) error {
|
||||
collection, err := cw.platform.getOrCreateMangaCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find and remove entry
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for i, entry := range list.GetEntries() {
|
||||
if len(isEntryId) > 0 && isEntryId[0] {
|
||||
if entry.GetID() == mediaId {
|
||||
list.Entries = append(list.Entries[:i], list.Entries[i+1:]...)
|
||||
cw.platform.localManager.SaveSimulatedMangaCollection(collection)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
list.Entries = append(list.Entries[:i], list.Entries[i+1:]...)
|
||||
cw.platform.localManager.SaveSimulatedMangaCollection(collection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) findMangaEntry(mediaId int, isEntryId ...bool) (*anilist.MangaCollection_MediaListCollection_Lists_Entries, error) {
|
||||
collection, err := cw.platform.getOrCreateMangaCollection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if len(isEntryId) > 0 && isEntryId[0] {
|
||||
if entry.GetID() == mediaId {
|
||||
return entry, nil
|
||||
}
|
||||
} else {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
return entry, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (cw *CollectionWrapper) updateMangaMediaData(mediaId int, mediaData *anilist.BaseManga) error {
|
||||
collection, err := cw.platform.getOrCreateMangaCollection()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, list := range collection.GetMediaListCollection().GetLists() {
|
||||
for _, entry := range list.GetEntries() {
|
||||
if entry.GetMedia().GetID() == mediaId {
|
||||
entry.Media = mediaData
|
||||
cw.platform.localManager.SaveSimulatedMangaCollection(collection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
package simulated_platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/local"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/util/limiter"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMediaNotFound means the media wasn't found in the local collection
|
||||
ErrMediaNotFound = errors.New("media not found")
|
||||
)
|
||||
|
||||
// SimulatedPlatform used when the user is not authenticated to AniList.
|
||||
// It acts as a dummy account using simulated collections stored locally.
|
||||
type SimulatedPlatform struct {
|
||||
logger *zerolog.Logger
|
||||
localManager local.Manager
|
||||
client anilist.AnilistClient // should only receive an unauthenticated client
|
||||
|
||||
// Cache for collections
|
||||
animeCollection *anilist.AnimeCollection
|
||||
mangaCollection *anilist.MangaCollection
|
||||
mu sync.RWMutex
|
||||
collectionMu sync.RWMutex // used to protect access to collections
|
||||
lastAnimeCollectionRefetchTime time.Time // used to prevent refetching too many times
|
||||
lastMangaCollectionRefetchTime time.Time // used to prevent refetching too many times
|
||||
anilistRateLimit *limiter.Limiter
|
||||
}
|
||||
|
||||
func NewSimulatedPlatform(localManager local.Manager, client anilist.AnilistClient, logger *zerolog.Logger) (platform.Platform, error) {
|
||||
sp := &SimulatedPlatform{
|
||||
logger: logger,
|
||||
localManager: localManager,
|
||||
client: client,
|
||||
anilistRateLimit: limiter.NewAnilistLimiter(),
|
||||
}
|
||||
|
||||
return sp, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implementation
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (sp *SimulatedPlatform) SetUsername(username string) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) SetAnilistClient(client anilist.AnilistClient) {
|
||||
sp.client = client // DEVNOTE: Should only be unauthenticated
|
||||
}
|
||||
|
||||
// UpdateEntry updates the entry for the given media ID.
|
||||
// If the entry doesn't exist, it will be added automatically after determining the media type.
|
||||
func (sp *SimulatedPlatform) UpdateEntry(ctx context.Context, mediaID int, status *anilist.MediaListStatus, scoreRaw *int, progress *int, startedAt *anilist.FuzzyDateInput, completedAt *anilist.FuzzyDateInput) error {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Updating entry")
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
|
||||
// Try anime first
|
||||
animeWrapper := sp.GetAnimeCollectionWrapper()
|
||||
if _, err := animeWrapper.FindEntry(mediaID); err == nil {
|
||||
return animeWrapper.UpdateEntry(mediaID, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
|
||||
// Try manga
|
||||
mangaWrapper := sp.GetMangaCollectionWrapper()
|
||||
if _, err := mangaWrapper.FindEntry(mediaID); err == nil {
|
||||
return mangaWrapper.UpdateEntry(mediaID, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
|
||||
// Entry doesn't exist, determine media type and add it
|
||||
defaultStatus := anilist.MediaListStatusPlanning
|
||||
if status != nil {
|
||||
defaultStatus = *status
|
||||
}
|
||||
|
||||
// Try to fetch as anime first
|
||||
if _, err := sp.client.BaseAnimeByID(ctx, &mediaID); err == nil {
|
||||
// It's an anime, add it to anime collection
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Adding new anime entry")
|
||||
if err := animeWrapper.AddEntry(mediaID, defaultStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update with provided values if there are additional updates needed
|
||||
if status != &defaultStatus || scoreRaw != nil || progress != nil || startedAt != nil || completedAt != nil {
|
||||
return animeWrapper.UpdateEntry(mediaID, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to fetch as manga
|
||||
if _, err := sp.client.BaseMangaByID(ctx, &mediaID); err == nil {
|
||||
// It's a manga, add it to manga collection
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Adding new manga entry")
|
||||
if err := mangaWrapper.AddEntry(mediaID, defaultStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
// Update with provided values if there are additional updates needed
|
||||
if status != &defaultStatus || scoreRaw != nil || progress != nil || startedAt != nil || completedAt != nil {
|
||||
return mangaWrapper.UpdateEntry(mediaID, status, scoreRaw, progress, startedAt, completedAt)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Media not found in either anime or manga
|
||||
return errors.New("media not found on AniList")
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) UpdateEntryProgress(ctx context.Context, mediaID int, progress int, totalEpisodes *int) error {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Int("progress", progress).Msg("simulated platform: Updating entry progress")
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
|
||||
status := anilist.MediaListStatusCurrent
|
||||
if totalEpisodes != nil && progress >= *totalEpisodes {
|
||||
status = anilist.MediaListStatusCompleted
|
||||
}
|
||||
|
||||
// Try anime first
|
||||
animeWrapper := sp.GetAnimeCollectionWrapper()
|
||||
if _, err := animeWrapper.FindEntry(mediaID); err == nil {
|
||||
return animeWrapper.UpdateEntryProgress(mediaID, progress, totalEpisodes)
|
||||
}
|
||||
|
||||
// Try manga
|
||||
mangaWrapper := sp.GetMangaCollectionWrapper()
|
||||
if _, err := mangaWrapper.FindEntry(mediaID); err == nil {
|
||||
return mangaWrapper.UpdateEntryProgress(mediaID, progress, totalEpisodes)
|
||||
}
|
||||
|
||||
// Entry doesn't exist, determine media type and add it
|
||||
// Try to fetch as anime first
|
||||
if _, err := sp.client.BaseAnimeByID(ctx, &mediaID); err == nil {
|
||||
// It's an anime, add it to anime collection
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Adding new anime entry for progress update")
|
||||
if err := animeWrapper.AddEntry(mediaID, status); err != nil {
|
||||
return err
|
||||
}
|
||||
return animeWrapper.UpdateEntryProgress(mediaID, progress, totalEpisodes)
|
||||
}
|
||||
|
||||
// Try to fetch as manga
|
||||
if _, err := sp.client.BaseMangaByID(ctx, &mediaID); err == nil {
|
||||
// It's a manga, add it to manga collection
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Adding new manga entry for progress update")
|
||||
if err := mangaWrapper.AddEntry(mediaID, status); err != nil {
|
||||
return err
|
||||
}
|
||||
return mangaWrapper.UpdateEntryProgress(mediaID, progress, totalEpisodes)
|
||||
}
|
||||
|
||||
// Media not found in either anime or manga
|
||||
return errors.New("media not found on AniList")
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) UpdateEntryRepeat(ctx context.Context, mediaID int, repeat int) error {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Int("repeat", repeat).Msg("simulated platform: Updating entry repeat")
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
|
||||
// Try anime first
|
||||
wrapper := sp.GetAnimeCollectionWrapper()
|
||||
if entry, err := wrapper.FindEntry(mediaID); err == nil {
|
||||
if animeEntry, ok := entry.(*anilist.AnimeCollection_MediaListCollection_Lists_Entries); ok {
|
||||
animeEntry.Repeat = &repeat
|
||||
sp.localManager.SaveSimulatedAnimeCollection(sp.animeCollection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Try manga
|
||||
wrapper = sp.GetMangaCollectionWrapper()
|
||||
if entry, err := wrapper.FindEntry(mediaID); err == nil {
|
||||
if mangaEntry, ok := entry.(*anilist.MangaCollection_MediaListCollection_Lists_Entries); ok {
|
||||
mangaEntry.Repeat = &repeat
|
||||
sp.localManager.SaveSimulatedMangaCollection(sp.mangaCollection)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) DeleteEntry(ctx context.Context, entryId int) error {
|
||||
sp.logger.Trace().Int("entryId", entryId).Msg("simulated platform: Deleting entry")
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
|
||||
// Try anime first
|
||||
wrapper := sp.GetAnimeCollectionWrapper()
|
||||
if _, err := wrapper.FindEntry(entryId, true); err == nil {
|
||||
return wrapper.DeleteEntry(entryId, true)
|
||||
}
|
||||
|
||||
// Try manga
|
||||
wrapper = sp.GetMangaCollectionWrapper()
|
||||
if _, err := wrapper.FindEntry(entryId, true); err == nil {
|
||||
return wrapper.DeleteEntry(entryId, true)
|
||||
}
|
||||
|
||||
return ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnime(ctx context.Context, mediaID int) (*anilist.BaseAnime, error) {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Getting anime")
|
||||
|
||||
// Get anime from anilist
|
||||
resp, err := sp.client.BaseAnimeByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update media data in collection if it exists
|
||||
sp.mu.Lock()
|
||||
wrapper := sp.GetAnimeCollectionWrapper()
|
||||
if _, err := wrapper.FindEntry(mediaID); err == nil {
|
||||
_ = wrapper.UpdateMediaData(mediaID, resp.GetMedia())
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeByMalID(ctx context.Context, malID int) (*anilist.BaseAnime, error) {
|
||||
sp.logger.Trace().Int("malID", malID).Msg("simulated platform: Getting anime by MAL ID")
|
||||
|
||||
resp, err := sp.client.BaseAnimeByMalID(ctx, &malID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update media data in collection if it exists
|
||||
if resp.GetMedia() != nil {
|
||||
sp.mu.Lock()
|
||||
wrapper := sp.GetAnimeCollectionWrapper()
|
||||
if _, err := wrapper.FindEntry(resp.GetMedia().GetID()); err == nil {
|
||||
_ = wrapper.UpdateMediaData(resp.GetMedia().GetID(), resp.GetMedia())
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
}
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeDetails(ctx context.Context, mediaID int) (*anilist.AnimeDetailsById_Media, error) {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Getting anime details")
|
||||
|
||||
resp, err := sp.client.AnimeDetailsByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeWithRelations(ctx context.Context, mediaID int) (*anilist.CompleteAnime, error) {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Getting anime with relations")
|
||||
|
||||
resp, err := sp.client.CompleteAnimeByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetManga(ctx context.Context, mediaID int) (*anilist.BaseManga, error) {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Getting manga")
|
||||
|
||||
// Get manga from anilist
|
||||
resp, err := sp.client.BaseMangaByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Update media data in collection if it exists
|
||||
sp.mu.Lock()
|
||||
wrapper := sp.GetMangaCollectionWrapper()
|
||||
if _, err := wrapper.FindEntry(mediaID); err == nil {
|
||||
_ = wrapper.UpdateMediaData(mediaID, resp.GetMedia())
|
||||
}
|
||||
sp.mu.Unlock()
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetMangaDetails(ctx context.Context, mediaID int) (*anilist.MangaDetailsById_Media, error) {
|
||||
sp.logger.Trace().Int("mediaID", mediaID).Msg("simulated platform: Getting manga details")
|
||||
|
||||
resp, err := sp.client.MangaDetailsByID(ctx, &mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.GetMedia(), nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
sp.logger.Trace().Bool("bypassCache", bypassCache).Msg("simulated platform: Getting anime collection")
|
||||
|
||||
if bypassCache {
|
||||
sp.invalidateAnimeCollectionCache()
|
||||
return sp.getOrCreateAnimeCollection()
|
||||
}
|
||||
|
||||
return sp.animeCollection, nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetRawAnimeCollection(ctx context.Context, bypassCache bool) (*anilist.AnimeCollection, error) {
|
||||
return sp.GetAnimeCollection(ctx, bypassCache)
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) RefreshAnimeCollection(ctx context.Context) (*anilist.AnimeCollection, error) {
|
||||
sp.logger.Trace().Msg("simulated platform: Refreshing anime collection")
|
||||
|
||||
sp.invalidateAnimeCollectionCache()
|
||||
return sp.getOrCreateAnimeCollection()
|
||||
}
|
||||
|
||||
// GetAnimeCollectionWithRelations returns the anime collection (without relations)
|
||||
func (sp *SimulatedPlatform) GetAnimeCollectionWithRelations(ctx context.Context) (*anilist.AnimeCollectionWithRelations, error) {
|
||||
sp.logger.Trace().Msg("simulated platform: Getting anime collection with relations")
|
||||
|
||||
// Use JSON to convert the collection structs
|
||||
collection, err := sp.getOrCreateAnimeCollection()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
collectionWithRelations := &anilist.AnimeCollectionWithRelations{}
|
||||
|
||||
marshaled, err := json.Marshal(collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = json.Unmarshal(marshaled, collectionWithRelations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For simulated platform, the anime collection will not have relations
|
||||
return collectionWithRelations, nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
sp.logger.Trace().Bool("bypassCache", bypassCache).Msg("simulated platform: Getting manga collection")
|
||||
|
||||
if bypassCache {
|
||||
sp.invalidateMangaCollectionCache()
|
||||
return sp.getOrCreateMangaCollection()
|
||||
}
|
||||
|
||||
return sp.mangaCollection, nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetRawMangaCollection(ctx context.Context, bypassCache bool) (*anilist.MangaCollection, error) {
|
||||
return sp.GetMangaCollection(ctx, bypassCache)
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) RefreshMangaCollection(ctx context.Context) (*anilist.MangaCollection, error) {
|
||||
sp.logger.Trace().Msg("simulated platform: Refreshing manga collection")
|
||||
|
||||
sp.invalidateMangaCollectionCache()
|
||||
return sp.getOrCreateMangaCollection()
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) AddMediaToCollection(ctx context.Context, mIds []int) error {
|
||||
sp.logger.Trace().Interface("mediaIDs", mIds).Msg("simulated platform: Adding media to collection")
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
|
||||
// DEVNOTE: We assume it's anime for now since it's only been used for anime
|
||||
wrapper := sp.GetAnimeCollectionWrapper()
|
||||
for _, mediaID := range mIds {
|
||||
// Try to add as anime first, if it fails, ignore
|
||||
_ = wrapper.AddEntry(mediaID, anilist.MediaListStatusPlanning)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetStudioDetails(ctx context.Context, studioID int) (*anilist.StudioDetails, error) {
|
||||
return sp.client.StudioDetails(ctx, &studioID)
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnilistClient() anilist.AnilistClient {
|
||||
return sp.client
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetViewerStats(ctx context.Context) (*anilist.ViewerStats, error) {
|
||||
return nil, errors.New("use a real account to get stats")
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) GetAnimeAiringSchedule(ctx context.Context) (*anilist.AnimeAiringSchedule, error) {
|
||||
collection, err := sp.GetAnimeCollection(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mediaIds := make([]*int, 0)
|
||||
for _, list := range collection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
mediaIds = append(mediaIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
}
|
||||
}
|
||||
|
||||
var ret *anilist.AnimeAiringSchedule
|
||||
|
||||
now := time.Now()
|
||||
currentSeason, currentSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindCurrent)
|
||||
previousSeason, previousSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindPrevious)
|
||||
nextSeason, nextSeasonYear := anilist.GetSeasonInfo(now, anilist.GetSeasonKindNext)
|
||||
|
||||
ret, err = sp.client.AnimeAiringSchedule(ctx, mediaIds, ¤tSeason, ¤tSeasonYear, &previousSeason, &previousSeasonYear, &nextSeason, &nextSeasonYear)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type animeScheduleMedia interface {
|
||||
GetMedia() []*anilist.AnimeSchedule
|
||||
}
|
||||
|
||||
foundIds := make(map[int]struct{})
|
||||
addIds := func(n animeScheduleMedia) {
|
||||
for _, m := range n.GetMedia() {
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
foundIds[m.GetID()] = struct{}{}
|
||||
}
|
||||
}
|
||||
addIds(ret.GetOngoing())
|
||||
addIds(ret.GetOngoingNext())
|
||||
addIds(ret.GetPreceding())
|
||||
addIds(ret.GetUpcoming())
|
||||
addIds(ret.GetUpcomingNext())
|
||||
|
||||
missingIds := make([]*int, 0)
|
||||
for _, list := range collection.MediaListCollection.Lists {
|
||||
for _, entry := range list.Entries {
|
||||
if _, found := foundIds[entry.GetMedia().GetID()]; found {
|
||||
continue
|
||||
}
|
||||
endDate := entry.GetMedia().GetEndDate()
|
||||
// Ignore if ended more than 2 months ago
|
||||
if endDate == nil || endDate.GetYear() == nil || endDate.GetMonth() == nil {
|
||||
missingIds = append(missingIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
continue
|
||||
}
|
||||
endTime := time.Date(*endDate.GetYear(), time.Month(*endDate.GetMonth()), 1, 0, 0, 0, 0, time.UTC)
|
||||
if endTime.Before(now.AddDate(0, -2, 0)) {
|
||||
continue
|
||||
}
|
||||
missingIds = append(missingIds, &[]int{entry.GetMedia().GetID()}[0])
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingIds) > 0 {
|
||||
retB, err := sp.client.AnimeAiringScheduleRaw(ctx, missingIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(retB.GetPage().GetMedia()) > 0 {
|
||||
// Add to ongoing next
|
||||
for _, m := range retB.Page.GetMedia() {
|
||||
if ret.OngoingNext == nil {
|
||||
ret.OngoingNext = &anilist.AnimeAiringSchedule_OngoingNext{
|
||||
Media: make([]*anilist.AnimeSchedule, 0),
|
||||
}
|
||||
}
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ret.OngoingNext.Media = append(ret.OngoingNext.Media, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Helper Methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (sp *SimulatedPlatform) getOrCreateAnimeCollection() (*anilist.AnimeCollection, error) {
|
||||
sp.collectionMu.RLock()
|
||||
if sp.animeCollection != nil {
|
||||
defer sp.collectionMu.RUnlock()
|
||||
return sp.animeCollection, nil
|
||||
}
|
||||
sp.collectionMu.RUnlock()
|
||||
|
||||
sp.collectionMu.Lock()
|
||||
defer sp.collectionMu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock
|
||||
if sp.animeCollection != nil {
|
||||
return sp.animeCollection, nil
|
||||
}
|
||||
|
||||
// Try to load from database
|
||||
if collection := sp.localManager.GetSimulatedAnimeCollection(); collection.IsPresent() {
|
||||
sp.animeCollection = collection.MustGet()
|
||||
return sp.animeCollection, nil
|
||||
}
|
||||
|
||||
// Create empty collection
|
||||
sp.animeCollection = &anilist.AnimeCollection{
|
||||
MediaListCollection: &anilist.AnimeCollection_MediaListCollection{
|
||||
Lists: []*anilist.AnimeCollection_MediaListCollection_Lists{},
|
||||
},
|
||||
}
|
||||
|
||||
// Save empty collection
|
||||
sp.localManager.SaveSimulatedAnimeCollection(sp.animeCollection)
|
||||
|
||||
return sp.animeCollection, nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) getOrCreateMangaCollection() (*anilist.MangaCollection, error) {
|
||||
sp.collectionMu.RLock()
|
||||
if sp.mangaCollection != nil {
|
||||
defer sp.collectionMu.RUnlock()
|
||||
return sp.mangaCollection, nil
|
||||
}
|
||||
sp.collectionMu.RUnlock()
|
||||
|
||||
sp.collectionMu.Lock()
|
||||
defer sp.collectionMu.Unlock()
|
||||
|
||||
// Double-check after acquiring write lock
|
||||
if sp.mangaCollection != nil {
|
||||
return sp.mangaCollection, nil
|
||||
}
|
||||
|
||||
// Try to load from database
|
||||
if collection := sp.localManager.GetSimulatedMangaCollection(); collection.IsPresent() {
|
||||
sp.mangaCollection = collection.MustGet()
|
||||
return sp.mangaCollection, nil
|
||||
}
|
||||
|
||||
// Create empty collection
|
||||
sp.mangaCollection = &anilist.MangaCollection{
|
||||
MediaListCollection: &anilist.MangaCollection_MediaListCollection{
|
||||
Lists: []*anilist.MangaCollection_MediaListCollection_Lists{},
|
||||
},
|
||||
}
|
||||
|
||||
// Save empty collection
|
||||
sp.localManager.SaveSimulatedMangaCollection(sp.mangaCollection)
|
||||
|
||||
return sp.mangaCollection, nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) invalidateAnimeCollectionCache() {
|
||||
sp.collectionMu.Lock()
|
||||
defer sp.collectionMu.Unlock()
|
||||
sp.animeCollection = nil
|
||||
}
|
||||
|
||||
func (sp *SimulatedPlatform) invalidateMangaCollectionCache() {
|
||||
sp.collectionMu.Lock()
|
||||
defer sp.collectionMu.Unlock()
|
||||
sp.mangaCollection = nil
|
||||
}
|
||||
Reference in New Issue
Block a user