1040 lines
32 KiB
Go
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
|
|
}
|