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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
package autodownloader
import (
"errors"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"seanime/internal/library/anime"
"sync"
"github.com/5rahim/habari"
"github.com/samber/lo"
)
type (
// NormalizedTorrent is a struct built from torrent from a provider.
// It is used to normalize the data from different providers so that it can be used by the AutoDownloader.
NormalizedTorrent struct {
hibiketorrent.AnimeTorrent
ParsedData *habari.Metadata `json:"parsedData"`
magnet string // Access using GetMagnet()
}
)
func (ad *AutoDownloader) getLatestTorrents(rules []*anime.AutoDownloaderRule) (ret []*NormalizedTorrent, err error) {
ad.logger.Debug().Msg("autodownloader: Checking for new episodes")
providerExtension, ok := ad.torrentRepository.GetDefaultAnimeProviderExtension()
if !ok {
ad.logger.Warn().Msg("autodownloader: No default torrent provider found")
return nil, errors.New("no default torrent provider found")
}
// Get the latest torrents
torrents, err := providerExtension.GetProvider().GetLatest()
if err != nil {
ad.logger.Error().Err(err).Msg("autodownloader: Failed to get latest torrents")
return nil, err
}
if ad.settings.EnableEnhancedQueries {
// Get unique release groups
uniqueReleaseGroups := GetUniqueReleaseGroups(rules)
// Filter the torrents
wg := sync.WaitGroup{}
mu := sync.Mutex{}
wg.Add(len(uniqueReleaseGroups))
for _, releaseGroup := range uniqueReleaseGroups {
go func(releaseGroup string) {
defer wg.Done()
filteredTorrents, err := providerExtension.GetProvider().Search(hibiketorrent.AnimeSearchOptions{
Media: hibiketorrent.Media{},
Query: releaseGroup,
})
if err != nil {
return
}
mu.Lock()
torrents = append(torrents, filteredTorrents...)
mu.Unlock()
}(releaseGroup)
}
wg.Wait()
// Remove duplicates
torrents = lo.UniqBy(torrents, func(t *hibiketorrent.AnimeTorrent) string {
return t.Name
})
}
// Normalize the torrents
ret = make([]*NormalizedTorrent, 0, len(torrents))
for _, t := range torrents {
parsedData := habari.Parse(t.Name)
ret = append(ret, &NormalizedTorrent{
AnimeTorrent: *t,
ParsedData: parsedData,
})
}
return ret, nil
}
// GetMagnet returns the magnet link for the torrent.
func (t *NormalizedTorrent) GetMagnet(providerExtension hibiketorrent.AnimeProvider) (string, error) {
if t.magnet == "" {
magnet, err := providerExtension.GetTorrentMagnetLink(&t.AnimeTorrent)
if err != nil {
return "", err
}
t.magnet = magnet
return t.magnet, nil
}
return t.magnet, nil
}

View File

