328 lines
11 KiB
Go
328 lines
11 KiB
Go
package local
|
|
|
|
import (
|
|
"fmt"
|
|
"seanime/internal/api/anilist"
|
|
hibikemanga "seanime/internal/extension/hibike/manga"
|
|
"seanime/internal/library/anime"
|
|
"seanime/internal/manga"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/rs/zerolog"
|
|
"github.com/samber/lo"
|
|
"github.com/samber/mo"
|
|
)
|
|
|
|
// DEVNOTE: Here we compare the media data from the current up-to-date collections with the local data.
|
|
// Outdated media are added to the Syncer to be updated.
|
|
// If the media doesn't have a snapshot -> a new snapshot is created.
|
|
// If the reference key is different -> the metadata is re-fetched and the snapshot is updated.
|
|
// If the list data is different -> the list data is updated.
|
|
|
|
const (
|
|
DiffTypeMissing DiffType = iota // We need to add a new snapshot
|
|
DiffTypeMetadata // We need to re-fetch the snapshot metadata (episode metadata / chapter containers), list data will be updated as well
|
|
DiffTypeListData // We need to update the list data
|
|
)
|
|
|
|
type (
|
|
Diff struct {
|
|
Logger *zerolog.Logger
|
|
}
|
|
|
|
DiffType int
|
|
)
|
|
|
|
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
type GetAnimeDiffOptions struct {
|
|
Collection *anilist.AnimeCollection
|
|
LocalCollection mo.Option[*anilist.AnimeCollection]
|
|
LocalFiles []*anime.LocalFile
|
|
TrackedAnime map[int]*TrackedMedia
|
|
Snapshots map[int]*AnimeSnapshot
|
|
}
|
|
|
|
type AnimeDiffResult struct {
|
|
AnimeEntry *anilist.AnimeListEntry
|
|
AnimeSnapshot *AnimeSnapshot
|
|
DiffType DiffType
|
|
}
|
|
|
|
// GetAnimeDiffs returns the anime that have changed.
|
|
// The anime is considered changed if:
|
|
// - It doesn't have a snapshot
|
|
// - The reference key is different (e.g. the number of local files has changed), meaning we need to update the snapshot.
|
|
func (d *Diff) GetAnimeDiffs(opts GetAnimeDiffOptions) map[int]*AnimeDiffResult {
|
|
|
|
collection := opts.Collection
|
|
localCollection := opts.LocalCollection
|
|
trackedAnimeMap := opts.TrackedAnime
|
|
snapshotMap := opts.Snapshots
|
|
|
|
changedMap := make(map[int]*AnimeDiffResult)
|
|
|
|
if len(collection.MediaListCollection.Lists) == 0 || len(trackedAnimeMap) == 0 {
|
|
return changedMap
|
|
}
|
|
|
|
for _, _list := range collection.MediaListCollection.Lists {
|
|
if _list.GetStatus() == nil || _list.GetEntries() == nil {
|
|
continue
|
|
}
|
|
for _, _entry := range _list.GetEntries() {
|
|
// Check if the anime is tracked
|
|
_, isTracked := trackedAnimeMap[_entry.GetMedia().GetID()]
|
|
if !isTracked {
|
|
continue
|
|
}
|
|
|
|
if localCollection.IsAbsent() {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Anime %d, local collection is missing", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
|
|
AnimeEntry: _entry,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next anime
|
|
}
|
|
|
|
// Check if the anime has a snapshot
|
|
snapshot, hasSnapshot := snapshotMap[_entry.GetMedia().GetID()]
|
|
if !hasSnapshot {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Anime %d is missing a snapshot", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
|
|
AnimeEntry: _entry,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next anime
|
|
}
|
|
|
|
_lfs := lo.Filter(opts.LocalFiles, func(lf *anime.LocalFile, _ int) bool {
|
|
return lf.MediaId == _entry.GetMedia().GetID()
|
|
})
|
|
|
|
// Check if the anime has changed
|
|
_referenceKey := GetAnimeReferenceKey(_entry.Media, _lfs)
|
|
|
|
// Check if the reference key is different
|
|
if snapshotMap[_entry.GetMedia().GetID()].ReferenceKey != _referenceKey {
|
|
d.Logger.Trace().Str("localReferenceKey", snapshotMap[_entry.GetMedia().GetID()].ReferenceKey).Str("currentReferenceKey", _referenceKey).Msgf("local manager: Diff > Anime %d has an outdated snapshot", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
|
|
AnimeEntry: _entry,
|
|
AnimeSnapshot: snapshot,
|
|
DiffType: DiffTypeMetadata,
|
|
}
|
|
continue // Go to the next anime
|
|
}
|
|
|
|
localEntry, found := localCollection.MustGet().GetListEntryFromAnimeId(_entry.GetMedia().GetID())
|
|
if !found {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Anime %d is missing from the local collection", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
|
|
AnimeEntry: _entry,
|
|
AnimeSnapshot: snapshot,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next anime
|
|
}
|
|
|
|
// Check if the list data has changed
|
|
_listDataKey := GetAnimeListDataKey(_entry)
|
|
localListDataKey := GetAnimeListDataKey(localEntry)
|
|
|
|
if _listDataKey != localListDataKey {
|
|
d.Logger.Trace().Str("localListDataKey", localListDataKey).Str("currentListDataKey", _listDataKey).Msgf("local manager: Diff > Anime %d has changed list data", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &AnimeDiffResult{
|
|
AnimeEntry: _entry,
|
|
AnimeSnapshot: snapshot,
|
|
DiffType: DiffTypeListData,
|
|
}
|
|
continue // Go to the next anime
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return changedMap
|
|
}
|
|
|
|
//----------------------------------------------------------------------------------------------------------------------------------------------------
|
|
|
|
type GetMangaDiffOptions struct {
|
|
Collection *anilist.MangaCollection
|
|
LocalCollection mo.Option[*anilist.MangaCollection]
|
|
DownloadedChapterContainers []*manga.ChapterContainer
|
|
TrackedManga map[int]*TrackedMedia
|
|
Snapshots map[int]*MangaSnapshot
|
|
}
|
|
|
|
type MangaDiffResult struct {
|
|
MangaEntry *anilist.MangaListEntry
|
|
MangaSnapshot *MangaSnapshot
|
|
DiffType DiffType
|
|
}
|
|
|
|
// GetMangaDiffs returns the manga that have changed.
|
|
func (d *Diff) GetMangaDiffs(opts GetMangaDiffOptions) map[int]*MangaDiffResult {
|
|
|
|
collection := opts.Collection
|
|
localCollection := opts.LocalCollection
|
|
trackedMangaMap := opts.TrackedManga
|
|
snapshotMap := opts.Snapshots
|
|
|
|
changedMap := make(map[int]*MangaDiffResult)
|
|
|
|
if len(collection.MediaListCollection.Lists) == 0 || len(trackedMangaMap) == 0 {
|
|
return changedMap
|
|
}
|
|
|
|
for _, _list := range collection.MediaListCollection.Lists {
|
|
if _list.GetStatus() == nil || _list.GetEntries() == nil {
|
|
continue
|
|
}
|
|
for _, _entry := range _list.GetEntries() {
|
|
// Check if the manga is tracked
|
|
_, isTracked := trackedMangaMap[_entry.GetMedia().GetID()]
|
|
if !isTracked {
|
|
continue
|
|
}
|
|
|
|
if localCollection.IsAbsent() {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Manga %d, local collection is missing", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
|
|
MangaEntry: _entry,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next manga
|
|
}
|
|
|
|
// Check if the manga has a snapshot
|
|
snapshot, hasSnapshot := snapshotMap[_entry.GetMedia().GetID()]
|
|
if !hasSnapshot {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Manga %d is missing a snapshot", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
|
|
MangaEntry: _entry,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next manga
|
|
}
|
|
|
|
// Check if the manga has changed
|
|
_referenceKey := GetMangaReferenceKey(_entry.Media, opts.DownloadedChapterContainers)
|
|
|
|
// Check if the reference key is different
|
|
if snapshotMap[_entry.GetMedia().GetID()].ReferenceKey != _referenceKey {
|
|
d.Logger.Trace().Str("localReferenceKey", snapshotMap[_entry.GetMedia().GetID()].ReferenceKey).Str("currentReferenceKey", _referenceKey).Msgf("local manager: Diff > Manga %d has an outdated snapshot", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
|
|
MangaEntry: _entry,
|
|
MangaSnapshot: snapshot,
|
|
DiffType: DiffTypeMetadata,
|
|
}
|
|
continue // Go to the next manga
|
|
}
|
|
|
|
localEntry, found := localCollection.MustGet().GetListEntryFromMangaId(_entry.GetMedia().GetID())
|
|
if !found {
|
|
d.Logger.Trace().Msgf("local manager: Diff > Manga %d is missing from the local collection", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
|
|
MangaEntry: _entry,
|
|
MangaSnapshot: snapshot,
|
|
DiffType: DiffTypeMissing,
|
|
}
|
|
continue // Go to the next manga
|
|
}
|
|
|
|
// Check if the list data has changed
|
|
_listDataKey := GetMangaListDataKey(_entry)
|
|
localListDataKey := GetMangaListDataKey(localEntry)
|
|
|
|
if _listDataKey != localListDataKey {
|
|
d.Logger.Trace().Str("localListDataKey", localListDataKey).Str("currentListDataKey", _listDataKey).Msgf("local manager: Diff > Manga %d has changed list data", _entry.GetMedia().GetID())
|
|
changedMap[_entry.GetMedia().GetID()] = &MangaDiffResult{
|
|
MangaEntry: _entry,
|
|
MangaSnapshot: snapshot,
|
|
DiffType: DiffTypeListData,
|
|
}
|
|
continue // Go to the next manga
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return changedMap
|
|
}
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func GetAnimeReferenceKey(bAnime *anilist.BaseAnime, lfs []*anime.LocalFile) string {
|
|
// Reference key is used to compare the snapshot with the current data.
|
|
// If the reference key is different, the snapshot is outdated.
|
|
animeLfs := lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
|
|
return lf.MediaId == bAnime.ID
|
|
})
|
|
|
|
// Extract the paths and sort them to maintain a consistent order.
|
|
paths := lo.Map(animeLfs, func(lf *anime.LocalFile, _ int) string {
|
|
return lf.Path
|
|
})
|
|
slices.Sort(paths)
|
|
|
|
return fmt.Sprintf("%d-%s", bAnime.ID, strings.Join(paths, ","))
|
|
}
|
|
|
|
func GetMangaReferenceKey(bManga *anilist.BaseManga, dcc []*manga.ChapterContainer) string {
|
|
// Reference key is used to compare the snapshot with the current data.
|
|
// If the reference key is different, the snapshot is outdated.
|
|
mangaDcc := lo.Filter(dcc, func(dc *manga.ChapterContainer, _ int) bool {
|
|
return dc.MediaId == bManga.ID
|
|
})
|
|
|
|
slices.SortFunc(mangaDcc, func(i, j *manga.ChapterContainer) int {
|
|
return strings.Compare(i.Provider, j.Provider)
|
|
})
|
|
var k string
|
|
for _, dc := range mangaDcc {
|
|
l := dc.Provider + "-"
|
|
slices.SortFunc(dc.Chapters, func(i, j *hibikemanga.ChapterDetails) int {
|
|
return strings.Compare(i.ID, j.ID)
|
|
})
|
|
for _, c := range dc.Chapters {
|
|
l += c.ID + "-"
|
|
}
|
|
k += l
|
|
}
|
|
|
|
return fmt.Sprintf("%d-%s", bManga.ID, k)
|
|
}
|
|
|
|
func GetAnimeListDataKey(entry *anilist.AnimeListEntry) string {
|
|
return fmt.Sprintf("%s-%d-%f-%d-%v-%v-%v-%v-%v-%v",
|
|
MediaListStatusPointerValue(entry.GetStatus()),
|
|
IntPointerValue(entry.GetProgress()),
|
|
Float64PointerValue(entry.GetScore()),
|
|
IntPointerValue(entry.GetRepeat()),
|
|
IntPointerValue(entry.GetStartedAt().GetYear()),
|
|
IntPointerValue(entry.GetStartedAt().GetMonth()),
|
|
IntPointerValue(entry.GetStartedAt().GetDay()),
|
|
IntPointerValue(entry.GetCompletedAt().GetYear()),
|
|
IntPointerValue(entry.GetCompletedAt().GetMonth()),
|
|
IntPointerValue(entry.GetCompletedAt().GetDay()),
|
|
)
|
|
}
|
|
|
|
func GetMangaListDataKey(entry *anilist.MangaListEntry) string {
|
|
return fmt.Sprintf("%s-%d-%f-%d-%v-%v-%v-%v-%v-%v",
|
|
MediaListStatusPointerValue(entry.GetStatus()),
|
|
IntPointerValue(entry.GetProgress()),
|
|
Float64PointerValue(entry.GetScore()),
|
|
IntPointerValue(entry.GetRepeat()),
|
|
IntPointerValue(entry.GetStartedAt().GetYear()),
|
|
IntPointerValue(entry.GetStartedAt().GetMonth()),
|
|
IntPointerValue(entry.GetStartedAt().GetDay()),
|
|
IntPointerValue(entry.GetCompletedAt().GetYear()),
|
|
IntPointerValue(entry.GetCompletedAt().GetMonth()),
|
|
IntPointerValue(entry.GetCompletedAt().GetDay()),
|
|
)
|
|
}
|