351 lines
9.8 KiB
Go
351 lines
9.8 KiB
Go
package anime
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/api/metadata"
|
|
"seanime/internal/hook"
|
|
"strconv"
|
|
|
|
"github.com/samber/lo"
|
|
"github.com/sourcegraph/conc/pool"
|
|
)
|
|
|
|
type (
|
|
// EntryDownloadInfo is instantiated by the Entry
|
|
EntryDownloadInfo struct {
|
|
EpisodesToDownload []*EntryDownloadEpisode `json:"episodesToDownload"`
|
|
CanBatch bool `json:"canBatch"`
|
|
BatchAll bool `json:"batchAll"`
|
|
HasInaccurateSchedule bool `json:"hasInaccurateSchedule"`
|
|
Rewatch bool `json:"rewatch"`
|
|
AbsoluteOffset int `json:"absoluteOffset"`
|
|
}
|
|
|
|
EntryDownloadEpisode struct {
|
|
EpisodeNumber int `json:"episodeNumber"`
|
|
AniDBEpisode string `json:"aniDBEpisode"`
|
|
Episode *Episode `json:"episode"`
|
|
}
|
|
)
|
|
|
|
type (
|
|
NewEntryDownloadInfoOptions struct {
|
|
// Media's local files
|
|
LocalFiles []*LocalFile
|
|
AnimeMetadata *metadata.AnimeMetadata
|
|
Media *anilist.BaseAnime
|
|
Progress *int
|
|
Status *anilist.MediaListStatus
|
|
MetadataProvider metadata.Provider
|
|
}
|
|
)
|
|
|
|
// NewEntryDownloadInfo returns a list of episodes to download or episodes for the torrent/debrid streaming views
|
|
// based on the options provided.
|
|
func NewEntryDownloadInfo(opts *NewEntryDownloadInfoOptions) (*EntryDownloadInfo, error) {
|
|
|
|
reqEvent := &AnimeEntryDownloadInfoRequestedEvent{
|
|
LocalFiles: opts.LocalFiles,
|
|
AnimeMetadata: opts.AnimeMetadata,
|
|
Media: opts.Media,
|
|
Progress: opts.Progress,
|
|
Status: opts.Status,
|
|
EntryDownloadInfo: &EntryDownloadInfo{},
|
|
}
|
|
|
|
err := hook.GlobalHookManager.OnAnimeEntryDownloadInfoRequested().Trigger(reqEvent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if reqEvent.DefaultPrevented {
|
|
return reqEvent.EntryDownloadInfo, nil
|
|
}
|
|
|
|
opts.LocalFiles = reqEvent.LocalFiles
|
|
opts.AnimeMetadata = reqEvent.AnimeMetadata
|
|
opts.Media = reqEvent.Media
|
|
opts.Progress = reqEvent.Progress
|
|
opts.Status = reqEvent.Status
|
|
|
|
if *opts.Media.Status == anilist.MediaStatusNotYetReleased {
|
|
return &EntryDownloadInfo{}, nil
|
|
}
|
|
if opts.AnimeMetadata == nil {
|
|
return nil, errors.New("could not get anime metadata")
|
|
}
|
|
currentEpisodeCount := opts.Media.GetCurrentEpisodeCount()
|
|
if currentEpisodeCount == -1 && opts.AnimeMetadata != nil {
|
|
currentEpisodeCount = opts.AnimeMetadata.GetCurrentEpisodeCount()
|
|
}
|
|
if currentEpisodeCount == -1 {
|
|
return nil, errors.New("could not get current media episode count")
|
|
}
|
|
|
|
// +---------------------+
|
|
// | Discrepancy |
|
|
// +---------------------+
|
|
|
|
// Whether AniList includes episode 0 as part of main episodes, but AniDB does not, however AniDB has "S1"
|
|
discrepancy := FindDiscrepancy(opts.Media, opts.AnimeMetadata)
|
|
|
|
// AniList is the source of truth for episode numbers
|
|
epSlice := newEpisodeSlice(currentEpisodeCount)
|
|
|
|
// Handle discrepancies
|
|
if discrepancy != DiscrepancyNone {
|
|
|
|
// If AniList includes episode 0 as part of main episodes, but AniDB does not, however AniDB has "S1"
|
|
if discrepancy == DiscrepancyAniListCountsEpisodeZero {
|
|
// Add "S1" to the beginning of the episode slice
|
|
epSlice.trimEnd(1)
|
|
epSlice.prepend(0, "S1")
|
|
}
|
|
|
|
// If AniList includes specials, but AniDB does not
|
|
if discrepancy == DiscrepancyAniListCountsSpecials {
|
|
diff := currentEpisodeCount - opts.AnimeMetadata.GetMainEpisodeCount()
|
|
epSlice.trimEnd(diff)
|
|
for i := 0; i < diff; i++ {
|
|
epSlice.add(currentEpisodeCount-i, "S"+strconv.Itoa(i+1))
|
|
}
|
|
}
|
|
|
|
// If AniDB has more episodes than AniList
|
|
if discrepancy == DiscrepancyAniDBHasMore {
|
|
// Do nothing
|
|
}
|
|
|
|
}
|
|
|
|
// Filter out episodes not aired
|
|
if opts.Media.NextAiringEpisode != nil {
|
|
epSlice.filter(func(item *episodeSliceItem, index int) bool {
|
|
// e.g. if the next airing episode is 13, then filter out episodes 14 and above
|
|
return index+1 < opts.Media.NextAiringEpisode.Episode
|
|
})
|
|
}
|
|
|
|
// Get progress, if the media isn't in the user's list, progress is 0
|
|
// If the media is completed, set progress is 0
|
|
progress := 0
|
|
if opts.Progress != nil {
|
|
progress = *opts.Progress
|
|
}
|
|
if opts.Status != nil {
|
|
if *opts.Status == anilist.MediaListStatusCompleted {
|
|
progress = 0
|
|
}
|
|
}
|
|
|
|
hasInaccurateSchedule := false
|
|
if opts.Media.NextAiringEpisode == nil && *opts.Media.Status == anilist.MediaStatusReleasing {
|
|
hasInaccurateSchedule = true
|
|
}
|
|
|
|
// Filter out episodes already watched (index+1 is the progress number)
|
|
toDownloadSlice := epSlice.filterNew(func(item *episodeSliceItem, index int) bool {
|
|
return index+1 > progress
|
|
})
|
|
|
|
// This slice contains episode numbers that are not downloaded
|
|
// The source of truth is AniDB, but we will handle discrepancies
|
|
lfsEpSlice := newEpisodeSlice(0)
|
|
if opts.LocalFiles != nil {
|
|
// Get all episode numbers of main local files
|
|
for _, lf := range opts.LocalFiles {
|
|
if lf.Metadata.Type == LocalFileTypeMain {
|
|
lfsEpSlice.add(lf.Metadata.Episode, lf.Metadata.AniDBEpisode)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter out downloaded episodes
|
|
toDownloadSlice.filter(func(item *episodeSliceItem, index int) bool {
|
|
isDownloaded := false
|
|
for _, lf := range opts.LocalFiles {
|
|
if lf.Metadata.Type != LocalFileTypeMain {
|
|
continue
|
|
}
|
|
// If the file episode number matches that of the episode slice item
|
|
if lf.Metadata.Episode == item.episodeNumber {
|
|
isDownloaded = true
|
|
}
|
|
// If the slice episode number is 0 and the file is a main S1
|
|
if discrepancy == DiscrepancyAniListCountsEpisodeZero && item.episodeNumber == 0 && lf.Metadata.AniDBEpisode == "S1" {
|
|
isDownloaded = true
|
|
}
|
|
}
|
|
|
|
return !isDownloaded
|
|
})
|
|
|
|
// +---------------------+
|
|
// | EntryEpisode |
|
|
// +---------------------+
|
|
|
|
// Generate `episodesToDownload` based on `toDownloadSlice`
|
|
|
|
// DEVNOTE: The EntryEpisode generated has inaccurate progress numbers since not local files are passed in
|
|
|
|
progressOffset := 0
|
|
if discrepancy == DiscrepancyAniListCountsEpisodeZero {
|
|
progressOffset = 1
|
|
}
|
|
|
|
p := pool.NewWithResults[*EntryDownloadEpisode]()
|
|
for _, ep := range toDownloadSlice.getSlice() {
|
|
p.Go(func() *EntryDownloadEpisode {
|
|
str := new(EntryDownloadEpisode)
|
|
str.EpisodeNumber = ep.episodeNumber
|
|
str.AniDBEpisode = ep.aniDBEpisode
|
|
// Create a new episode with a placeholder local file
|
|
// We pass that placeholder local file so that all episodes are hydrated as main episodes for consistency
|
|
str.Episode = NewEpisode(&NewEpisodeOptions{
|
|
LocalFile: &LocalFile{
|
|
ParsedData: &LocalFileParsedData{},
|
|
ParsedFolderData: []*LocalFileParsedData{},
|
|
Metadata: &LocalFileMetadata{
|
|
Episode: ep.episodeNumber,
|
|
Type: LocalFileTypeMain,
|
|
AniDBEpisode: ep.aniDBEpisode,
|
|
},
|
|
},
|
|
OptionalAniDBEpisode: str.AniDBEpisode,
|
|
AnimeMetadata: opts.AnimeMetadata,
|
|
Media: opts.Media,
|
|
ProgressOffset: progressOffset,
|
|
IsDownloaded: false,
|
|
MetadataProvider: opts.MetadataProvider,
|
|
})
|
|
str.Episode.AniDBEpisode = ep.aniDBEpisode
|
|
// Reset the local file to nil, since it's a placeholder
|
|
str.Episode.LocalFile = nil
|
|
return str
|
|
})
|
|
}
|
|
episodesToDownload := p.Wait()
|
|
|
|
//--------------
|
|
|
|
canBatch := false
|
|
if *opts.Media.GetStatus() == anilist.MediaStatusFinished && opts.Media.GetTotalEpisodeCount() > 0 {
|
|
canBatch = true
|
|
}
|
|
batchAll := false
|
|
if canBatch && lfsEpSlice.len() == 0 && progress == 0 {
|
|
batchAll = true
|
|
}
|
|
rewatch := false
|
|
if opts.Status != nil && *opts.Status == anilist.MediaListStatusCompleted {
|
|
rewatch = true
|
|
}
|
|
|
|
downloadInfo := &EntryDownloadInfo{
|
|
EpisodesToDownload: episodesToDownload,
|
|
CanBatch: canBatch,
|
|
BatchAll: batchAll,
|
|
Rewatch: rewatch,
|
|
HasInaccurateSchedule: hasInaccurateSchedule,
|
|
AbsoluteOffset: opts.AnimeMetadata.GetOffset(),
|
|
}
|
|
|
|
event := &AnimeEntryDownloadInfoEvent{
|
|
EntryDownloadInfo: downloadInfo,
|
|
}
|
|
err = hook.GlobalHookManager.OnAnimeEntryDownloadInfo().Trigger(event)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return event.EntryDownloadInfo, nil
|
|
}
|
|
|
|
type episodeSliceItem struct {
|
|
episodeNumber int
|
|
aniDBEpisode string
|
|
}
|
|
|
|
type episodeSlice []*episodeSliceItem
|
|
|
|
func newEpisodeSlice(episodeCount int) *episodeSlice {
|
|
s := make([]*episodeSliceItem, 0)
|
|
for i := 0; i < episodeCount; i++ {
|
|
s = append(s, &episodeSliceItem{episodeNumber: i + 1, aniDBEpisode: strconv.Itoa(i + 1)})
|
|
}
|
|
ret := &episodeSlice{}
|
|
ret.set(s)
|
|
return ret
|
|
}
|
|
|
|
func (s *episodeSlice) set(eps []*episodeSliceItem) {
|
|
*s = eps
|
|
}
|
|
|
|
func (s *episodeSlice) add(episodeNumber int, aniDBEpisode string) {
|
|
*s = append(*s, &episodeSliceItem{episodeNumber: episodeNumber, aniDBEpisode: aniDBEpisode})
|
|
}
|
|
|
|
func (s *episodeSlice) prepend(episodeNumber int, aniDBEpisode string) {
|
|
*s = append([]*episodeSliceItem{{episodeNumber: episodeNumber, aniDBEpisode: aniDBEpisode}}, *s...)
|
|
}
|
|
|
|
func (s *episodeSlice) trimEnd(n int) {
|
|
*s = (*s)[:len(*s)-n]
|
|
}
|
|
|
|
func (s *episodeSlice) trimStart(n int) {
|
|
*s = (*s)[n:]
|
|
}
|
|
|
|
func (s *episodeSlice) len() int {
|
|
return len(*s)
|
|
}
|
|
|
|
func (s *episodeSlice) get(index int) *episodeSliceItem {
|
|
return (*s)[index]
|
|
}
|
|
|
|
func (s *episodeSlice) getEpisodeNumber(episodeNumber int) *episodeSliceItem {
|
|
for _, item := range *s {
|
|
if item.episodeNumber == episodeNumber {
|
|
return item
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *episodeSlice) filter(filter func(*episodeSliceItem, int) bool) {
|
|
*s = lo.Filter(*s, filter)
|
|
}
|
|
|
|
func (s *episodeSlice) filterNew(filter func(*episodeSliceItem, int) bool) *episodeSlice {
|
|
s2 := make(episodeSlice, 0)
|
|
for i, item := range *s {
|
|
if filter(item, i) {
|
|
s2 = append(s2, item)
|
|
}
|
|
}
|
|
return &s2
|
|
}
|
|
|
|
func (s *episodeSlice) copy() *episodeSlice {
|
|
s2 := make(episodeSlice, len(*s), cap(*s))
|
|
for i, item := range *s {
|
|
s2[i] = item
|
|
}
|
|
return &s2
|
|
}
|
|
|
|
func (s *episodeSlice) getSlice() []*episodeSliceItem {
|
|
return *s
|
|
}
|
|
|
|
func (s *episodeSlice) print() {
|
|
for i, item := range *s {
|
|
fmt.Printf("(%d) %d -> %s\n", i, item.episodeNumber, item.aniDBEpisode)
|
|
}
|
|
}
|