@@ -0,0 +1,327 @@
package autodownloader
import (
"github.com/5rahim/habari"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/database/models"
"seanime/internal/library/anime"
"testing"
)
func TestComparison(t *testing.T) {
ad := AutoDownloader{
metadataProvider: metadata.GetMockProvider(t),
settings: &models.AutoDownloaderSettings{
EnableSeasonCheck: true,
},
}
name1 := "[Oshi no Ko] 2nd Season"
name2 := "Oshi no Ko Season 2"
aniListEntry := &anilist.AnimeListEntry{
Media: &anilist.BaseAnime{
ID: 166531,
Title: &anilist.BaseAnime_Title{
Romaji: &name1,
English: &name2,
},
Episodes: lo.ToPtr(13),
Format: lo.ToPtr(anilist.MediaFormatTv),
},
}
rule := &anime.AutoDownloaderRule{
MediaId: 166531,
ReleaseGroups: []string{"SubsPlease", "Erai-raws"},
Resolutions: []string{"1080p"},
TitleComparisonType: "likely",
EpisodeType: "recent",
EpisodeNumbers: []int{3}, // ignored
Destination: "/data/seanime/library/[Oshi no Ko] 2nd Season",
ComparisonTitle: "[Oshi no Ko] 2nd Season",
}
tests := []struct {
torrentName string
succeedTitleComparison bool
succeedSeasonAndEpisodeMatch bool
enableSeasonCheck bool
}{
{
torrentName: "[Erai-raws] Oshi no Ko 2nd Season - 03 [720p][Multiple Subtitle] [ENG][FRE]",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: true,
enableSeasonCheck: true,
},
{
torrentName: "[SubsPlease] Oshi no Ko - 16 (1080p)",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: true,
enableSeasonCheck: true,
},
{
torrentName: "[Erai-raws] Oshi no Ko 3rd Season - 03 [720p][Multiple Subtitle] [ENG][FRE]",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: false,
enableSeasonCheck: true,
},
{
torrentName: "[Erai-raws] Oshi no Ko 2nd Season - 03 [720p][Multiple Subtitle] [ENG][FRE]",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: true,
enableSeasonCheck: false,
},
{
torrentName: "[SubsPlease] Oshi no Ko - 16 (1080p)",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: true,
enableSeasonCheck: false,
},
{
torrentName: "[Erai-raws] Oshi no Ko 3rd Season - 03 [720p][Multiple Subtitle] [ENG][FRE]",
succeedTitleComparison: true,
succeedSeasonAndEpisodeMatch: true,
enableSeasonCheck: false,
},
}
lfw := anime.NewLocalFileWrapper([]*anime.LocalFile{
{
Path: "/data/seanime/library/[Oshi no Ko] 2nd Season/[SubsPlease] Oshi no Ko - 12 (1080p).mkv",
Name: "Oshi no Ko - 12 (1080p).mkv",
ParsedData: &anime.LocalFileParsedData{
Original: "Oshi no Ko - 12 (1080p).mkv",
Title: "Oshi no Ko",
ReleaseGroup: "SubsPlease",
},
ParsedFolderData: []*anime.LocalFileParsedData{
{
Original: "[Oshi no Ko] 2nd Season",
Title: "[Oshi no Ko]",
},
},
Metadata: &anime.LocalFileMetadata{
Episode: 1,
AniDBEpisode: "1",
Type: "main",
},
MediaId: 166531,
},
})
for _, tt := range tests {
t.Run(tt.torrentName, func(t *testing.T) {
ad.settings.EnableSeasonCheck = tt.enableSeasonCheck
p := habari.Parse(tt.torrentName)
if tt.succeedTitleComparison {
require.True(t, ad.isTitleMatch(p, tt.torrentName, rule, aniListEntry))
} else {
require.False(t, ad.isTitleMatch(p, tt.torrentName, rule, aniListEntry))
}
lfwe, ok := lfw.GetLocalEntryById(166531)
require.True(t, ok)
_, ok = ad.isSeasonAndEpisodeMatch(p, rule, aniListEntry, lfwe, []*models.AutoDownloaderItem{})
if tt.succeedSeasonAndEpisodeMatch {
require.True(t, ok)
} else {
require.False(t, ok)
}
})
}
}
func TestComparison2(t *testing.T) {
ad := AutoDownloader{
metadataProvider: metadata.GetMockProvider(t),
settings: &models.AutoDownloaderSettings{
EnableSeasonCheck: true,
},
}
name1 := "DANDADAN"
name2 := "Dandadan"
aniListEntry := &anilist.AnimeListEntry{
Media: &anilist.BaseAnime{
Title: &anilist.BaseAnime_Title{
Romaji: &name1,
English: &name2,
},
Episodes: lo.ToPtr(12),
Status: lo.ToPtr(anilist.MediaStatusFinished),
Format: lo.ToPtr(anilist.MediaFormatTv),
},
}
rule := &anime.AutoDownloaderRule{
MediaId: 166531,
ReleaseGroups: []string{},
Resolutions: []string{"1080p"},
TitleComparisonType: "likely",
EpisodeType: "recent",
EpisodeNumbers: []int{},
Destination: "/data/seanime/library/Dandadan",
ComparisonTitle: "Dandadan",
}
tests := []struct {
torrentName string
succeedAdditionalTermsMatch bool
ruleAdditionalTerms []string
}{
{
torrentName: "[Anime Time] Dandadan - 04 [Dual Audio][1080p][HEVC 10bit x265][AAC][Multi Sub] [Weekly]",
ruleAdditionalTerms: []string{},
succeedAdditionalTermsMatch: true,
},
{
torrentName: "[Anime Time] Dandadan - 04 [Dual Audio][1080p][HEVC 10bit x265][AAC][Multi Sub] [Weekly]",
ruleAdditionalTerms: []string{
"H265,H.265, H 265,x265",
"10bit,10-bit,10 bit",
},
succeedAdditionalTermsMatch: true,
},
{
torrentName: "[Raze] Dandadan - 04 x265 10bit 1080p 143.8561fps.mkv",
ruleAdditionalTerms: []string{
"H265,H.265, H 265,x265",
"10bit,10-bit,10 bit",
},
succeedAdditionalTermsMatch: true,
},
//{ // DEVNOTE: Doesn't pass because of title
// torrentName: "[Sokudo] DAN DA DAN | Dandadan - S01E03 [1080p EAC-3 AV1][Dual Audio] (weekly)",
// ruleAdditionalTerms: []string{
// "H265,H.265, H 265,x265",
// "10bit,10-bit,10 bit",
// },
// succeedAdditionalTermsMatch: false,
//},
{
torrentName: "[Raze] Dandadan - 04 x265 10bit 1080p 143.8561fps.mkv",
ruleAdditionalTerms: []string{
"H265,H.265, H 265,x265",
"10bit,10-bit,10 bit",
"AAC",
},
succeedAdditionalTermsMatch: false,
},
}
for _, tt := range tests {
t.Run(tt.torrentName, func(t *testing.T) {
rule.AdditionalTerms = tt.ruleAdditionalTerms
ok := ad.isTitleMatch(habari.Parse(tt.torrentName), tt.torrentName, rule, aniListEntry)
assert.True(t, ok)
ok = ad.isAdditionalTermsMatch(tt.torrentName, rule)
if tt.succeedAdditionalTermsMatch {
assert.True(t, ok)
} else {
assert.False(t, ok)
}
})
}
}
func TestComparison3(t *testing.T) {
ad := AutoDownloader{
metadataProvider: metadata.GetMockProvider(t),
settings: &models.AutoDownloaderSettings{
EnableSeasonCheck: true,
},
}
name1 := "Dandadan"
name2 := "DAN DA DAN"
aniListEntry := &anilist.AnimeListEntry{
Media: &anilist.BaseAnime{
Title: &anilist.BaseAnime_Title{
Romaji: &name1,
English: &name2,
},
Status: lo.ToPtr(anilist.MediaStatusFinished),
Episodes: lo.ToPtr(12),
Format: lo.ToPtr(anilist.MediaFormatTv),
},
}
rule := &anime.AutoDownloaderRule{
MediaId: 166531,
ReleaseGroups: []string{},
Resolutions: []string{},
TitleComparisonType: "likely",
EpisodeType: "recent",
EpisodeNumbers: []int{},
Destination: "/data/seanime/library/Dandadan",
ComparisonTitle: "Dandadan",
}
tests := []struct {
torrentName string
succeedTitleComparison bool
succeedSeasonAndEpisodeMatch bool
enableSeasonCheck bool
}{
{
torrentName: "[Salieri] Zom 100 Bucket List of the Dead - S1 - BD (1080p) (HDR) [Dual Audio]",
succeedTitleComparison: false,
succeedSeasonAndEpisodeMatch: false,
enableSeasonCheck: false,
},
}
lfw := anime.NewLocalFileWrapper([]*anime.LocalFile{
{
Path: "/data/seanime/library/Dandadan/[SubsPlease] Dandadan - 01 (1080p).mkv",
Name: "Dandadan - 01 (1080p).mkv",
ParsedData: &anime.LocalFileParsedData{
Original: "Dandadan - 01 (1080p).mkv",
Title: "Dandadan",
ReleaseGroup: "SubsPlease",
},
ParsedFolderData: []*anime.LocalFileParsedData{
{
Original: "Dandadan",
Title: "Dandadan",
},
},
Metadata: &anime.LocalFileMetadata{
Episode: 1,
AniDBEpisode: "1",
Type: "main",
},
MediaId: 171018,
},
})
for _, tt := range tests {
t.Run(tt.torrentName, func(t *testing.T) {
ad.settings.EnableSeasonCheck = tt.enableSeasonCheck
p := habari.Parse(tt.torrentName)
if tt.succeedTitleComparison {
require.True(t, ad.isTitleMatch(p, tt.torrentName, rule, aniListEntry))
} else {
require.False(t, ad.isTitleMatch(p, tt.torrentName, rule, aniListEntry))
}
lfwe, ok := lfw.GetLocalEntryById(171018)
require.True(t, ok)
_, ok = ad.isSeasonAndEpisodeMatch(p, rule, aniListEntry, lfwe, []*models.AutoDownloaderItem{})
if tt.succeedSeasonAndEpisodeMatch {
assert.True(t, ok)
} else {
assert.False(t, ok)
}
})
}
}

