node build fixed
This commit is contained in:
414
seanime-2.9.10/internal/torrentstream/finder.go
Normal file
414
seanime-2.9.10/internal/torrentstream/finder.go
Normal file
@@ -0,0 +1,414 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/hook"
|
||||
torrentanalyzer "seanime/internal/torrents/analyzer"
|
||||
itorrent "seanime/internal/torrents/torrent"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/torrentutil"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoTorrentsFound = fmt.Errorf("no torrents found, please select manually")
|
||||
ErrNoEpisodeFound = fmt.Errorf("could not select episode from torrents, please select manually")
|
||||
)
|
||||
|
||||
type (
|
||||
playbackTorrent struct {
|
||||
Torrent *torrent.Torrent
|
||||
File *torrent.File
|
||||
}
|
||||
)
|
||||
|
||||
// setPriorityDownloadStrategy sets piece priorities for optimal streaming experience
|
||||
// This helps to optimize initial buffering, seeking, and end-of-file playback
|
||||
func (r *Repository) setPriorityDownloadStrategy(t *torrent.Torrent, file *torrent.File) {
|
||||
torrentutil.PrioritizeDownloadPieces(t, file, r.logger)
|
||||
}
|
||||
|
||||
func (r *Repository) findBestTorrent(media *anilist.CompleteAnime, aniDbEpisode string, episodeNumber int) (ret *playbackTorrent, err error) {
|
||||
defer util.HandlePanicInModuleWithError("torrentstream/findBestTorrent", &err)
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Finding best torrent for %s, Episode %d", media.GetTitleSafe(), episodeNumber)
|
||||
|
||||
providerId := itorrent.ProviderAnimeTosho // todo: get provider from settings
|
||||
fallbackProviderId := itorrent.ProviderNyaa
|
||||
|
||||
// Get AnimeTosho provider extension
|
||||
providerExtension, ok := r.torrentRepository.GetAnimeProviderExtension(providerId)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", itorrent.ProviderAnimeTosho).Msg("torrentstream: AnimeTosho provider extension not found")
|
||||
return nil, fmt.Errorf("provider extension not found")
|
||||
}
|
||||
|
||||
searchBatch := false
|
||||
// Search batch if not a movie and finished
|
||||
yearsSinceStart := 999
|
||||
if media.StartDate != nil && *media.StartDate.Year > 0 {
|
||||
yearsSinceStart = time.Now().Year() - *media.StartDate.Year // e.g. 2024 - 2020 = 4
|
||||
}
|
||||
if !media.IsMovie() && media.IsFinished() && yearsSinceStart > 4 {
|
||||
searchBatch = true
|
||||
}
|
||||
|
||||
r.sendStateEvent(eventLoading, TLSStateSearchingTorrents)
|
||||
|
||||
var data *itorrent.SearchData
|
||||
var currentProvider string = providerId
|
||||
searchLoop:
|
||||
for {
|
||||
var err error
|
||||
data, err = r.torrentRepository.SearchAnime(context.Background(), itorrent.AnimeSearchOptions{
|
||||
Provider: currentProvider,
|
||||
Type: itorrent.AnimeSearchTypeSmart,
|
||||
Media: media.ToBaseAnime(),
|
||||
Query: "",
|
||||
Batch: searchBatch,
|
||||
EpisodeNumber: episodeNumber,
|
||||
BestReleases: false,
|
||||
Resolution: r.settings.MustGet().PreferredResolution,
|
||||
})
|
||||
// If we are searching for batches, we don't want to return an error if no torrents are found
|
||||
// We will just search again without the batch flag
|
||||
if err != nil && !searchBatch {
|
||||
r.logger.Error().Err(err).Msg("torrentstream: Error searching torrents")
|
||||
|
||||
// Try fallback provider if we're still on primary provider
|
||||
if currentProvider == providerId {
|
||||
r.logger.Debug().Msgf("torrentstream: Primary provider failed, trying fallback provider %s", fallbackProviderId)
|
||||
currentProvider = fallbackProviderId
|
||||
// Get fallback provider extension
|
||||
providerExtension, ok = r.torrentRepository.GetAnimeProviderExtension(currentProvider)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", fallbackProviderId).Msg("torrentstream: Fallback provider extension not found")
|
||||
return nil, fmt.Errorf("fallback provider extension not found")
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
} else if err != nil {
|
||||
searchBatch = false
|
||||
continue
|
||||
}
|
||||
|
||||
// This whole thing below just means that
|
||||
// If we are looking for batches, there should be at least 3 torrents found or the max seeders should be at least 15
|
||||
if searchBatch {
|
||||
nbFound := len(data.Torrents)
|
||||
seedersArr := lo.Map(data.Torrents, func(t *hibiketorrent.AnimeTorrent, _ int) int {
|
||||
return t.Seeders
|
||||
})
|
||||
if len(seedersArr) == 0 {
|
||||
searchBatch = false
|
||||
continue
|
||||
}
|
||||
maxSeeders := slices.Max(seedersArr)
|
||||
if maxSeeders >= 15 || nbFound > 2 {
|
||||
break searchLoop
|
||||
} else {
|
||||
searchBatch = false
|
||||
}
|
||||
} else {
|
||||
break searchLoop
|
||||
}
|
||||
}
|
||||
|
||||
if data == nil || len(data.Torrents) == 0 {
|
||||
// Try fallback provider if we're still on primary provider
|
||||
if currentProvider == providerId {
|
||||
r.logger.Debug().Msgf("torrentstream: No torrents found with primary provider, trying fallback provider %s", fallbackProviderId)
|
||||
currentProvider = fallbackProviderId
|
||||
// Get fallback provider extension
|
||||
providerExtension, ok = r.torrentRepository.GetAnimeProviderExtension(currentProvider)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", fallbackProviderId).Msg("torrentstream: Fallback provider extension not found")
|
||||
return nil, fmt.Errorf("fallback provider extension not found")
|
||||
}
|
||||
|
||||
// Try searching with fallback provider (reset searchBatch)
|
||||
searchBatch = false
|
||||
if !media.IsMovie() && media.IsFinished() && yearsSinceStart > 4 {
|
||||
searchBatch = true
|
||||
}
|
||||
|
||||
// Restart the search with fallback provider
|
||||
goto searchLoop
|
||||
}
|
||||
|
||||
r.logger.Error().Msg("torrentstream: No torrents found")
|
||||
return nil, ErrNoTorrentsFound
|
||||
}
|
||||
|
||||
// Sort by seeders from highest to lowest
|
||||
slices.SortStableFunc(data.Torrents, func(a, b *hibiketorrent.AnimeTorrent) int {
|
||||
return cmp.Compare(b.Seeders, a.Seeders)
|
||||
})
|
||||
|
||||
// Trigger hook
|
||||
fetchedEvent := &TorrentStreamAutoSelectTorrentsFetchedEvent{
|
||||
Torrents: data.Torrents,
|
||||
}
|
||||
_ = hook.GlobalHookManager.OnTorrentStreamAutoSelectTorrentsFetched().Trigger(fetchedEvent)
|
||||
data.Torrents = fetchedEvent.Torrents
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Found %d torrents", len(data.Torrents))
|
||||
|
||||
// Go through the top 3 torrents
|
||||
// - For each torrent, add it, get the files, and check if it has the episode
|
||||
// - If it does, return the magnet link
|
||||
var selectedTorrent *torrent.Torrent
|
||||
var selectedFile *torrent.File
|
||||
tries := 0
|
||||
|
||||
for _, searchT := range data.Torrents {
|
||||
if tries >= 2 {
|
||||
break
|
||||
}
|
||||
r.sendStateEvent(eventLoading, struct {
|
||||
State any `json:"state"`
|
||||
TorrentBeingLoaded string `json:"torrentBeingLoaded"`
|
||||
}{
|
||||
State: TLSStateAddingTorrent,
|
||||
TorrentBeingLoaded: searchT.Name,
|
||||
})
|
||||
r.logger.Trace().Msgf("torrentstream: Getting torrent magnet")
|
||||
magnet, err := providerExtension.GetProvider().GetTorrentMagnetLink(searchT)
|
||||
if err != nil {
|
||||
r.logger.Warn().Err(err).Msgf("torrentstream: Error scraping magnet link for %s", searchT.Link)
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
r.logger.Debug().Msgf("torrentstream: Adding torrent %s from magnet", searchT.Link)
|
||||
|
||||
t, err := r.client.AddTorrent(magnet)
|
||||
if err != nil {
|
||||
r.logger.Warn().Err(err).Msgf("torrentstream: Error adding torrent %s", searchT.Link)
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
|
||||
r.sendStateEvent(eventLoading, struct {
|
||||
State any `json:"state"`
|
||||
TorrentBeingLoaded string `json:"torrentBeingLoaded"`
|
||||
}{
|
||||
State: TLSStateCheckingTorrent,
|
||||
TorrentBeingLoaded: searchT.Name,
|
||||
})
|
||||
|
||||
// If the torrent has only one file, return it
|
||||
if len(t.Files()) == 1 {
|
||||
tFile := t.Files()[0]
|
||||
tFile.Download()
|
||||
r.setPriorityDownloadStrategy(t, tFile)
|
||||
r.logger.Debug().Msgf("torrentstream: Found single file torrent: %s", tFile.DisplayPath())
|
||||
|
||||
return &playbackTorrent{
|
||||
Torrent: t,
|
||||
File: tFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
r.sendStateEvent(eventLoading, TLSStateSelectingFile)
|
||||
|
||||
// DEVNOTE: The gap between adding the torrent and file analysis causes some pieces to be downloaded
|
||||
// We currently can't Pause/Resume torrents so :shrug:
|
||||
|
||||
filepaths := lo.Map(t.Files(), func(f *torrent.File, _ int) string {
|
||||
return f.DisplayPath()
|
||||
})
|
||||
|
||||
if len(filepaths) == 0 {
|
||||
r.logger.Error().Msg("torrentstream: No files found in the torrent")
|
||||
return nil, fmt.Errorf("no files found in the torrent")
|
||||
}
|
||||
|
||||
// Create a new Torrent Analyzer
|
||||
analyzer := torrentanalyzer.NewAnalyzer(&torrentanalyzer.NewAnalyzerOptions{
|
||||
Logger: r.logger,
|
||||
Filepaths: filepaths,
|
||||
Media: media,
|
||||
Platform: r.platform,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
ForceMatch: true,
|
||||
})
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Analyzing torrent %s", searchT.Link)
|
||||
|
||||
// Analyze torrent files
|
||||
analysis, err := analyzer.AnalyzeTorrentFiles()
|
||||
if err != nil {
|
||||
r.logger.Warn().Err(err).Msg("torrentstream: Error analyzing torrent files")
|
||||
// Remove torrent on failure
|
||||
go func() {
|
||||
_ = r.client.RemoveTorrent(t.InfoHash().AsString())
|
||||
}()
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
|
||||
analysisFile, found := analysis.GetFileByAniDBEpisode(aniDbEpisode)
|
||||
// Check if analyzer found the episode
|
||||
if !found {
|
||||
r.logger.Error().Msgf("torrentstream: Failed to auto-select episode from torrent %s", searchT.Link)
|
||||
// Remove torrent on failure
|
||||
go func() {
|
||||
_ = r.client.RemoveTorrent(t.InfoHash().AsString())
|
||||
}()
|
||||
tries++
|
||||
continue
|
||||
}
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Found corresponding file for episode %s: %s", aniDbEpisode, analysisFile.GetLocalFile().Name)
|
||||
|
||||
// Download the file and unselect the rest
|
||||
for i, f := range t.Files() {
|
||||
if i != analysisFile.GetIndex() {
|
||||
f.SetPriority(torrent.PiecePriorityNone)
|
||||
}
|
||||
}
|
||||
tFile := t.Files()[analysisFile.GetIndex()]
|
||||
r.logger.Debug().Msgf("torrentstream: Selecting file %s", tFile.DisplayPath())
|
||||
r.setPriorityDownloadStrategy(t, tFile)
|
||||
|
||||
selectedTorrent = t
|
||||
selectedFile = tFile
|
||||
break
|
||||
}
|
||||
|
||||
if selectedTorrent == nil {
|
||||
return nil, ErrNoEpisodeFound
|
||||
}
|
||||
|
||||
ret = &playbackTorrent{
|
||||
Torrent: selectedTorrent,
|
||||
File: selectedFile,
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// findBestTorrentFromManualSelection is like findBestTorrent but no need to search for the best torrent first
|
||||
func (r *Repository) findBestTorrentFromManualSelection(t *hibiketorrent.AnimeTorrent, media *anilist.CompleteAnime, aniDbEpisode string, chosenFileIndex *int) (*playbackTorrent, error) {
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Analyzing torrent from %s for %s", t.Link, media.GetTitleSafe())
|
||||
|
||||
// Get the torrent's provider extension
|
||||
providerExtension, ok := r.torrentRepository.GetAnimeProviderExtension(t.Provider)
|
||||
if !ok {
|
||||
r.logger.Error().Str("provider", t.Provider).Msg("torrentstream: provider extension not found")
|
||||
return nil, fmt.Errorf("provider extension not found")
|
||||
}
|
||||
|
||||
// First, add the torrent
|
||||
magnet, err := providerExtension.GetProvider().GetTorrentMagnetLink(t)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msgf("torrentstream: Error scraping magnet link for %s", t.Link)
|
||||
return nil, fmt.Errorf("could not get magnet link from %s", t.Link)
|
||||
}
|
||||
selectedTorrent, err := r.client.AddTorrent(magnet)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msgf("torrentstream: Error adding torrent %s", t.Link)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the torrent has only one file, return it
|
||||
if len(selectedTorrent.Files()) == 1 {
|
||||
tFile := selectedTorrent.Files()[0]
|
||||
tFile.Download()
|
||||
r.setPriorityDownloadStrategy(selectedTorrent, tFile)
|
||||
r.logger.Debug().Msgf("torrentstream: Found single file torrent: %s", tFile.DisplayPath())
|
||||
|
||||
return &playbackTorrent{
|
||||
Torrent: selectedTorrent,
|
||||
File: tFile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var fileIndex int
|
||||
|
||||
// If the file index is already selected
|
||||
if chosenFileIndex != nil {
|
||||
|
||||
fileIndex = *chosenFileIndex
|
||||
|
||||
} else {
|
||||
|
||||
// We know the torrent has multiple files, so we'll need to analyze it
|
||||
filepaths := lo.Map(selectedTorrent.Files(), func(f *torrent.File, _ int) string {
|
||||
return f.DisplayPath()
|
||||
})
|
||||
|
||||
if len(filepaths) == 0 {
|
||||
r.logger.Error().Msg("torrentstream: No files found in the torrent")
|
||||
return nil, fmt.Errorf("no files found in the torrent")
|
||||
}
|
||||
|
||||
// Create a new Torrent Analyzer
|
||||
analyzer := torrentanalyzer.NewAnalyzer(&torrentanalyzer.NewAnalyzerOptions{
|
||||
Logger: r.logger,
|
||||
Filepaths: filepaths,
|
||||
Media: media,
|
||||
Platform: r.platform,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
ForceMatch: true,
|
||||
})
|
||||
|
||||
// Analyze torrent files
|
||||
analysis, err := analyzer.AnalyzeTorrentFiles()
|
||||
if err != nil {
|
||||
r.logger.Warn().Err(err).Msg("torrentstream: Error analyzing torrent files")
|
||||
// Remove torrent on failure
|
||||
go func() {
|
||||
_ = r.client.RemoveTorrent(selectedTorrent.InfoHash().AsString())
|
||||
}()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
analysisFile, found := analysis.GetFileByAniDBEpisode(aniDbEpisode)
|
||||
// Check if analyzer found the episode
|
||||
if !found {
|
||||
r.logger.Error().Msgf("torrentstream: Failed to auto-select episode from torrent %s", selectedTorrent.Info().Name)
|
||||
// Remove torrent on failure
|
||||
go func() {
|
||||
_ = r.client.RemoveTorrent(selectedTorrent.InfoHash().AsString())
|
||||
}()
|
||||
return nil, ErrNoEpisodeFound
|
||||
}
|
||||
|
||||
r.logger.Debug().Msgf("torrentstream: Found corresponding file for episode %s: %s", aniDbEpisode, analysisFile.GetLocalFile().Name)
|
||||
|
||||
fileIndex = analysisFile.GetIndex()
|
||||
|
||||
}
|
||||
|
||||
// Download the file and unselect the rest
|
||||
for i, f := range selectedTorrent.Files() {
|
||||
if i != fileIndex {
|
||||
f.SetPriority(torrent.PiecePriorityNone)
|
||||
}
|
||||
}
|
||||
//selectedTorrent.Files()[fileIndex].SetPriority(torrent.PiecePriorityNormal)
|
||||
r.logger.Debug().Msgf("torrentstream: Selected torrent %s", selectedTorrent.Files()[fileIndex].DisplayPath())
|
||||
|
||||
tFile := selectedTorrent.Files()[fileIndex]
|
||||
tFile.Download()
|
||||
r.setPriorityDownloadStrategy(selectedTorrent, tFile)
|
||||
|
||||
ret := &playbackTorrent{
|
||||
Torrent: selectedTorrent,
|
||||
File: selectedTorrent.Files()[fileIndex],
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
Reference in New Issue
Block a user