mirror of
https://github.com/5rahim/seanime
synced 2026-04-18 22:24:55 +02:00
371 lines
9.8 KiB
Go
371 lines
9.8 KiB
Go
package offline
|
|
|
|
import (
|
|
"github.com/goccy/go-json"
|
|
"github.com/samber/lo"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/database/db_bridge"
|
|
"seanime/internal/library/anime"
|
|
"seanime/internal/manga"
|
|
"seanime/internal/util/limiter"
|
|
"slices"
|
|
"time"
|
|
)
|
|
|
|
type (
|
|
NewSnapshotOptions struct {
|
|
AnimeToDownload []int // MediaIds
|
|
DownloadAssetsOf []int // MediaIds
|
|
}
|
|
)
|
|
|
|
// CreateSnapshot creates a snapshot of the current state of the library and stores it for offline use.
|
|
// This is called by the user before going offline.
|
|
func (h *Hub) CreateSnapshot(opts *NewSnapshotOptions) error {
|
|
|
|
h.logger.Debug().Msg("offline hub: Creating snapshot")
|
|
|
|
// Get local files
|
|
lfs, _, err := db_bridge.GetLocalFiles(h.db)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Retrieved local files")
|
|
|
|
// Get user
|
|
dbAcc, err := h.db.GetAccount()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
user, err := anime.NewUser(dbAcc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Retrieved user")
|
|
|
|
//
|
|
// Collections
|
|
//
|
|
animeCollection, err := h.platform.GetAnimeCollection(false)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: [Snapshot] Failed to get Anilist anime collection")
|
|
return err
|
|
}
|
|
mangaCollection, err := h.platform.GetMangaCollection(false)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: [Snapshot] Failed to get Anilist manga collection")
|
|
return err
|
|
}
|
|
collections := Collections{
|
|
AnimeCollection: animeCollection,
|
|
MangaCollection: mangaCollection,
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Retrieved collections")
|
|
|
|
//
|
|
// Anime Entries
|
|
//
|
|
|
|
animeEntries := make([]*AnimeEntry, 0)
|
|
|
|
lfWrapper := anime.NewLocalFileWrapper(lfs)
|
|
lfEntries := lfWrapper.GetLocalEntries()
|
|
|
|
rateLimiter := limiter.NewLimiter(1*time.Second, 5)
|
|
for _, lfEntry := range lfEntries {
|
|
if !slices.Contains(opts.AnimeToDownload, lfEntry.GetMediaId()) {
|
|
continue
|
|
}
|
|
|
|
// Get the media
|
|
listEntry, ok := animeCollection.GetListEntryFromAnimeId(lfEntry.GetMediaId())
|
|
if !ok {
|
|
h.logger.Error().Err(err).Msgf("offline hub: [Snapshot] Failed to get Anilist media %d", lfEntry.GetMediaId())
|
|
return err
|
|
}
|
|
|
|
if listEntry.GetStatus() == nil {
|
|
continue
|
|
}
|
|
|
|
h.logger.Debug().Msgf("offline hub: Creating media entry snapshot for media %d", lfEntry.GetMediaId())
|
|
|
|
rateLimiter.Wait()
|
|
_mediaEntry, err := anime.NewAnimeEntry(&anime.NewAnimeEntryOptions{
|
|
MediaId: lfEntry.GetMediaId(),
|
|
LocalFiles: lfs,
|
|
AnimeCollection: animeCollection,
|
|
Platform: h.platform,
|
|
MetadataProvider: h.metadataProvider,
|
|
})
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msgf("offline hub: [Snapshot] Failed to create media entry for anime %d", lfEntry.GetMediaId())
|
|
return err
|
|
}
|
|
|
|
mediaEpisodes := _mediaEntry.Episodes
|
|
// Note: We don't need the BaseAnime in each episode for the snapshot
|
|
// it's a waste of space
|
|
for _, episode := range mediaEpisodes {
|
|
episode.BaseAnime = nil
|
|
}
|
|
|
|
// Create the AnimeEntry
|
|
animeEntry := &AnimeEntry{
|
|
MediaId: lfEntry.GetMediaId(),
|
|
ListData: &ListData{
|
|
Score: int(*listEntry.GetScore()),
|
|
Status: *listEntry.GetStatus(),
|
|
Progress: *listEntry.GetProgress(),
|
|
StartedAt: anilist.FuzzyDateToString(listEntry.StartedAt),
|
|
CompletedAt: anilist.FuzzyDateToString(listEntry.CompletedAt),
|
|
},
|
|
Media: listEntry.GetMedia(),
|
|
Episodes: mediaEpisodes,
|
|
//DownloadedAssets: slices.Contains(opts.DownloadAssetsOf, lfEntry.GetMediaId()),
|
|
DownloadedAssets: true,
|
|
}
|
|
|
|
// Add the AnimeEntry
|
|
animeEntries = append(animeEntries, animeEntry)
|
|
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Generated anime entries")
|
|
|
|
//
|
|
// Manga Entries
|
|
//
|
|
|
|
mangaEntries := make([]*MangaEntry, 0)
|
|
|
|
containers, err := h.mangaRepository.GetDownloadedChapterContainers(mangaCollection)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: [Snapshot] Failed to get downloaded manga chapters")
|
|
return err
|
|
}
|
|
|
|
uniqContainers := lo.UniqBy(containers, func(c *manga.ChapterContainer) int {
|
|
return c.MediaId
|
|
})
|
|
|
|
for _, container := range uniqContainers {
|
|
// Get the media
|
|
listEntry, ok := mangaCollection.GetListEntryFromMediaId(container.MediaId)
|
|
if !ok {
|
|
h.logger.Error().Err(err).Msgf("offline hub: [Snapshot] Failed to get Anilist media %d", container.MediaId)
|
|
return err
|
|
}
|
|
|
|
if listEntry.GetStatus() == nil {
|
|
continue
|
|
}
|
|
|
|
h.logger.Debug().Msgf("offline hub: Creating media entry snapshot for manga %d", container.MediaId)
|
|
|
|
// Get all chapter containers for this media
|
|
// A manga entry can have multiple chapter containers due to different sources
|
|
eContainers := make([]*manga.ChapterContainer, 0)
|
|
for _, c := range containers {
|
|
if c.MediaId == container.MediaId {
|
|
eContainers = append(eContainers, c)
|
|
}
|
|
}
|
|
|
|
// Create the MangaEntry
|
|
mangaEntry := &MangaEntry{
|
|
MediaId: container.MediaId,
|
|
ListData: &ListData{
|
|
Score: int(*listEntry.GetScore()),
|
|
Status: *listEntry.GetStatus(),
|
|
Progress: *listEntry.GetProgress(),
|
|
StartedAt: anilist.FuzzyDateToString(listEntry.StartedAt),
|
|
CompletedAt: anilist.FuzzyDateToString(listEntry.CompletedAt),
|
|
},
|
|
Media: listEntry.GetMedia(),
|
|
ChapterContainers: eContainers,
|
|
//DownloadedAssets: slices.Contains(opts.DownloadAssetsOf, container.MediaId),
|
|
DownloadedAssets: true,
|
|
}
|
|
|
|
// Add the MangaEntry
|
|
mangaEntries = append(mangaEntries, mangaEntry)
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Generated manga entries")
|
|
|
|
//
|
|
// DownloadAssets
|
|
//
|
|
assetMap, err := h.assetsHandler.DownloadAssets(animeEntries, mangaEntries, user, opts.DownloadAssetsOf)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: [Snapshot] Failed to download assets")
|
|
return err
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Downloaded assets")
|
|
|
|
snapshot := Snapshot{
|
|
User: user,
|
|
Collections: &collections,
|
|
Entries: &Entries{
|
|
AnimeEntries: animeEntries,
|
|
MangaEntries: mangaEntries,
|
|
},
|
|
AssetMap: assetMap,
|
|
}
|
|
|
|
marshaledUser, err := json.Marshal(snapshot.User)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
marshaledCollections, err := json.Marshal(snapshot.Collections)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
marshaledAssetMap, err := json.Marshal(snapshot.AssetMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save the snapshot
|
|
snapshotEntry, err := h.offlineDb.InsertSnapshot(marshaledUser, marshaledCollections, marshaledAssetMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
h.logger.Info().Msg("offline hub: Saved snapshot")
|
|
|
|
// Save the snapshot media entries
|
|
for _, animeEntry := range animeEntries {
|
|
marshaledAnimeEntry, err := json.Marshal(animeEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = h.offlineDb.InsertSnapshotMediaEntry(snapshotEntry.ID, SnapshotMediaEntryTypeAnime, animeEntry.MediaId, marshaledAnimeEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, mangaEntry := range mangaEntries {
|
|
marshaledMangaEntry, err := json.Marshal(mangaEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = h.offlineDb.InsertSnapshotMediaEntry(snapshotEntry.ID, SnapshotMediaEntryTypeManga, mangaEntry.MediaId, marshaledMangaEntry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
h.logger.Info().Msg("offline hub: Saved snapshot media entries")
|
|
|
|
return err
|
|
}
|
|
|
|
func (h *Hub) GetLatestSnapshotEntry() (snapshotEntry *SnapshotEntry, err error) {
|
|
return h.offlineDb.GetLatestSnapshot()
|
|
}
|
|
|
|
func (h *Hub) GetLatestSnapshot(bypassCache bool) (snapshot *Snapshot, err error) {
|
|
|
|
if h.currentSnapshot != nil && !bypassCache {
|
|
return h.currentSnapshot, nil
|
|
}
|
|
|
|
h.logger.Debug().Msg("offline hub: Getting latest snapshot")
|
|
|
|
snapshot = &Snapshot{
|
|
User: &anime.User{},
|
|
Entries: &Entries{
|
|
AnimeEntries: make([]*AnimeEntry, 0),
|
|
MangaEntries: make([]*MangaEntry, 0),
|
|
},
|
|
Collections: &Collections{},
|
|
AssetMap: new(AssetMapImageMap),
|
|
}
|
|
|
|
snapshotEntry, err := h.offlineDb.GetLatestSnapshot()
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to get latest snapshot")
|
|
return nil, err
|
|
}
|
|
|
|
if snapshotEntry == nil {
|
|
h.logger.Info().Msg("offline hub: No snapshot found")
|
|
return nil, nil
|
|
}
|
|
|
|
snapshotEntry.Synced = false
|
|
if h.isOffline {
|
|
snapshotEntry.Used = true
|
|
}
|
|
go func() {
|
|
_, _ = h.offlineDb.UpdateSnapshotT(snapshotEntry)
|
|
}()
|
|
|
|
snapshot.DbId = snapshotEntry.ID
|
|
|
|
// Get the user
|
|
err = json.Unmarshal(snapshotEntry.User, &snapshot.User)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to unmarshal user")
|
|
return nil, err
|
|
}
|
|
|
|
// Get the collections
|
|
err = json.Unmarshal(snapshotEntry.Collections, &snapshot.Collections)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to unmarshal collections")
|
|
return nil, err
|
|
}
|
|
|
|
// Get the asset map
|
|
err = json.Unmarshal(snapshotEntry.AssetMap, &snapshot.AssetMap)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to unmarshal asset map")
|
|
return nil, err
|
|
}
|
|
|
|
// Get the snapshot media entries
|
|
mediaEntries, err := h.offlineDb.GetSnapshotMediaEntries(snapshotEntry.ID)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to get snapshot media entries")
|
|
return nil, err
|
|
}
|
|
|
|
for _, mediaEntry := range mediaEntries {
|
|
switch SnapshotMediaEntryType(mediaEntry.Type) {
|
|
case SnapshotMediaEntryTypeAnime:
|
|
animeEntry := &AnimeEntry{}
|
|
err = json.Unmarshal(mediaEntry.Value, animeEntry)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to unmarshal anime entry")
|
|
return nil, err
|
|
}
|
|
snapshot.Entries.AnimeEntries = append(snapshot.Entries.AnimeEntries, animeEntry)
|
|
case SnapshotMediaEntryTypeManga:
|
|
mangaEntry := &MangaEntry{}
|
|
err = json.Unmarshal(mediaEntry.Value, mangaEntry)
|
|
if err != nil {
|
|
h.logger.Error().Err(err).Msg("offline hub: Failed to unmarshal manga entry")
|
|
return nil, err
|
|
}
|
|
snapshot.Entries.MangaEntries = append(snapshot.Entries.MangaEntries, mangaEntry)
|
|
}
|
|
}
|
|
|
|
h.logger.Info().Msg("offline hub: Retrieved latest snapshot")
|
|
|
|
h.currentSnapshot = snapshot
|
|
|
|
return snapshot, nil
|
|
}
|