View File

@@ -0,0 +1,21 @@
package autodownloader
import (
"seanime/internal/library/anime"
"strings"
)
func GetUniqueReleaseGroups(rules []*anime.AutoDownloaderRule) []string {
uniqueReleaseGroups := make(map[string]string)
for _, rule := range rules {
for _, releaseGroup := range rule.ReleaseGroups {
// make it case-insensitive
uniqueReleaseGroups[strings.ToLower(releaseGroup)] = releaseGroup
}
}
var result []string
for k := range uniqueReleaseGroups {
result = append(result, k)
}
return result
}

View File

@@ -0,0 +1,60 @@
package autodownloader
import (
"seanime/internal/api/anilist"
"seanime/internal/database/models"
"seanime/internal/hook_resolver"
"seanime/internal/library/anime"
)
// AutoDownloaderRunStartedEvent is triggered when the autodownloader starts checking for new episodes.
// Prevent default to abort the run.
type AutoDownloaderRunStartedEvent struct {
hook_resolver.Event
Rules []*anime.AutoDownloaderRule `json:"rules"`
}
// AutoDownloaderTorrentsFetchedEvent is triggered at the beginning of a run, when the autodownloader fetches torrents from the provider.
type AutoDownloaderTorrentsFetchedEvent struct {
hook_resolver.Event
Torrents []*NormalizedTorrent `json:"torrents"`
}
// AutoDownloaderMatchVerifiedEvent is triggered when a torrent is verified to follow a rule.
// Prevent default to abort the download if the match is found.
type AutoDownloaderMatchVerifiedEvent struct {
hook_resolver.Event
// Fetched torrent
Torrent *NormalizedTorrent `json:"torrent"`
Rule *anime.AutoDownloaderRule `json:"rule"`
ListEntry *anilist.AnimeListEntry `json:"listEntry"`
LocalEntry *anime.LocalFileWrapperEntry `json:"localEntry"`
// The episode number found for the match
// If the match failed, this will be 0
Episode int `json:"episode"`
// Whether the torrent matches the rule
// Changing this value to true will trigger a download even if the match failed;
MatchFound bool `json:"matchFound"`
}
// AutoDownloaderSettingsUpdatedEvent is triggered when the autodownloader settings are updated
type AutoDownloaderSettingsUpdatedEvent struct {
hook_resolver.Event
Settings *models.AutoDownloaderSettings `json:"settings"`
}
// AutoDownloaderBeforeDownloadTorrentEvent is triggered when the autodownloader is about to download a torrent.
// Prevent default to abort the download.
type AutoDownloaderBeforeDownloadTorrentEvent struct {
hook_resolver.Event
Torrent *NormalizedTorrent `json:"torrent"`
Rule *anime.AutoDownloaderRule `json:"rule"`
Items []*models.AutoDownloaderItem `json:"items"`
}
// AutoDownloaderAfterDownloadTorrentEvent is triggered when the autodownloader has downloaded a torrent.
type AutoDownloaderAfterDownloadTorrentEvent struct {
hook_resolver.Event
Torrent *NormalizedTorrent `json:"torrent"`
Rule *anime.AutoDownloaderRule `json:"rule"`
}