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,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
}