Files
seanime-docker/seanime-2.9.10/internal/local/manager.go
2025-09-20 14:08:38 +01:00

1040 lines
32 KiB
Go

package local
import (
"context"
"fmt"
"os"
"path/filepath"
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/database/db"
"seanime/internal/database/db_bridge"
"seanime/internal/events"
"seanime/internal/library/anime"
"seanime/internal/manga"
"seanime/internal/platforms/platform"
"github.com/rs/zerolog"
"github.com/samber/lo"
"github.com/samber/mo"
)
var (
ErrAlreadyTracked = fmt.Errorf("local manager: Media already tracked")
)
const (
AnimeType = "anime"
MangaType = "manga"
)
type Manager interface {
// SetAnimeCollection updates the online anime collection in the manager.
SetAnimeCollection(ac *anilist.AnimeCollection)
// SetMangaCollection updates the online manga collection in the manager.
SetMangaCollection(mc *anilist.MangaCollection)
// GetLocalAnimeCollection returns the local anime collection stored in the local database.
GetLocalAnimeCollection() mo.Option[*anilist.AnimeCollection]
// GetLocalMangaCollection returns the local manga collection stored in the local database.
GetLocalMangaCollection() mo.Option[*anilist.MangaCollection]
// UpdateLocalAnimeCollection updates the local anime collection using the online data.
UpdateLocalAnimeCollection(ac *anilist.AnimeCollection)
// UpdateLocalMangaCollection updates the local manga collection using the online data.
UpdateLocalMangaCollection(mc *anilist.MangaCollection)
// GetOfflineMetadataProvider returns the offline metadata provider.
GetOfflineMetadataProvider() metadata.Provider
// GetSyncer returns the syncer (used to synchronize the anime and manga snapshots in the local database).
GetSyncer() *Syncer
AutoTrackCurrentMedia() (bool, error)
// TrackAnime adds an anime to track for offline use.
// It checks that the anime is currently in the user's anime collection.
TrackAnime(mId int) error
// UntrackAnime removes the anime from tracking.
UntrackAnime(mId int) error
// TrackManga adds a manga to track for offline use.
// It checks that the manga is currently in the user's manga collection.
TrackManga(mId int) error
// UntrackManga removes a manga from tracking.
UntrackManga(mId int) error
// IsMediaTracked checks if the media is tracked in the local database.
IsMediaTracked(aId int, kind string) bool
// GetTrackedMediaItems returns all tracked media items.
GetTrackedMediaItems() []*TrackedMediaItem
// SynchronizeLocal syncs all currently tracked media.
// Compares the local database with the user's anime and manga collections and updates the local database accordingly.
SynchronizeLocal() error
// SynchronizeAnilist syncs the user's AniList data with data stored in the local database.
SynchronizeAnilist() error
// SetRefreshAnilistCollectionsFunc sets the function to call to refresh the online AniList collections.
SetRefreshAnilistCollectionsFunc(func())
// HasLocalChanges checks if there are any local changes that need to be uploaded or ignored.
HasLocalChanges() bool
// SetHasLocalChanges sets the flag to determine if there are local changes that need to be uploaded or ignored.
SetHasLocalChanges(bool)
// GetLocalStorageSize returns the size of the local storage in bytes.
GetLocalStorageSize() int64
// GetSimulatedAnimeCollection returns the simulated anime collection for unauthenticated users.
GetSimulatedAnimeCollection() mo.Option[*anilist.AnimeCollection]
// GetSimulatedMangaCollection returns the simulated manga collection for unauthenticated users.
GetSimulatedMangaCollection() mo.Option[*anilist.MangaCollection]
// SaveSimulatedAnimeCollection sets the simulated anime collection for unauthenticated users.
SaveSimulatedAnimeCollection(ac *anilist.AnimeCollection)
// SaveSimulatedMangaCollection sets the simulated manga collection for unauthenticated users.
SaveSimulatedMangaCollection(mc *anilist.MangaCollection)
// SynchronizeSimulatedCollectionToAnilist synchronizes the simulated anime and manga collections to the user's AniList account.
SynchronizeSimulatedCollectionToAnilist() error
// SynchronizeAnilistToSimulatedCollection synchronizes the user's AniList account to the simulated anime and manga collections.
SynchronizeAnilistToSimulatedCollection() error
SetOffline(bool)
}
type (
ManagerImpl struct {
db *db.Database
localDb *Database
localDir string
localAssetsDir string
isOffline bool
logger *zerolog.Logger
metadataProvider metadata.Provider
mangaRepository *manga.Repository
wsEventManager events.WSEventManagerInterface
offlineMetadataProvider metadata.Provider
anilistPlatform platform.Platform
syncer *Syncer
// Anime collection stored in the local database, without modifications
localAnimeCollection mo.Option[*anilist.AnimeCollection]
// Manga collection stored in the local database, without modifications
localMangaCollection mo.Option[*anilist.MangaCollection]
// Anime collection from the user's AniList account, changed by ManagerImpl.SetAnimeCollection
animeCollection mo.Option[*anilist.AnimeCollection]
// Manga collection from the user's AniList account, changed by ManagerImpl.SetMangaCollection
mangaCollection mo.Option[*anilist.MangaCollection]
// Downloaded chapter containers, set by ManagerImpl.Synchronize, accessed by the synchronization Syncer
downloadedChapterContainers []*manga.ChapterContainer
// Local files, set by ManagerImpl.Synchronize, accessed by the synchronization Syncer
localFiles []*anime.LocalFile
RefreshAnilistCollectionsFunc func()
}
TrackedMediaItem struct {
MediaId int `json:"mediaId"`
Type string `json:"type"`
AnimeEntry *anilist.AnimeListEntry `json:"animeEntry,omitempty"`
MangaEntry *anilist.MangaListEntry `json:"mangaEntry,omitempty"`
}
NewManagerOptions struct {
LocalDir string
AssetDir string
Logger *zerolog.Logger
MetadataProvider metadata.Provider
MangaRepository *manga.Repository
Database *db.Database
WSEventManager events.WSEventManagerInterface
AnilistPlatform platform.Platform
IsOffline bool
}
)
func NewManager(opts *NewManagerOptions) (Manager, error) {
_ = os.MkdirAll(opts.LocalDir, os.ModePerm)
localDb, err := newLocalSyncDatabase(opts.LocalDir, "local", opts.Logger)
if err != nil {
return nil, err
}
ret := &ManagerImpl{
db: opts.Database,
localDb: localDb,
localDir: opts.LocalDir,
localAssetsDir: opts.AssetDir,
logger: opts.Logger,
animeCollection: mo.None[*anilist.AnimeCollection](),
mangaCollection: mo.None[*anilist.MangaCollection](),
localAnimeCollection: mo.None[*anilist.AnimeCollection](),
localMangaCollection: mo.None[*anilist.MangaCollection](),
metadataProvider: opts.MetadataProvider,
mangaRepository: opts.MangaRepository,
downloadedChapterContainers: make([]*manga.ChapterContainer, 0),
localFiles: make([]*anime.LocalFile, 0),
wsEventManager: opts.WSEventManager,
isOffline: opts.IsOffline,
anilistPlatform: opts.AnilistPlatform,
RefreshAnilistCollectionsFunc: func() {},
}
ret.syncer = NewQueue(ret)
ret.offlineMetadataProvider = NewOfflineMetadataProvider(ret)
// Load the local collections
ret.loadLocalAnimeCollection()
ret.loadLocalMangaCollection()
_ = ret.localDb.GetSettings()
return ret, nil
}
func (m *ManagerImpl) SetRefreshAnilistCollectionsFunc(f func()) {
m.RefreshAnilistCollectionsFunc = f
}
func (m *ManagerImpl) GetSyncer() *Syncer {
return m.syncer
}
func (m *ManagerImpl) GetOfflineMetadataProvider() metadata.Provider {
return m.offlineMetadataProvider
}
func (m *ManagerImpl) SetOffline(enabled bool) {
m.isOffline = enabled
}
func (m *ManagerImpl) HasLocalChanges() bool {
s := m.localDb.GetSettings()
return s.Updated
}
func (m *ManagerImpl) SetHasLocalChanges(b bool) {
s := m.localDb.GetSettings()
if s.Updated == b {
return
}
s.Updated = b
_ = m.localDb.SaveSettings(s)
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (m *ManagerImpl) loadLocalAnimeCollection() {
collection, ok := m.localDb.GetLocalAnimeCollection()
if !ok {
m.localAnimeCollection = mo.None[*anilist.AnimeCollection]()
}
m.localAnimeCollection = mo.Some(collection)
}
func (m *ManagerImpl) loadLocalMangaCollection() {
collection, ok := m.localDb.GetLocalMangaCollection()
if !ok {
m.localMangaCollection = mo.None[*anilist.MangaCollection]()
}
m.localMangaCollection = mo.Some(collection)
}
func (m *ManagerImpl) SetAnimeCollection(ac *anilist.AnimeCollection) {
if ac == nil {
m.animeCollection = mo.None[*anilist.AnimeCollection]()
} else {
m.animeCollection = mo.Some[*anilist.AnimeCollection](ac)
}
}
func (m *ManagerImpl) SetMangaCollection(mc *anilist.MangaCollection) {
if mc == nil {
m.mangaCollection = mo.None[*anilist.MangaCollection]()
} else {
m.mangaCollection = mo.Some[*anilist.MangaCollection](mc)
}
}
func (m *ManagerImpl) GetLocalAnimeCollection() mo.Option[*anilist.AnimeCollection] {
return m.localAnimeCollection
}
func (m *ManagerImpl) GetLocalMangaCollection() mo.Option[*anilist.MangaCollection] {
return m.localMangaCollection
}
func (m *ManagerImpl) UpdateLocalAnimeCollection(ac *anilist.AnimeCollection) {
_ = m.localDb.SaveAnimeCollection(ac)
m.loadLocalAnimeCollection()
}
func (m *ManagerImpl) UpdateLocalMangaCollection(mc *anilist.MangaCollection) {
_ = m.localDb.SaveMangaCollection(mc)
m.loadLocalMangaCollection()
}
func (m *ManagerImpl) AutoTrackCurrentMedia() (added bool, err error) {
m.logger.Trace().Msgf("local manager: Saving all current media for offline use")
trackedMedia := m.GetTrackedMediaItems()
trackedMediaMap := make(map[int]struct{})
for _, item := range trackedMedia {
trackedMediaMap[item.MediaId] = struct{}{}
}
groupedLocalFiles := lo.GroupBy(m.localFiles, func(f *anime.LocalFile) int {
return f.MediaId
})
animeCollection, ok := m.animeCollection.Get()
if ok {
for _, list := range animeCollection.MediaListCollection.Lists {
for _, entry := range list.GetEntries() {
if entry.Status == nil || *entry.GetStatus() != anilist.MediaListStatusCurrent {
continue
}
if _, found := trackedMediaMap[entry.Media.GetID()]; found {
continue
}
m.logger.Trace().Msgf("local manager: Adding anime %d to local database", entry.Media.GetID())
lfs, ok := groupedLocalFiles[entry.Media.GetID()]
if !ok || len(lfs) == 0 {
continue
}
err := m.TrackAnime(entry.Media.GetID())
if err != nil {
continue
}
added = true
}
}
}
groupedDownloadedChapterContainers := lo.GroupBy(m.downloadedChapterContainers, func(c *manga.ChapterContainer) int {
return c.MediaId
})
mangaCollection, ok := m.mangaCollection.Get()
if ok {
for _, list := range mangaCollection.MediaListCollection.Lists {
for _, entry := range list.GetEntries() {
if entry.Status == nil || *entry.GetStatus() != anilist.MediaListStatusCurrent {
continue
}
if _, found := trackedMediaMap[entry.Media.GetID()]; found {
continue
}
m.logger.Trace().Msgf("local manager: Adding manga %d to local database", entry.Media.GetID())
ccs, ok := groupedDownloadedChapterContainers[entry.Media.GetID()]
if !ok || len(ccs) == 0 {
continue
}
err := m.TrackManga(entry.Media.GetID())
if err != nil {
continue
}
added = true
}
}
}
return
}
// TrackAnime adds an anime to track.
// It checks that the anime is currently in the user's anime collection.
// The anime should have local files, or else ManagerImpl.Synchronize will remove it from tracking.
func (m *ManagerImpl) TrackAnime(mId int) error {
m.logger.Trace().Msgf("local manager: Adding anime %d to local database", mId)
s := &TrackedMedia{
MediaId: mId,
Type: AnimeType,
}
// Check if the anime is in the user's anime collection
if m.animeCollection.IsAbsent() {
m.logger.Error().Msg("local manager: Anime collection not set")
return fmt.Errorf("anime collection not set")
}
if _, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(mId); !found {
m.logger.Error().Msgf("local manager: Anime %d not found in user's anime collection", mId)
return fmt.Errorf("anime is not in AniList collection")
}
if _, found := m.localDb.GetTrackedMedia(mId, AnimeType); found {
return ErrAlreadyTracked
}
err := m.localDb.gormdb.Create(s).Error
if err != nil {
m.logger.Error().Msgf("local manager: Failed to add anime %d to local database: %w", mId, err)
return fmt.Errorf("failed to add anime %d to local database: %w", mId, err)
}
return nil
}
func (m *ManagerImpl) UntrackAnime(mId int) error {
m.logger.Trace().Msgf("local manager: Removing anime %d from local database", mId)
if _, found := m.localDb.GetTrackedMedia(mId, AnimeType); !found {
m.logger.Error().Msgf("local manager: Anime %d not in local database", mId)
return fmt.Errorf("anime is not in local database")
}
err := m.removeAnime(mId)
if err != nil {
return err
}
m.GetSyncer().refreshCollections()
return nil
}
//----------------------------------------------------------------------------------------------------------------------------------------------------
// TrackManga adds a manga to track.
// It checks that the manga is currently in the user's manga collection.
// The manga should have downloaded chapter containers, or else ManagerImpl.Synchronize will remove it from tracking.
func (m *ManagerImpl) TrackManga(mId int) error {
m.logger.Trace().Msgf("local manager: Adding manga %d to local database", mId)
s := &TrackedMedia{
MediaId: mId,
Type: MangaType,
}
// Check if the manga is in the user's manga collection
if m.mangaCollection.IsAbsent() {
m.logger.Error().Msg("local manager: Manga collection not set")
return fmt.Errorf("manga collection not set")
}
if _, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(mId); !found {
m.logger.Error().Msgf("local manager: Manga %d not found in user's manga collection", mId)
return fmt.Errorf("manga is not in AniList collection")
}
if _, found := m.localDb.GetTrackedMedia(mId, MangaType); found {
return ErrAlreadyTracked
}
err := m.localDb.gormdb.Create(s).Error
if err != nil {
m.logger.Error().Msgf("local manager: Failed to add manga %d to local database: %w", mId, err)
return fmt.Errorf("failed to add manga %d to local database: %w", mId, err)
}
return nil
}
func (m *ManagerImpl) UntrackManga(mId int) error {
m.logger.Trace().Msgf("local manager: Removing manga %d from local database", mId)
if _, found := m.localDb.GetTrackedMedia(mId, MangaType); !found {
m.logger.Error().Msgf("local manager: Manga %d not in local database", mId)
return fmt.Errorf("manga is not in local database")
}
err := m.removeManga(mId)
if err != nil {
return err
}
m.GetSyncer().refreshCollections()
return nil
}
//----------------------------------------------------------------------------------------------------------------------------------------------------
func (m *ManagerImpl) IsMediaTracked(aId int, kind string) bool {
_, found := m.localDb.GetTrackedMedia(aId, kind)
return found
}
//----------------------------------------------------------------------------------------------------------------------------------------------------
func (m *ManagerImpl) GetTrackedMediaItems() (ret []*TrackedMediaItem) {
trackedMedia, ok := m.localDb.GetAllTrackedMedia()
if !ok {
return
}
if m.animeCollection.IsAbsent() || m.mangaCollection.IsAbsent() {
return
}
for _, item := range trackedMedia {
if item.Type == AnimeType {
if localAnimeCollection, found := m.localAnimeCollection.Get(); found {
if e, found := localAnimeCollection.GetListEntryFromAnimeId(item.MediaId); found {
ret = append(ret, &TrackedMediaItem{
MediaId: item.MediaId,
Type: item.Type,
AnimeEntry: e,
})
continue
}
if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(item.MediaId); found {
ret = append(ret, &TrackedMediaItem{
MediaId: item.MediaId,
Type: item.Type,
AnimeEntry: e,
})
continue
}
}
} else if item.Type == MangaType {
if localMangaCollection, found := m.localMangaCollection.Get(); found {
if e, found := localMangaCollection.GetListEntryFromMangaId(item.MediaId); found {
ret = append(ret, &TrackedMediaItem{
MediaId: item.MediaId,
Type: item.Type,
MangaEntry: e,
})
continue
}
}
if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(item.MediaId); found {
ret = append(ret, &TrackedMediaItem{
MediaId: item.MediaId,
Type: item.Type,
MangaEntry: e,
})
continue
}
}
}
return
}
//----------------------------------------------------------------------------------------------------------------------------------------------------
// SynchronizeLocal should be called after updates to the user's anime and manga collections.
//
// - After adding/removing an anime or manga to track
// - After the user's anime and manga collections have been updated (e.g. after a user's anime and manga list has been updated)
//
// It will add media list entries from the user's collection to the Syncer only if the media is tracked.
// - The Syncer will then synchronize the anime & manga with the local database if needed
//
// It will remove any anime & manga from the local database that are not in the user's collection anymore.
// It will then update the ManagerImpl.localAnimeCollection and ManagerImpl.localMangaCollection
func (m *ManagerImpl) SynchronizeLocal() error {
localStorageSizeCache = 0
m.loadLocalAnimeCollection()
m.loadLocalMangaCollection()
settings := m.localDb.GetSettings()
if settings.Updated {
return fmt.Errorf("cannot sync, upload or ignore local changes before syncing")
}
lfs, _, err := db_bridge.GetLocalFiles(m.db)
if err != nil {
return fmt.Errorf("local manager: Couldn't start syncing, failed to get local files: %w", err)
}
// Check if the anime and manga collections are set
if m.animeCollection.IsAbsent() {
return fmt.Errorf("local manager: Couldn't start syncing, anime collection not set")
}
if m.mangaCollection.IsAbsent() {
return fmt.Errorf("local manager: Couldn't start syncing, manga collection not set")
}
mangaChapterContainers, err := m.mangaRepository.GetDownloadedChapterContainers(m.mangaCollection.MustGet())
if err != nil {
return fmt.Errorf("local manager: Couldn't start syncing, failed to get downloaded chapter containers: %w", err)
}
return m.synchronize(lfs, mangaChapterContainers)
}
func (m *ManagerImpl) synchronize(lfs []*anime.LocalFile, mangaChapterContainers []*manga.ChapterContainer) error {
m.logger.Trace().Msg("local manager: Synchronizing local database with user's anime and manga collections")
m.localFiles = lfs
m.downloadedChapterContainers = mangaChapterContainers
// Check if the anime and manga collections are set
if m.animeCollection.IsAbsent() {
return fmt.Errorf("local manager: Anime collection not set")
}
if m.mangaCollection.IsAbsent() {
return fmt.Errorf("local manager: Manga collection not set")
}
trackedAnimeMap, trackedMangaMap := m.loadTrackedMedia()
// Remove anime and manga from the local database that are not in the user's anime and manga collections
for _, item := range trackedAnimeMap {
// If the anime is not in the user's anime collection, remove it from the local database
if _, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(item.MediaId); !found {
err := m.removeAnime(item.MediaId)
if err != nil {
return fmt.Errorf("local manager: Failed to remove anime %d from local database: %w", item.MediaId, err)
}
}
}
for _, item := range trackedMangaMap {
// If the manga is not in the user's manga collection, remove it from the local database
if _, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(item.MediaId); !found {
err := m.removeManga(item.MediaId)
if err != nil {
return fmt.Errorf("local manager: Failed to remove manga %d from local database: %w", item.MediaId, err)
}
}
}
// Get snapshots for all tracked anime and manga
animeSnapshots, _ := m.localDb.GetAnimeSnapshots()
mangaSnapshots, _ := m.localDb.GetMangaSnapshots()
// Create a map of the snapshots
animeSnapshotMap := make(map[int]*AnimeSnapshot)
for _, snapshot := range animeSnapshots {
animeSnapshotMap[snapshot.MediaId] = snapshot
}
mangaSnapshotMap := make(map[int]*MangaSnapshot)
for _, snapshot := range mangaSnapshots {
mangaSnapshotMap[snapshot.MediaId] = snapshot
}
m.syncer.runDiffs(trackedAnimeMap, animeSnapshotMap, trackedMangaMap, mangaSnapshotMap, m.localFiles, m.downloadedChapterContainers)
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (m *ManagerImpl) SynchronizeAnilist() error {
if m.animeCollection.IsAbsent() {
return fmt.Errorf("local manager: Anime collection not set")
}
if m.mangaCollection.IsAbsent() {
return fmt.Errorf("local manager: Manga collection not set")
}
m.loadLocalAnimeCollection()
m.loadLocalMangaCollection()
if localAnimeCollection, ok := m.localAnimeCollection.Get(); ok {
for _, list := range localAnimeCollection.MediaListCollection.Lists {
if list.GetStatus() == nil || list.GetEntries() == nil {
continue
}
for _, entry := range list.GetEntries() {
if entry.GetStatus() == nil {
continue
}
// Get the entry from AniList
var originalEntry *anilist.AnimeListEntry
if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(entry.GetMedia().GetID()); found {
originalEntry = e
}
if originalEntry == nil {
continue
}
key1 := GetAnimeListDataKey(entry)
key2 := GetAnimeListDataKey(originalEntry)
// If the entry is the same, skip
if key1 == key2 {
continue
}
var startDate *anilist.FuzzyDateInput
if entry.GetStartedAt() != nil {
startDate = &anilist.FuzzyDateInput{
Year: entry.GetStartedAt().GetYear(),
Month: entry.GetStartedAt().GetMonth(),
Day: entry.GetStartedAt().GetDay(),
}
}
var endDate *anilist.FuzzyDateInput
if entry.GetCompletedAt() != nil {
endDate = &anilist.FuzzyDateInput{
Year: entry.GetCompletedAt().GetYear(),
Month: entry.GetCompletedAt().GetMonth(),
Day: entry.GetCompletedAt().GetDay(),
}
}
var score *int
if entry.GetScore() != nil {
score = lo.ToPtr(int(*entry.GetScore()))
}
_ = m.anilistPlatform.UpdateEntry(
context.Background(),
entry.GetMedia().GetID(),
entry.GetStatus(),
score,
entry.GetProgress(),
startDate,
endDate,
)
}
}
}
if localMangaCollection, ok := m.localMangaCollection.Get(); ok {
for _, list := range localMangaCollection.MediaListCollection.Lists {
if list.GetStatus() == nil || list.GetEntries() == nil {
continue
}
for _, entry := range list.GetEntries() {
if entry.GetStatus() == nil {
continue
}
// Get the entry from AniList
var originalEntry *anilist.MangaListEntry
if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(entry.GetMedia().GetID()); found {
originalEntry = e
}
if originalEntry == nil {
continue
}
key1 := GetMangaListDataKey(entry)
key2 := GetMangaListDataKey(originalEntry)
// If the entry is the same, skip
if key1 == key2 {
continue
}
var startDate *anilist.FuzzyDateInput
if entry.GetStartedAt() != nil {
startDate = &anilist.FuzzyDateInput{
Year: entry.GetStartedAt().GetYear(),
Month: entry.GetStartedAt().GetMonth(),
Day: entry.GetStartedAt().GetDay(),
}
}
var endDate *anilist.FuzzyDateInput
if entry.GetCompletedAt() != nil {
endDate = &anilist.FuzzyDateInput{
Year: entry.GetCompletedAt().GetYear(),
Month: entry.GetCompletedAt().GetMonth(),
Day: entry.GetCompletedAt().GetDay(),
}
}
var score *int
if entry.GetScore() != nil {
score = lo.ToPtr(int(*entry.GetScore()))
}
_ = m.anilistPlatform.UpdateEntry(
context.Background(),
entry.GetMedia().GetID(),
entry.GetStatus(),
score,
entry.GetProgress(),
startDate,
endDate,
)
}
}
}
m.RefreshAnilistCollectionsFunc()
m.wsEventManager.SendEvent(events.RefreshedAnilistAnimeCollection, nil)
m.wsEventManager.SendEvent(events.RefreshedAnilistMangaCollection, nil)
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (m *ManagerImpl) loadTrackedMedia() (trackedAnimeMap map[int]*TrackedMedia, trackedMangaMap map[int]*TrackedMedia) {
trackedAnime, _ := m.localDb.GetAllTrackedMediaByType(AnimeType)
trackedManga, _ := m.localDb.GetAllTrackedMediaByType(MangaType)
trackedAnimeMap = make(map[int]*TrackedMedia)
for _, item := range trackedAnime {
trackedAnimeMap[item.MediaId] = item
}
trackedMangaMap = make(map[int]*TrackedMedia)
for _, m := range trackedManga {
trackedMangaMap[m.MediaId] = m
}
m.GetSyncer().trackedMangaMap = trackedMangaMap
m.GetSyncer().trackedAnimeMap = trackedAnimeMap
return trackedAnimeMap, trackedMangaMap
}
func (m *ManagerImpl) removeAnime(aId int) error {
m.logger.Trace().Msgf("local manager: Removing anime %d from local database", aId)
// Remove the tracked anime
err := m.localDb.RemoveTrackedMedia(aId, AnimeType)
if err != nil {
return fmt.Errorf("local manager: Failed to remove anime %d from local database: %w", aId, err)
}
// Remove the anime snapshot
_ = m.localDb.RemoveAnimeSnapshot(aId)
// Remove the images
_ = m.removeMediaImages(aId)
return nil
}
func (m *ManagerImpl) removeManga(mId int) error {
m.logger.Trace().Msgf("local manager: Removing manga %d from local database", mId)
// Remove the tracked manga
err := m.localDb.RemoveTrackedMedia(mId, MangaType)
if err != nil {
return fmt.Errorf("local manager: Failed to remove manga %d from local database: %w", mId, err)
}
// Remove the manga snapshot
_ = m.localDb.RemoveMangaSnapshot(mId)
// Remove the images
_ = m.removeMediaImages(mId)
return nil
}
// removeMediaImages removes the images for the media with the given ID.
// - The images are stored in the local assets' directory.
// - e.g. datadir/local/assets/{mediaId}/*
func (m *ManagerImpl) removeMediaImages(mediaId int) error {
m.logger.Trace().Msgf("local manager: Removing images for media %d", mediaId)
path := filepath.Join(m.localAssetsDir, fmt.Sprintf("%d", mediaId))
_ = os.RemoveAll(path)
//if err != nil {
// return fmt.Errorf("local manager: Failed to remove images for media %d: %w", mediaId, err)
//}
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Avoids recalculating the size of the cache directory every time it is requested
var localStorageSizeCache int64
func (m *ManagerImpl) GetLocalStorageSize() int64 {
if localStorageSizeCache != 0 {
return localStorageSizeCache
}
var size int64
_ = filepath.Walk(m.localDir, func(path string, info os.FileInfo, err error) error {
if info != nil {
size += info.Size()
}
return nil
})
localStorageSizeCache = size
return size
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (m *ManagerImpl) GetSimulatedAnimeCollection() mo.Option[*anilist.AnimeCollection] {
ac, ok := m.localDb.GetSimulatedAnimeCollection()
if !ok {
return mo.None[*anilist.AnimeCollection]()
}
return mo.Some(ac)
}
func (m *ManagerImpl) GetSimulatedMangaCollection() mo.Option[*anilist.MangaCollection] {
mc, ok := m.localDb.GetSimulatedMangaCollection()
if !ok {
return mo.None[*anilist.MangaCollection]()
}
return mo.Some(mc)
}
func (m *ManagerImpl) SaveSimulatedAnimeCollection(ac *anilist.AnimeCollection) {
//// Remove airing dates from each entry
//for _, list := range ac.MediaListCollection.Lists {
// for _, entry := range list.Entries {
// entry.GetMedia().NextAiringEpisode = nil
// }
//}
_ = m.localDb.SaveSimulatedAnimeCollection(ac)
}
func (m *ManagerImpl) SaveSimulatedMangaCollection(mc *anilist.MangaCollection) {
_ = m.localDb.SaveSimulatedMangaCollection(mc)
}
func (m *ManagerImpl) SynchronizeAnilistToSimulatedCollection() error {
if animeCollection, ok := m.animeCollection.Get(); ok {
m.SaveSimulatedAnimeCollection(animeCollection)
}
if mangaCollection, ok := m.mangaCollection.Get(); ok {
m.SaveSimulatedMangaCollection(mangaCollection)
}
return nil
}
func (m *ManagerImpl) SynchronizeSimulatedCollectionToAnilist() error {
if localAnimeCollection, ok := m.localDb.GetSimulatedAnimeCollection(); ok {
for _, list := range localAnimeCollection.MediaListCollection.Lists {
if list.GetStatus() == nil || list.GetEntries() == nil {
continue
}
for _, entry := range list.GetEntries() {
if entry.GetStatus() == nil {
continue
}
// Get the entry from AniList
var originalEntry *anilist.AnimeListEntry
if e, found := m.animeCollection.MustGet().GetListEntryFromAnimeId(entry.GetMedia().GetID()); found {
originalEntry = e
}
if originalEntry == nil {
continue
}
key1 := GetAnimeListDataKey(entry)
key2 := GetAnimeListDataKey(originalEntry)
// If the entry is the same, skip
if key1 == key2 {
continue
}
var startDate *anilist.FuzzyDateInput
if entry.GetStartedAt() != nil {
startDate = &anilist.FuzzyDateInput{
Year: entry.GetStartedAt().GetYear(),
Month: entry.GetStartedAt().GetMonth(),
Day: entry.GetStartedAt().GetDay(),
}
}
var endDate *anilist.FuzzyDateInput
if entry.GetCompletedAt() != nil {
endDate = &anilist.FuzzyDateInput{
Year: entry.GetCompletedAt().GetYear(),
Month: entry.GetCompletedAt().GetMonth(),
Day: entry.GetCompletedAt().GetDay(),
}
}
var score *int
if entry.GetScore() != nil {
score = lo.ToPtr(int(*entry.GetScore()))
} else {
score = lo.ToPtr(0)
}
_ = m.anilistPlatform.UpdateEntry(
context.Background(),
entry.GetMedia().GetID(),
entry.GetStatus(),
score,
entry.GetProgress(),
startDate,
endDate,
)
}
}
}
if localMangaCollection, ok := m.localDb.GetSimulatedMangaCollection(); ok {
for _, list := range localMangaCollection.MediaListCollection.Lists {
if list.GetStatus() == nil || list.GetEntries() == nil {
continue
}
for _, entry := range list.GetEntries() {
if entry.GetStatus() == nil {
continue
}
// Get the entry from AniList
var originalEntry *anilist.MangaListEntry
if e, found := m.mangaCollection.MustGet().GetListEntryFromMangaId(entry.GetMedia().GetID()); found {
originalEntry = e
}
if originalEntry == nil {
continue
}
key1 := GetMangaListDataKey(entry)
key2 := GetMangaListDataKey(originalEntry)
// If the entry is the same, skip
if key1 == key2 {
continue
}
var startDate *anilist.FuzzyDateInput
if entry.GetStartedAt() != nil {
startDate = &anilist.FuzzyDateInput{
Year: entry.GetStartedAt().GetYear(),
Month: entry.GetStartedAt().GetMonth(),
Day: entry.GetStartedAt().GetDay(),
}
}
var endDate *anilist.FuzzyDateInput
if entry.GetCompletedAt() != nil {
endDate = &anilist.FuzzyDateInput{
Year: entry.GetCompletedAt().GetYear(),
Month: entry.GetCompletedAt().GetMonth(),
Day: entry.GetCompletedAt().GetDay(),
}
}
var score *int
if entry.GetScore() != nil {
score = lo.ToPtr(int(*entry.GetScore()))
} else {
score = lo.ToPtr(0)
}
_ = m.anilistPlatform.UpdateEntry(
context.Background(),
entry.GetMedia().GetID(),
entry.GetStatus(),
score,
entry.GetProgress(),
startDate,
endDate,
)
}
}
}
m.RefreshAnilistCollectionsFunc()
m.wsEventManager.SendEvent(events.RefreshedAnilistAnimeCollection, nil)
m.wsEventManager.SendEvent(events.RefreshedAnilistMangaCollection, nil)
return nil
}