node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,309 @@
package anime
import (
"cmp"
"context"
"fmt"
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/hook"
"seanime/internal/platforms/platform"
"seanime/internal/util/result"
"slices"
"time"
"github.com/rs/zerolog"
"github.com/samber/lo"
)
var episodeCollectionCache = result.NewBoundedCache[int, *EpisodeCollection](10)
var EpisodeCollectionFromLocalFilesCache = result.NewBoundedCache[int, *EpisodeCollection](10)
type (
// EpisodeCollection represents a collection of episodes.
EpisodeCollection struct {
HasMappingError bool `json:"hasMappingError"`
Episodes []*Episode `json:"episodes"`
Metadata *metadata.AnimeMetadata `json:"metadata"`
}
)
type NewEpisodeCollectionOptions struct {
// AnimeMetadata can be nil, if not provided, it will be fetched from the metadata provider.
AnimeMetadata *metadata.AnimeMetadata
Media *anilist.BaseAnime
MetadataProvider metadata.Provider
Logger *zerolog.Logger
}
// NewEpisodeCollection creates a new episode collection by leveraging EntryDownloadInfo.
// The returned EpisodeCollection is cached for 6 hours.
//
// AnimeMetadata is optional, if not provided, it will be fetched from the metadata provider.
//
// Note: This is used by Torrent and Debrid streaming
func NewEpisodeCollection(opts NewEpisodeCollectionOptions) (ec *EpisodeCollection, err error) {
if opts.Logger == nil {
opts.Logger = lo.ToPtr(zerolog.Nop())
}
if opts.Media == nil {
return nil, fmt.Errorf("cannont create episode collectiom, media is nil")
}
if opts.MetadataProvider == nil {
return nil, fmt.Errorf("cannot create episode collection, metadata provider is nil")
}
if ec, ok := episodeCollectionCache.Get(opts.Media.ID); ok {
opts.Logger.Debug().Msg("torrentstream: Using cached episode collection")
return ec, nil
}
if opts.AnimeMetadata == nil {
// Fetch the metadata
opts.AnimeMetadata, err = opts.MetadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, opts.Media.ID)
if err != nil {
opts.AnimeMetadata = &metadata.AnimeMetadata{
Titles: make(map[string]string),
Episodes: make(map[string]*metadata.EpisodeMetadata),
EpisodeCount: 0,
SpecialCount: 0,
Mappings: &metadata.AnimeMappings{
AnilistId: opts.Media.GetID(),
},
}
opts.AnimeMetadata.Titles["en"] = opts.Media.GetTitleSafe()
opts.AnimeMetadata.Titles["x-jat"] = opts.Media.GetRomajiTitleSafe()
err = nil
}
}
reqEvent := &AnimeEpisodeCollectionRequestedEvent{
Media: opts.Media,
Metadata: opts.AnimeMetadata,
EpisodeCollection: &EpisodeCollection{},
}
err = hook.GlobalHookManager.OnAnimEpisodeCollectionRequested().Trigger(reqEvent)
if err != nil {
return nil, err
}
opts.Media = reqEvent.Media
opts.AnimeMetadata = reqEvent.Metadata
if reqEvent.DefaultPrevented {
return reqEvent.EpisodeCollection, nil
}
ec = &EpisodeCollection{
HasMappingError: false,
Episodes: make([]*Episode, 0),
Metadata: opts.AnimeMetadata,
}
// +---------------------+
// | Download Info |
// +---------------------+
info, err := NewEntryDownloadInfo(&NewEntryDownloadInfoOptions{
LocalFiles: nil,
AnimeMetadata: opts.AnimeMetadata,
Progress: lo.ToPtr(0), // Progress is 0 because we want the entire list
Status: lo.ToPtr(anilist.MediaListStatusCurrent),
Media: opts.Media,
MetadataProvider: opts.MetadataProvider,
})
if err != nil {
opts.Logger.Error().Err(err).Msg("torrentstream: could not get media entry info")
return nil, err
}
// As of v2.8.0, this should never happen, getMediaInfo always returns an anime metadata struct, even if it's not found
// causing NewEntryDownloadInfo to return a valid list of episodes to download
if info == nil || info.EpisodesToDownload == nil {
opts.Logger.Debug().Msg("torrentstream: no episodes found from AniDB, using AniList")
for epIdx := range opts.Media.GetCurrentEpisodeCount() {
episodeNumber := epIdx + 1
mediaWrapper := opts.MetadataProvider.GetAnimeMetadataWrapper(opts.Media, nil)
episodeMetadata := mediaWrapper.GetEpisodeMetadata(episodeNumber)
episode := &Episode{
Type: LocalFileTypeMain,
DisplayTitle: fmt.Sprintf("Episode %d", episodeNumber),
EpisodeTitle: opts.Media.GetPreferredTitle(),
EpisodeNumber: episodeNumber,
AniDBEpisode: fmt.Sprintf("%d", episodeNumber),
AbsoluteEpisodeNumber: episodeNumber,
ProgressNumber: episodeNumber,
LocalFile: nil,
IsDownloaded: false,
EpisodeMetadata: &EpisodeMetadata{
AnidbId: 0,
Image: episodeMetadata.Image,
AirDate: "",
Length: 0,
Summary: "",
Overview: "",
IsFiller: false,
},
FileMetadata: nil,
IsInvalid: false,
MetadataIssue: "",
BaseAnime: opts.Media,
}
ec.Episodes = append(ec.Episodes, episode)
}
ec.HasMappingError = true
return
}
if len(info.EpisodesToDownload) == 0 {
opts.Logger.Error().Msg("torrentstream: no episodes found")
return nil, fmt.Errorf("no episodes found")
}
ec.Episodes = lo.Map(info.EpisodesToDownload, func(episode *EntryDownloadEpisode, i int) *Episode {
return episode.Episode
})
slices.SortStableFunc(ec.Episodes, func(i, j *Episode) int {
return cmp.Compare(i.EpisodeNumber, j.EpisodeNumber)
})
event := &AnimeEpisodeCollectionEvent{
EpisodeCollection: ec,
}
err = hook.GlobalHookManager.OnAnimeEpisodeCollection().Trigger(event)
if err != nil {
return nil, err
}
ec = event.EpisodeCollection
episodeCollectionCache.SetT(opts.Media.ID, ec, time.Minute*10)
return
}
func ClearEpisodeCollectionCache() {
episodeCollectionCache.Clear()
}
/////////
type NewEpisodeCollectionFromLocalFilesOptions struct {
LocalFiles []*LocalFile
Media *anilist.BaseAnime
AnimeCollection *anilist.AnimeCollection
Platform platform.Platform
MetadataProvider metadata.Provider
Logger *zerolog.Logger
}
func NewEpisodeCollectionFromLocalFiles(ctx context.Context, opts NewEpisodeCollectionFromLocalFilesOptions) (*EpisodeCollection, error) {
if opts.Logger == nil {
opts.Logger = lo.ToPtr(zerolog.Nop())
}
if ec, ok := EpisodeCollectionFromLocalFilesCache.Get(opts.Media.GetID()); ok {
return ec, nil
}
// Make sure to keep the local files from the media only
opts.LocalFiles = lo.Filter(opts.LocalFiles, func(lf *LocalFile, i int) bool {
return lf.MediaId == opts.Media.GetID()
})
// Create a new media entry
entry, err := NewEntry(ctx, &NewEntryOptions{
MediaId: opts.Media.GetID(),
LocalFiles: opts.LocalFiles,
AnimeCollection: opts.AnimeCollection,
Platform: opts.Platform,
MetadataProvider: opts.MetadataProvider,
})
if err != nil {
return nil, fmt.Errorf("cannot play local file, could not create entry: %w", err)
}
// Should be cached if it exists
animeMetadata, err := opts.MetadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, opts.Media.ID)
if err != nil {
animeMetadata = &metadata.AnimeMetadata{
Titles: make(map[string]string),
Episodes: make(map[string]*metadata.EpisodeMetadata),
EpisodeCount: 0,
SpecialCount: 0,
Mappings: &metadata.AnimeMappings{
AnilistId: opts.Media.GetID(),
},
}
animeMetadata.Titles["en"] = opts.Media.GetTitleSafe()
animeMetadata.Titles["x-jat"] = opts.Media.GetRomajiTitleSafe()
err = nil
}
ec := &EpisodeCollection{
HasMappingError: false,
Episodes: entry.Episodes,
Metadata: animeMetadata,
}
EpisodeCollectionFromLocalFilesCache.SetT(opts.Media.GetID(), ec, time.Hour*6)
return ec, nil
}
/////////
func (ec *EpisodeCollection) FindEpisodeByNumber(episodeNumber int) (*Episode, bool) {
for _, episode := range ec.Episodes {
if episode.EpisodeNumber == episodeNumber {
return episode, true
}
}
return nil, false
}
func (ec *EpisodeCollection) FindEpisodeByAniDB(anidbEpisode string) (*Episode, bool) {
for _, episode := range ec.Episodes {
if episode.AniDBEpisode == anidbEpisode {
return episode, true
}
}
return nil, false
}
// GetMainLocalFiles returns the *main* local files.
func (ec *EpisodeCollection) GetMainLocalFiles() ([]*Episode, bool) {
ret := make([]*Episode, 0)
for _, episode := range ec.Episodes {
if episode.LocalFile == nil || episode.LocalFile.IsMain() {
ret = append(ret, episode)
}
}
if len(ret) == 0 {
return nil, false
}
return ret, true
}
// FindNextEpisode returns the *main* local file whose episode number is after the given local file.
func (ec *EpisodeCollection) FindNextEpisode(current *Episode) (*Episode, bool) {
episodes, ok := ec.GetMainLocalFiles()
if !ok {
return nil, false
}
// Get the local file whose episode number is after the given local file
var next *Episode
for _, e := range episodes {
if e.GetEpisodeNumber() == current.GetEpisodeNumber()+1 {
next = e
break
}
}
if next == nil {
return nil, false
}
return next, true
}