node build fixed
This commit is contained in:
293
seanime-2.9.10/internal/torrents/analyzer/analyzer.go
Normal file
293
seanime-2.9.10/internal/torrents/analyzer/analyzer.go
Normal 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
|
||||
}
|
||||
110
seanime-2.9.10/internal/torrents/analyzer/analyzer_test.go
Normal file
110
seanime-2.9.10/internal/torrents/analyzer/analyzer_test.go
Normal 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)
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user