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,293 @@
package torrent_analyzer
import (
"errors"
"path/filepath"
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/library/anime"
"seanime/internal/library/scanner"
"seanime/internal/platforms/platform"
"seanime/internal/util"
"seanime/internal/util/limiter"
"github.com/rs/zerolog"
lop "github.com/samber/lo/parallel"
)
type (
// Analyzer is a service similar to the scanner, but it is used to analyze torrent files.
// i.e. torrent files instead of local files.
Analyzer struct {
files []*File
media *anilist.CompleteAnime
platform platform.Platform
logger *zerolog.Logger
metadataProvider metadata.Provider
forceMatch bool
}
// Analysis contains the results of the analysis.
Analysis struct {
files []*File // Hydrated after scanFiles is called
selectedFiles []*File // Hydrated after findCorrespondingFiles is called
media *anilist.CompleteAnime
}
// File represents a torrent file and contains its metadata.
File struct {
index int
path string
localFile *anime.LocalFile
}
)
type (
NewAnalyzerOptions struct {
Logger *zerolog.Logger
Filepaths []string // Filepath of the torrent files
Media *anilist.CompleteAnime // The media to compare the files with
Platform platform.Platform
MetadataProvider metadata.Provider
// This basically skips the matching process and forces the media ID to be set.
// Used for the auto-select feature because the media is already known.
ForceMatch bool
}
)
func NewAnalyzer(opts *NewAnalyzerOptions) *Analyzer {
files := lop.Map(opts.Filepaths, func(filepath string, idx int) *File {
return newFile(idx, filepath)
})
return &Analyzer{
files: files,
media: opts.Media,
platform: opts.Platform,
logger: opts.Logger,
metadataProvider: opts.MetadataProvider,
forceMatch: opts.ForceMatch,
}
}
// AnalyzeTorrentFiles scans the files and returns an Analysis struct containing methods to get the results.
func (a *Analyzer) AnalyzeTorrentFiles() (*Analysis, error) {
if a.platform == nil {
return nil, errors.New("anilist client wrapper is nil")
}
if err := a.scanFiles(); err != nil {
return nil, err
}
analysis := &Analysis{
files: a.files,
media: a.media,
}
return analysis, nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (a *Analysis) GetCorrespondingFiles() map[int]*File {
ret, _ := a.getCorrespondingFiles(func(f *File) bool {
return true
})
return ret
}
func (a *Analysis) GetCorrespondingMainFiles() map[int]*File {
ret, _ := a.getCorrespondingFiles(func(f *File) bool {
return f.localFile.IsMain()
})
return ret
}
func (a *Analysis) GetMainFileByEpisode(episodeNumber int) (*File, bool) {
ret, _ := a.getCorrespondingFiles(func(f *File) bool {
return f.localFile.IsMain()
})
for _, f := range ret {
if f.localFile.Metadata.Episode == episodeNumber {
return f, true
}
}
return nil, false
}
func (a *Analysis) GetFileByAniDBEpisode(episode string) (*File, bool) {
for _, f := range a.files {
if f.localFile.Metadata.AniDBEpisode == episode {
return f, true
}
}
return nil, false
}
func (a *Analysis) GetUnselectedFiles() map[int]*File {
_, uRet := a.getCorrespondingFiles(func(f *File) bool {
return true
})
return uRet
}
func (a *Analysis) getCorrespondingFiles(filter func(f *File) bool) (map[int]*File, map[int]*File) {
ret := make(map[int]*File)
uRet := make(map[int]*File)
for _, af := range a.files {
if af.localFile.MediaId == a.media.ID {
if filter(af) {
ret[af.index] = af
} else {
uRet[af.index] = af
}
} else {
uRet[af.index] = af
}
}
return ret, uRet
}
// GetIndices returns the indices of the files.
//
// Example:
//
// selectedFilesMap := analysis.GetCorrespondingMainFiles()
// selectedIndices := analysis.GetIndices(selectedFilesMap)
func (a *Analysis) GetIndices(files map[int]*File) []int {
indices := make([]int, 0)
for i := range files {
indices = append(indices, i)
}
return indices
}
func (a *Analysis) GetFiles() []*File {
return a.files
}
// GetUnselectedIndices takes a map of selected files and returns the indices of the unselected files.
//
// Example:
//
// analysis, _ := analyzer.AnalyzeTorrentFiles()
// selectedFiles := analysis.GetCorrespondingMainFiles()
// indicesToRemove := analysis.GetUnselectedIndices(selectedFiles)
func (a *Analysis) GetUnselectedIndices(files map[int]*File) []int {
indices := make([]int, 0)
for i := range a.files {
if _, ok := files[i]; !ok {
indices = append(indices, i)
}
}
return indices
}
func (f *File) GetLocalFile() *anime.LocalFile {
return f.localFile
}
func (f *File) GetIndex() int {
return f.index
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// scanFiles scans the files and matches them with the media.
func (a *Analyzer) scanFiles() error {
completeAnimeCache := anilist.NewCompleteAnimeCache()
anilistRateLimiter := limiter.NewAnilistLimiter()
lfs := a.getLocalFiles() // Extract local files from the Files
// +---------------------+
// | MediaContainer |
// +---------------------+
tree := anilist.NewCompleteAnimeRelationTree()
if err := a.media.FetchMediaTree(anilist.FetchMediaTreeAll, a.platform.GetAnilistClient(), anilistRateLimiter, tree, completeAnimeCache); err != nil {
return err
}
allMedia := tree.Values()
mc := scanner.NewMediaContainer(&scanner.MediaContainerOptions{
AllMedia: allMedia,
})
//scanLogger, _ := scanner.NewScanLogger("./logs")
// +---------------------+
// | Matcher |
// +---------------------+
matcher := &scanner.Matcher{
LocalFiles: lfs,
MediaContainer: mc,
CompleteAnimeCache: completeAnimeCache,
Logger: util.NewLogger(),
ScanLogger: nil,
ScanSummaryLogger: nil,
}
err := matcher.MatchLocalFilesWithMedia()
if err != nil {
return err
}
if a.forceMatch {
for _, lf := range lfs {
lf.MediaId = a.media.GetID()
}
}
// +---------------------+
// | FileHydrator |
// +---------------------+
fh := &scanner.FileHydrator{
LocalFiles: lfs,
AllMedia: mc.NormalizedMedia,
CompleteAnimeCache: completeAnimeCache,
Platform: a.platform,
MetadataProvider: a.metadataProvider,
AnilistRateLimiter: anilistRateLimiter,
Logger: a.logger,
ScanLogger: nil,
ScanSummaryLogger: nil,
ForceMediaId: map[bool]int{true: a.media.GetID(), false: 0}[a.forceMatch],
}
fh.HydrateMetadata()
for _, af := range a.files {
for _, lf := range lfs {
if lf.Path == af.localFile.Path {
af.localFile = lf // Update the local file in the File
break
}
}
}
return nil
}
// newFile creates a new File from a file path.
func newFile(idx int, path string) *File {
path = filepath.ToSlash(path)
return &File{
index: idx,
path: path,
localFile: anime.NewLocalFile(path, ""),
}
}
func (a *Analyzer) getLocalFiles() []*anime.LocalFile {
files := make([]*anime.LocalFile, len(a.files))
for i, f := range a.files {
files[i] = f.localFile
}
return files
}

View File

@@ -0,0 +1,110 @@
package torrent_analyzer
import (
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/platforms/anilist_platform"
"seanime/internal/test_utils"
"seanime/internal/util"
"testing"
"github.com/stretchr/testify/assert"
)
// TestSelectFilesFromSeason tests the selection of the accurate season files from a list of files from all seasons.
func TestSelectFilesFromSeason(t *testing.T) {
test_utils.InitTestProvider(t, test_utils.Anilist())
logger := util.NewLogger()
anilistClient := anilist.TestGetMockAnilistClient()
anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger)
metadataProvider := metadata.GetMockProvider(t)
tests := []struct {
name string
mediaId int // The media ID of the season
filepaths []string // All filepaths from all seasons
expectedIndices []int // The indices of the selected files
}{
{
name: "Kakegurui xx",
filepaths: []string{
"Kakegurui [BD][1080p][HEVC 10bit x265][Dual Audio][Tenrai-Sensei]/Season 1/Kakegurui - S01E01 - The Woman Called Yumeko Jabami.mkv", // should be selected
"Kakegurui [BD][1080p][HEVC 10bit x265][Dual Audio][Tenrai-Sensei]/Season 2/Kakegurui xx - S02E01 - The Woman Called Yumeko Jabami.mkv",
},
mediaId: 98314,
expectedIndices: []int{0},
},
{
name: "Kimi ni Todoke Season 2",
filepaths: []string{
"[Judas] Kimi ni Todoke (Seasons 1-2) [BD 1080p][HEVC x265 10bit][Eng-Subs]/[Judas] Kimi ni Todoke S1/[Judas] Kimi ni Todoke - S01E01.mkv",
"[Judas] Kimi ni Todoke (Seasons 1-2) [BD 1080p][HEVC x265 10bit][Eng-Subs]/[Judas] Kimi ni Todoke S1/[Judas] Kimi ni Todoke - S01E02.mkv",
"[Judas] Kimi ni Todoke (Seasons 1-2) [BD 1080p][HEVC x265 10bit][Eng-Subs]/[Judas] Kimi ni Todoke S2/[Judas] Kimi ni Todoke - S02E01.mkv", // should be selected
"[Judas] Kimi ni Todoke (Seasons 1-2) [BD 1080p][HEVC x265 10bit][Eng-Subs]/[Judas] Kimi ni Todoke S2/[Judas] Kimi ni Todoke - S02E02.mkv", // should be selected
},
mediaId: 9656,
expectedIndices: []int{2, 3},
},
{
name: "Spy x Family Part 2",
filepaths: []string{
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 10v2 (1080p) [F9F5C62B].mkv",
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 11v2 (1080p) [F9F5C62B].mkv",
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 12v2 (1080p) [F9F5C62B].mkv",
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 13v2 (1080p) [F9F5C62B].mkv", // should be selected
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 14v2 (1080p) [F9F5C62B].mkv", // should be selected
"[SubsPlease] Spy x Family (01-25) (1080p) [Batch]/[SubsPlease] Spy x Family - 15v2 (1080p) [F9F5C62B].mkv", // should be selected
},
mediaId: 142838,
expectedIndices: []int{3, 4, 5},
},
{
name: "Mushoku Tensei: Jobless Reincarnation Season 2 Part 2",
filepaths: []string{
"[EMBER] Mushoku Tensei S2 - 13.mkv", // should be selected
"[EMBER] Mushoku Tensei S2 - 14.mkv", // should be selected
"[EMBER] Mushoku Tensei S2 - 15.mkv", // should be selected
"[EMBER] Mushoku Tensei S2 - 16.mkv", // should be selected
},
mediaId: 166873,
expectedIndices: []int{0, 1, 2, 3},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Get media
media, err := anilistPlatform.GetAnimeWithRelations(t.Context(), tt.mediaId)
if err != nil {
t.Fatal("expected result, got error:", err.Error())
}
analyzer := NewAnalyzer(&NewAnalyzerOptions{
Logger: logger,
Filepaths: tt.filepaths,
Media: media,
Platform: anilistPlatform,
MetadataProvider: metadataProvider,
ForceMatch: false,
})
// AnalyzeTorrentFiles
analysis, err := analyzer.AnalyzeTorrentFiles()
if assert.NoError(t, err) {
selectedFilesMap := analysis.GetCorrespondingMainFiles()
selectedIndices := analysis.GetIndices(selectedFilesMap)
// Check selected files
assert.ElementsMatch(t, tt.expectedIndices, selectedIndices)
}
})
}
}