node build fixed
This commit is contained in:
2
seanime-2.9.10/internal/torrents/torrent/README.md
Normal file
2
seanime-2.9.10/internal/torrents/torrent/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
Do not import:
|
||||
- torrent_client
|
||||
142
seanime-2.9.10/internal/torrents/torrent/repository.go
Normal file
142
seanime-2.9.10/internal/torrents/torrent/repository.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/util/result"
|
||||
"sync"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository struct {
|
||||
logger *zerolog.Logger
|
||||
extensionBank *extension.UnifiedBank
|
||||
animeProviderSearchCaches *result.Map[string, *result.Cache[string, *SearchData]]
|
||||
animeProviderSmartSearchCaches *result.Map[string, *result.Cache[string, *SearchData]]
|
||||
settings RepositorySettings
|
||||
metadataProvider metadata.Provider
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
RepositorySettings struct {
|
||||
DefaultAnimeProvider string // Default torrent provider
|
||||
}
|
||||
)
|
||||
|
||||
type NewRepositoryOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
MetadataProvider metadata.Provider
|
||||
}
|
||||
|
||||
func NewRepository(opts *NewRepositoryOptions) *Repository {
|
||||
ret := &Repository{
|
||||
logger: opts.Logger,
|
||||
metadataProvider: opts.MetadataProvider,
|
||||
extensionBank: extension.NewUnifiedBank(),
|
||||
animeProviderSearchCaches: result.NewResultMap[string, *result.Cache[string, *SearchData]](),
|
||||
animeProviderSmartSearchCaches: result.NewResultMap[string, *result.Cache[string, *SearchData]](),
|
||||
settings: RepositorySettings{},
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *Repository) InitExtensionBank(bank *extension.UnifiedBank) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.extensionBank = bank
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-bank.OnExtensionAdded():
|
||||
//r.logger.Debug().Msg("torrent repo: Anime provider extension added")
|
||||
r.OnExtensionReloaded()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-bank.OnExtensionRemoved():
|
||||
r.OnExtensionReloaded()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
r.logger.Debug().Msg("torrent repo: Initialized anime provider extension bank")
|
||||
}
|
||||
|
||||
func (r *Repository) OnExtensionReloaded() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.reloadExtensions()
|
||||
}
|
||||
|
||||
// This is called each time a new extension is added or removed
|
||||
func (r *Repository) reloadExtensions() {
|
||||
// Clear the search caches
|
||||
r.animeProviderSearchCaches = result.NewResultMap[string, *result.Cache[string, *SearchData]]()
|
||||
r.animeProviderSmartSearchCaches = result.NewResultMap[string, *result.Cache[string, *SearchData]]()
|
||||
|
||||
go func() {
|
||||
// Create new caches for each provider
|
||||
extension.RangeExtensions(r.extensionBank, func(provider string, value extension.AnimeTorrentProviderExtension) bool {
|
||||
r.animeProviderSearchCaches.Set(provider, result.NewCache[string, *SearchData]())
|
||||
r.animeProviderSmartSearchCaches.Set(provider, result.NewCache[string, *SearchData]())
|
||||
return true
|
||||
})
|
||||
}()
|
||||
|
||||
// Check if the default provider is in the list of providers
|
||||
//if r.settings.DefaultAnimeProvider != "" && r.settings.DefaultAnimeProvider != "none" {
|
||||
// if _, ok := r.extensionBank.Get(r.settings.DefaultAnimeProvider); !ok {
|
||||
// //r.logger.Error().Str("defaultProvider", r.settings.DefaultAnimeProvider).Msg("torrent repo: Default torrent provider not found in extensions")
|
||||
// // Set the default provider to empty
|
||||
// r.settings.DefaultAnimeProvider = ""
|
||||
// }
|
||||
//}
|
||||
|
||||
//r.logger.Trace().Str("defaultProvider", r.settings.DefaultAnimeProvider).Msg("torrent repo: Reloaded extensions")
|
||||
}
|
||||
|
||||
// SetSettings should be called after the repository is created and settings are refreshed
|
||||
func (r *Repository) SetSettings(s *RepositorySettings) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.logger.Trace().Msg("torrent repo: Setting settings")
|
||||
|
||||
if s != nil {
|
||||
r.settings = *s
|
||||
} else {
|
||||
r.settings = RepositorySettings{
|
||||
DefaultAnimeProvider: "",
|
||||
}
|
||||
}
|
||||
|
||||
if r.settings.DefaultAnimeProvider == "none" {
|
||||
r.settings.DefaultAnimeProvider = ""
|
||||
}
|
||||
|
||||
// Reload extensions after settings change
|
||||
r.reloadExtensions()
|
||||
}
|
||||
|
||||
func (r *Repository) GetDefaultAnimeProviderExtension() (extension.AnimeTorrentProviderExtension, bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.settings.DefaultAnimeProvider == "" {
|
||||
return nil, false
|
||||
}
|
||||
return extension.GetExtension[extension.AnimeTorrentProviderExtension](r.extensionBank, r.settings.DefaultAnimeProvider)
|
||||
}
|
||||
|
||||
func (r *Repository) GetAnimeProviderExtension(id string) (extension.AnimeTorrentProviderExtension, bool) {
|
||||
return extension.GetExtension[extension.AnimeTorrentProviderExtension](r.extensionBank, id)
|
||||
}
|
||||
67
seanime-2.9.10/internal/torrents/torrent/repository_test.go
Normal file
67
seanime-2.9.10/internal/torrents/torrent/repository_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/extension"
|
||||
"seanime/internal/torrents/animetosho"
|
||||
"seanime/internal/torrents/nyaa"
|
||||
"seanime/internal/torrents/seadex"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func getTestRepo(t *testing.T) *Repository {
|
||||
logger := util.NewLogger()
|
||||
metadataProvider := metadata.GetMockProvider(t)
|
||||
|
||||
extensionBank := extension.NewUnifiedBank()
|
||||
|
||||
extensionBank.Set("nyaa", extension.NewAnimeTorrentProviderExtension(&extension.Extension{
|
||||
ID: "nyaa",
|
||||
Name: "Nyaa",
|
||||
Version: "1.0.0",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
}, nyaa.NewProvider(logger, nyaa.CategoryAnimeEng)))
|
||||
|
||||
extensionBank.Set("nyaa-sukebei", extension.NewAnimeTorrentProviderExtension(&extension.Extension{
|
||||
ID: "nyaa-sukebei",
|
||||
Name: "Nyaa Sukebei",
|
||||
Version: "1.0.0",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
}, nyaa.NewSukebeiProvider(logger)))
|
||||
|
||||
extensionBank.Set("animetosho", extension.NewAnimeTorrentProviderExtension(&extension.Extension{
|
||||
ID: "animetosho",
|
||||
Name: "AnimeTosho",
|
||||
Version: "1.0.0",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
}, animetosho.NewProvider(logger)))
|
||||
|
||||
extensionBank.Set("seadex", extension.NewAnimeTorrentProviderExtension(&extension.Extension{
|
||||
ID: "seadex",
|
||||
Name: "SeaDex",
|
||||
Version: "1.0.0",
|
||||
Language: extension.LanguageGo,
|
||||
Type: extension.TypeAnimeTorrentProvider,
|
||||
Author: "Seanime",
|
||||
}, seadex.NewProvider(logger)))
|
||||
|
||||
repo := NewRepository(&NewRepositoryOptions{
|
||||
Logger: logger,
|
||||
MetadataProvider: metadataProvider,
|
||||
})
|
||||
|
||||
repo.InitExtensionBank(extensionBank)
|
||||
|
||||
repo.SetSettings(&RepositorySettings{
|
||||
DefaultAnimeProvider: ProviderAnimeTosho,
|
||||
})
|
||||
|
||||
return repo
|
||||
}
|
||||
463
seanime-2.9.10/internal/torrents/torrent/search.go
Normal file
463
seanime-2.9.10/internal/torrents/torrent/search.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/debrid/debrid"
|
||||
"seanime/internal/extension"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/comparison"
|
||||
"seanime/internal/util/result"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/5rahim/habari"
|
||||
"github.com/samber/lo"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
const (
|
||||
AnimeSearchTypeSmart AnimeSearchType = "smart"
|
||||
AnimeSearchTypeSimple AnimeSearchType = "simple"
|
||||
)
|
||||
|
||||
var (
|
||||
metadataCache = result.NewResultMap[string, *TorrentMetadata]()
|
||||
)
|
||||
|
||||
type (
|
||||
AnimeSearchType string
|
||||
|
||||
AnimeSearchOptions struct {
|
||||
// Provider extension ID
|
||||
Provider string
|
||||
Type AnimeSearchType
|
||||
Media *anilist.BaseAnime
|
||||
// Search options
|
||||
Query string
|
||||
// Filter options
|
||||
Batch bool
|
||||
EpisodeNumber int
|
||||
BestReleases bool
|
||||
Resolution string
|
||||
}
|
||||
|
||||
// Preview contains the torrent and episode information
|
||||
Preview struct {
|
||||
Episode *anime.Episode `json:"episode"` // nil if batch
|
||||
Torrent *hibiketorrent.AnimeTorrent `json:"torrent"`
|
||||
}
|
||||
|
||||
TorrentMetadata struct {
|
||||
Distance int `json:"distance"`
|
||||
Metadata *habari.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
// SearchData is the struct returned by NewSmartSearch
|
||||
SearchData struct {
|
||||
Torrents []*hibiketorrent.AnimeTorrent `json:"torrents"` // Torrents found
|
||||
Previews []*Preview `json:"previews"` // TorrentPreview for each torrent
|
||||
TorrentMetadata map[string]*TorrentMetadata `json:"torrentMetadata"` // Torrent metadata
|
||||
DebridInstantAvailability map[string]debrid.TorrentItemInstantAvailability `json:"debridInstantAvailability"` // Debrid instant availability
|
||||
AnimeMetadata *metadata.AnimeMetadata `json:"animeMetadata"` // Animap media
|
||||
}
|
||||
)
|
||||
|
||||
func (r *Repository) SearchAnime(ctx context.Context, opts AnimeSearchOptions) (ret *SearchData, err error) {
|
||||
defer util.HandlePanicInModuleWithError("torrents/torrent/SearchAnime", &err)
|
||||
|
||||
r.logger.Debug().Str("provider", opts.Provider).Str("type", string(opts.Type)).Str("query", opts.Query).Msg("torrent repo: Searching for anime torrents")
|
||||
|
||||
// Find the provider by ID
|
||||
providerExtension, ok := extension.GetExtension[extension.AnimeTorrentProviderExtension](r.extensionBank, opts.Provider)
|
||||
if !ok {
|
||||
// Get the default provider
|
||||
providerExtension, ok = r.GetDefaultAnimeProviderExtension()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("torrent provider not found")
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Type == AnimeSearchTypeSmart && !providerExtension.GetProvider().GetSettings().CanSmartSearch {
|
||||
return nil, fmt.Errorf("provider does not support smart search")
|
||||
}
|
||||
|
||||
var torrents []*hibiketorrent.AnimeTorrent
|
||||
|
||||
// Fetch Animap media
|
||||
animeMetadata := mo.None[*metadata.AnimeMetadata]()
|
||||
animeMetadataF, err := r.metadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, opts.Media.GetID())
|
||||
if err == nil {
|
||||
animeMetadata = mo.Some(animeMetadataF)
|
||||
}
|
||||
|
||||
queryMedia := hibiketorrent.Media{
|
||||
ID: opts.Media.GetID(),
|
||||
IDMal: opts.Media.GetIDMal(),
|
||||
Status: string(*opts.Media.GetStatus()),
|
||||
Format: string(*opts.Media.GetFormat()),
|
||||
EnglishTitle: opts.Media.GetTitle().GetEnglish(),
|
||||
RomajiTitle: opts.Media.GetRomajiTitleSafe(),
|
||||
EpisodeCount: opts.Media.GetTotalEpisodeCount(),
|
||||
AbsoluteSeasonOffset: 0,
|
||||
Synonyms: opts.Media.GetSynonymsContainingSeason(),
|
||||
IsAdult: *opts.Media.GetIsAdult(),
|
||||
StartDate: &hibiketorrent.FuzzyDate{
|
||||
Year: *opts.Media.GetStartDate().GetYear(),
|
||||
Month: opts.Media.GetStartDate().GetMonth(),
|
||||
Day: opts.Media.GetStartDate().GetDay(),
|
||||
},
|
||||
}
|
||||
|
||||
//// Force simple search if Animap media is absent
|
||||
//if opts.Type == AnimeSearchTypeSmart && animeMetadata.IsAbsent() {
|
||||
// opts.Type = AnimeSearchTypeSimple
|
||||
//}
|
||||
|
||||
var queryKey string
|
||||
|
||||
switch opts.Type {
|
||||
case AnimeSearchTypeSmart:
|
||||
anidbAID := 0
|
||||
anidbEID := 0
|
||||
|
||||
// Get the AniDB Anime ID and Episode ID
|
||||
if animeMetadata.IsPresent() {
|
||||
// Override absolute offset value of queryMedia
|
||||
queryMedia.AbsoluteSeasonOffset = animeMetadata.MustGet().GetOffset()
|
||||
|
||||
if animeMetadata.MustGet().GetMappings() != nil {
|
||||
|
||||
anidbAID = animeMetadata.MustGet().GetMappings().AnidbId
|
||||
// Find Animap Episode based on inputted episode number
|
||||
episodeMetadata, found := animeMetadata.MustGet().FindEpisode(strconv.Itoa(opts.EpisodeNumber))
|
||||
if found {
|
||||
anidbEID = episodeMetadata.AnidbEid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queryKey = fmt.Sprintf("%d-%s-%d-%d-%d-%s-%t-%t", opts.Media.GetID(), opts.Query, opts.EpisodeNumber, anidbAID, anidbEID, opts.Resolution, opts.BestReleases, opts.Batch)
|
||||
if cache, found := r.animeProviderSmartSearchCaches.Get(opts.Provider); found {
|
||||
// Check the cache
|
||||
data, found := cache.Get(queryKey)
|
||||
if found {
|
||||
r.logger.Debug().Str("provider", opts.Provider).Str("type", string(opts.Type)).Msg("torrent repo: Cache HIT")
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for context cancellation before making the request
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
torrents, err = providerExtension.GetProvider().SmartSearch(hibiketorrent.AnimeSmartSearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: opts.Query,
|
||||
Batch: opts.Batch,
|
||||
EpisodeNumber: opts.EpisodeNumber,
|
||||
Resolution: opts.Resolution,
|
||||
AnidbAID: anidbAID,
|
||||
AnidbEID: anidbEID,
|
||||
BestReleases: opts.BestReleases,
|
||||
})
|
||||
|
||||
torrents = lo.UniqBy(torrents, func(t *hibiketorrent.AnimeTorrent) string {
|
||||
return t.InfoHash
|
||||
})
|
||||
|
||||
case AnimeSearchTypeSimple:
|
||||
|
||||
queryKey = fmt.Sprintf("%d-%s", opts.Media.GetID(), opts.Query)
|
||||
if cache, found := r.animeProviderSearchCaches.Get(opts.Provider); found {
|
||||
// Check the cache
|
||||
data, found := cache.Get(queryKey)
|
||||
if found {
|
||||
r.logger.Debug().Str("provider", opts.Provider).Str("type", string(opts.Type)).Msg("torrent repo: Cache HIT")
|
||||
return data, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Check for context cancellation before making the request
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
torrents, err = providerExtension.GetProvider().Search(hibiketorrent.AnimeSearchOptions{
|
||||
Media: queryMedia,
|
||||
Query: opts.Query,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//
|
||||
// Torrent metadata
|
||||
//
|
||||
torrentMetadata := make(map[string]*TorrentMetadata)
|
||||
mu := sync.Mutex{}
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(torrents))
|
||||
for _, t := range torrents {
|
||||
go func(t *hibiketorrent.AnimeTorrent) {
|
||||
defer wg.Done()
|
||||
metadata, found := metadataCache.Get(t.Name)
|
||||
if !found {
|
||||
m := habari.Parse(t.Name)
|
||||
var distance *comparison.LevenshteinResult
|
||||
distance, ok := comparison.FindBestMatchWithLevenshtein(&m.Title, opts.Media.GetAllTitles())
|
||||
if !ok {
|
||||
distance = &comparison.LevenshteinResult{
|
||||
Distance: 1000,
|
||||
}
|
||||
}
|
||||
metadata = &TorrentMetadata{
|
||||
Distance: distance.Distance,
|
||||
Metadata: m,
|
||||
}
|
||||
metadataCache.Set(t.Name, metadata)
|
||||
}
|
||||
mu.Lock()
|
||||
torrentMetadata[t.InfoHash] = metadata
|
||||
mu.Unlock()
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
//
|
||||
// Previews
|
||||
//
|
||||
previews := make([]*Preview, 0)
|
||||
|
||||
if opts.Type == AnimeSearchTypeSmart {
|
||||
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(len(torrents))
|
||||
for _, t := range torrents {
|
||||
go func(t *hibiketorrent.AnimeTorrent) {
|
||||
defer wg.Done()
|
||||
|
||||
// Check for context cancellation in each goroutine
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
preview := r.createAnimeTorrentPreview(createAnimeTorrentPreviewOptions{
|
||||
torrent: t,
|
||||
media: opts.Media,
|
||||
animeMetadata: animeMetadata,
|
||||
searchOpts: &opts,
|
||||
})
|
||||
if preview != nil {
|
||||
previews = append(previews, preview)
|
||||
}
|
||||
}(t)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// Check if context was cancelled during preview creation
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// sort both by seeders
|
||||
slices.SortFunc(torrents, func(i, j *hibiketorrent.AnimeTorrent) int {
|
||||
return cmp.Compare(j.Seeders, i.Seeders)
|
||||
})
|
||||
previews = lo.Filter(previews, func(p *Preview, _ int) bool {
|
||||
return p != nil && p.Torrent != nil
|
||||
})
|
||||
slices.SortFunc(previews, func(i, j *Preview) int {
|
||||
return cmp.Compare(j.Torrent.Seeders, i.Torrent.Seeders)
|
||||
})
|
||||
|
||||
ret = &SearchData{
|
||||
Torrents: torrents,
|
||||
Previews: previews,
|
||||
TorrentMetadata: torrentMetadata,
|
||||
}
|
||||
|
||||
if animeMetadata.IsPresent() {
|
||||
ret.AnimeMetadata = animeMetadata.MustGet()
|
||||
}
|
||||
|
||||
// Store the data in the cache
|
||||
switch opts.Type {
|
||||
case AnimeSearchTypeSmart:
|
||||
if cache, found := r.animeProviderSmartSearchCaches.Get(opts.Provider); found {
|
||||
cache.Set(queryKey, ret)
|
||||
}
|
||||
case AnimeSearchTypeSimple:
|
||||
if cache, found := r.animeProviderSearchCaches.Get(opts.Provider); found {
|
||||
cache.Set(queryKey, ret)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
type createAnimeTorrentPreviewOptions struct {
|
||||
torrent *hibiketorrent.AnimeTorrent
|
||||
media *anilist.BaseAnime
|
||||
animeMetadata mo.Option[*metadata.AnimeMetadata]
|
||||
searchOpts *AnimeSearchOptions
|
||||
}
|
||||
|
||||
func (r *Repository) createAnimeTorrentPreview(opts createAnimeTorrentPreviewOptions) *Preview {
|
||||
|
||||
var parsedData *habari.Metadata
|
||||
metadata, found := metadataCache.Get(opts.torrent.Name)
|
||||
if !found { // Should always be found
|
||||
parsedData = habari.Parse(opts.torrent.Name)
|
||||
metadataCache.Set(opts.torrent.Name, &TorrentMetadata{
|
||||
Distance: 1000,
|
||||
Metadata: parsedData,
|
||||
})
|
||||
}
|
||||
parsedData = metadata.Metadata
|
||||
|
||||
isBatch := opts.torrent.IsBestRelease ||
|
||||
opts.torrent.IsBatch ||
|
||||
comparison.ValueContainsBatchKeywords(opts.torrent.Name) || // Contains batch keywords
|
||||
(!opts.media.IsMovieOrSingleEpisode() && len(parsedData.EpisodeNumber) > 1) // Multiple episodes parsed & not a movie
|
||||
|
||||
if opts.torrent.ReleaseGroup == "" {
|
||||
opts.torrent.ReleaseGroup = parsedData.ReleaseGroup
|
||||
}
|
||||
|
||||
if opts.torrent.Resolution == "" {
|
||||
opts.torrent.Resolution = parsedData.VideoResolution
|
||||
}
|
||||
|
||||
if opts.torrent.FormattedSize == "" {
|
||||
opts.torrent.FormattedSize = util.Bytes(uint64(opts.torrent.Size))
|
||||
}
|
||||
|
||||
if isBatch {
|
||||
return &Preview{
|
||||
Episode: nil, // Will be displayed as batch
|
||||
Torrent: opts.torrent,
|
||||
}
|
||||
}
|
||||
|
||||
// If past this point we haven't detected a batch but the episode number returned from the provider is -1
|
||||
// we will parse it from the torrent name
|
||||
if opts.torrent.EpisodeNumber == -1 && len(parsedData.EpisodeNumber) == 1 {
|
||||
opts.torrent.EpisodeNumber = util.StringToIntMust(parsedData.EpisodeNumber[0])
|
||||
}
|
||||
|
||||
// If the torrent is confirmed, use the episode number from the search options
|
||||
// because it could be absolute
|
||||
if opts.torrent.Confirmed {
|
||||
opts.torrent.EpisodeNumber = opts.searchOpts.EpisodeNumber
|
||||
}
|
||||
|
||||
// If there was no single episode number parsed but the media is movie, set the episode number to 1
|
||||
if opts.torrent.EpisodeNumber == -1 && opts.media.IsMovieOrSingleEpisode() {
|
||||
opts.torrent.EpisodeNumber = 1
|
||||
}
|
||||
|
||||
if opts.animeMetadata.IsPresent() {
|
||||
|
||||
// normalize episode number
|
||||
if opts.torrent.EpisodeNumber >= 0 && opts.torrent.EpisodeNumber > opts.media.GetCurrentEpisodeCount() {
|
||||
opts.torrent.EpisodeNumber = opts.torrent.EpisodeNumber - opts.animeMetadata.MustGet().GetOffset()
|
||||
}
|
||||
|
||||
animeMetadata := opts.animeMetadata.MustGet()
|
||||
_, foundEp := animeMetadata.FindEpisode(strconv.Itoa(opts.searchOpts.EpisodeNumber))
|
||||
|
||||
if foundEp {
|
||||
var episode *anime.Episode
|
||||
|
||||
// Remove the episode if the parsed episode number is not the same as the search option
|
||||
if isProbablySameEpisode(parsedData.EpisodeNumber, opts.searchOpts.EpisodeNumber, opts.animeMetadata.MustGet().GetOffset()) {
|
||||
ep := opts.searchOpts.EpisodeNumber
|
||||
episode = anime.NewEpisode(&anime.NewEpisodeOptions{
|
||||
LocalFile: nil,
|
||||
OptionalAniDBEpisode: strconv.Itoa(ep),
|
||||
AnimeMetadata: animeMetadata,
|
||||
Media: opts.media,
|
||||
ProgressOffset: 0,
|
||||
IsDownloaded: false,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
})
|
||||
episode.IsInvalid = false
|
||||
|
||||
if episode.DisplayTitle == "" {
|
||||
episode.DisplayTitle = parsedData.Title
|
||||
}
|
||||
}
|
||||
|
||||
return &Preview{
|
||||
Episode: episode,
|
||||
Torrent: opts.torrent,
|
||||
}
|
||||
}
|
||||
|
||||
var episode *anime.Episode
|
||||
|
||||
// Remove the episode if the parsed episode number is not the same as the search option
|
||||
if isProbablySameEpisode(parsedData.EpisodeNumber, opts.searchOpts.EpisodeNumber, opts.animeMetadata.MustGet().GetOffset()) {
|
||||
displayTitle := ""
|
||||
if len(parsedData.EpisodeNumber) == 1 && parsedData.EpisodeNumber[0] != strconv.Itoa(opts.searchOpts.EpisodeNumber) {
|
||||
displayTitle = fmt.Sprintf("Episode %s", parsedData.EpisodeNumber[0])
|
||||
}
|
||||
// If the episode number could not be found in the Animap media, create a new episode
|
||||
episode = &anime.Episode{
|
||||
Type: anime.LocalFileTypeMain,
|
||||
DisplayTitle: displayTitle,
|
||||
EpisodeTitle: "",
|
||||
EpisodeNumber: opts.searchOpts.EpisodeNumber,
|
||||
ProgressNumber: opts.searchOpts.EpisodeNumber,
|
||||
AniDBEpisode: "",
|
||||
AbsoluteEpisodeNumber: 0,
|
||||
LocalFile: nil,
|
||||
IsDownloaded: false,
|
||||
EpisodeMetadata: anime.NewEpisodeMetadata(opts.animeMetadata.MustGet(), nil, opts.media, r.metadataProvider),
|
||||
FileMetadata: nil,
|
||||
IsInvalid: false,
|
||||
MetadataIssue: "",
|
||||
BaseAnime: opts.media,
|
||||
}
|
||||
}
|
||||
|
||||
return &Preview{
|
||||
Episode: episode,
|
||||
Torrent: opts.torrent,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return &Preview{
|
||||
Episode: nil,
|
||||
Torrent: opts.torrent,
|
||||
}
|
||||
}
|
||||
|
||||
func isProbablySameEpisode(parsedEpisode []string, searchEpisode int, absoluteOffset int) bool {
|
||||
if len(parsedEpisode) == 1 {
|
||||
if util.StringToIntMust(parsedEpisode[0]) == searchEpisode || util.StringToIntMust(parsedEpisode[0]) == searchEpisode+absoluteOffset {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
111
seanime-2.9.10/internal/torrents/torrent/search_test.go
Normal file
111
seanime-2.9.10/internal/torrents/torrent/search_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/platforms/anilist_platform"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSmartSearch(t *testing.T) {
|
||||
test_utils.InitTestProvider(t)
|
||||
|
||||
anilistClient := anilist.TestGetMockAnilistClient()
|
||||
logger := util.NewLogger()
|
||||
anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger)
|
||||
|
||||
repo := getTestRepo(t)
|
||||
|
||||
tests := []struct {
|
||||
smartSearch bool
|
||||
query string
|
||||
episodeNumber int
|
||||
batch bool
|
||||
mediaId int
|
||||
absoluteOffset int
|
||||
resolution string
|
||||
provider string
|
||||
}{
|
||||
{
|
||||
smartSearch: true,
|
||||
query: "",
|
||||
episodeNumber: 5,
|
||||
batch: false,
|
||||
mediaId: 162670, // Dr. Stone S3
|
||||
absoluteOffset: 48,
|
||||
resolution: "1080",
|
||||
provider: "animetosho",
|
||||
},
|
||||
{
|
||||
smartSearch: true,
|
||||
query: "",
|
||||
episodeNumber: 1,
|
||||
batch: true,
|
||||
mediaId: 77, // Mahou Shoujo Lyrical Nanoha A's
|
||||
absoluteOffset: 0,
|
||||
resolution: "1080",
|
||||
provider: "animetosho",
|
||||
},
|
||||
{
|
||||
smartSearch: true,
|
||||
query: "",
|
||||
episodeNumber: 1,
|
||||
batch: true,
|
||||
mediaId: 109731, // Hibike Season 3
|
||||
absoluteOffset: 0,
|
||||
resolution: "1080",
|
||||
provider: "animetosho",
|
||||
},
|
||||
{
|
||||
smartSearch: true,
|
||||
query: "",
|
||||
episodeNumber: 1,
|
||||
batch: true,
|
||||
mediaId: 1915, // Magical Girl Lyrical Nanoha StrikerS
|
||||
absoluteOffset: 0,
|
||||
resolution: "",
|
||||
provider: "animetosho",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.query, func(t *testing.T) {
|
||||
|
||||
media, err := anilistPlatform.GetAnime(t.Context(), tt.mediaId)
|
||||
if err != nil {
|
||||
t.Fatalf("could not fetch media id %d", tt.mediaId)
|
||||
}
|
||||
|
||||
data, err := repo.SearchAnime(context.Background(), AnimeSearchOptions{
|
||||
Provider: tt.provider,
|
||||
Type: AnimeSearchTypeSmart,
|
||||
Media: media,
|
||||
Query: "",
|
||||
Batch: tt.batch,
|
||||
EpisodeNumber: tt.episodeNumber,
|
||||
BestReleases: false,
|
||||
Resolution: tt.resolution,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("NewSmartSearch() failed: %v", err)
|
||||
}
|
||||
|
||||
t.Log("----------------------- Previews --------------------------")
|
||||
for _, preview := range data.Previews {
|
||||
t.Logf("> %s", preview.Torrent.Name)
|
||||
if preview.Episode != nil {
|
||||
t.Logf("\t\t %s", preview.Episode.DisplayTitle)
|
||||
} else {
|
||||
t.Logf("\t\t Batch")
|
||||
}
|
||||
}
|
||||
t.Log("----------------------- Torrents --------------------------")
|
||||
for _, torrent := range data.Torrents {
|
||||
t.Logf("> %s", torrent.Name)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
7
seanime-2.9.10/internal/torrents/torrent/torrent.go
Normal file
7
seanime-2.9.10/internal/torrents/torrent/torrent.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package torrent
|
||||
|
||||
const (
|
||||
ProviderNyaa = "nyaa"
|
||||
ProviderAnimeTosho = "animetosho"
|
||||
ProviderNone = "none"
|
||||
)
|
||||
20
seanime-2.9.10/internal/torrents/torrent/utils.go
Normal file
20
seanime-2.9.10/internal/torrents/torrent/utils.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/anacrolix/torrent/metainfo"
|
||||
)
|
||||
|
||||
func StrDataToMagnetLink(data string) (string, error) {
|
||||
meta, err := metainfo.Load(bytes.NewReader([]byte(data)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
magnetLink, err := meta.MagnetV2()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return magnetLink.String(), nil
|
||||
}
|
||||
43
seanime-2.9.10/internal/torrents/torrent/utils_test.go
Normal file
43
seanime-2.9.10/internal/torrents/torrent/utils_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package torrent
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileToMagnetLink(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
}{
|
||||
{
|
||||
name: "1",
|
||||
url: "https://animetosho.org/storage/torrent/da9aad67b6f8bb82757bb3ef95235b42624c34f7/%5BSubsPlease%5D%20Make%20Heroine%20ga%20Oosugiru%21%20-%2011%20%281080p%29%20%5B58B3496A%5D.torrent",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
client := http.Client{}
|
||||
resp, err := client.Get(test.url)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
|
||||
magnet, err := StrDataToMagnetLink(string(data))
|
||||
if err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
t.Log(magnet)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user