node build fixed
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user