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,391 @@
package torrentstream
import (
"context"
"fmt"
"seanime/internal/api/anilist"
"seanime/internal/api/metadata"
"seanime/internal/directstream"
"seanime/internal/events"
hibiketorrent "seanime/internal/extension/hibike/torrent"
"seanime/internal/hook"
"seanime/internal/library/playbackmanager"
"seanime/internal/util"
"strconv"
"time"
"github.com/anacrolix/torrent"
"github.com/samber/mo"
)
type PlaybackType string
const (
PlaybackTypeExternal PlaybackType = "default" // External player
PlaybackTypeExternalPlayerLink PlaybackType = "externalPlayerLink"
PlaybackTypeNativePlayer PlaybackType = "nativeplayer"
PlaybackTypeNone PlaybackType = "none"
PlaybackTypeNoneAndAwait PlaybackType = "noneAndAwait"
)
type StartStreamOptions struct {
MediaId int
EpisodeNumber int // RELATIVE Episode number to identify the file
AniDBEpisode string // Animap episode
AutoSelect bool // Automatically select the best file to stream
Torrent *hibiketorrent.AnimeTorrent // Selected torrent (Manual selection)
FileIndex *int // Index of the file to stream (Manual selection)
UserAgent string
ClientId string
PlaybackType PlaybackType
}
// StartStream is called by the client to start streaming a torrent
func (r *Repository) StartStream(ctx context.Context, opts *StartStreamOptions) (err error) {
defer util.HandlePanicInModuleWithError("torrentstream/stream/StartStream", &err)
// DEVNOTE: Do not
//r.Shutdown()
r.previousStreamOptions = mo.Some(opts)
r.logger.Info().
Str("clientId", opts.ClientId).
Any("playbackType", opts.PlaybackType).
Int("mediaId", opts.MediaId).Msgf("torrentstream: Starting stream for episode %s", opts.AniDBEpisode)
r.sendStateEvent(eventLoading)
r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
defer func() {
r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
}()
if opts.PlaybackType == PlaybackTypeNativePlayer {
r.directStreamManager.PrepareNewStream(opts.ClientId, "Selecting torrent...")
}
//
// Get the media info
//
media, _, err := r.GetMediaInfo(ctx, opts.MediaId)
if err != nil {
return err
}
episodeNumber := opts.EpisodeNumber
aniDbEpisode := strconv.Itoa(episodeNumber)
//
// Find the best torrent / Select the torrent
//
var torrentToStream *playbackTorrent
if opts.AutoSelect {
torrentToStream, err = r.findBestTorrent(media, aniDbEpisode, episodeNumber)
if err != nil {
r.sendStateEvent(eventLoadingFailed)
return err
}
} else {
if opts.Torrent == nil {
return fmt.Errorf("torrentstream: No torrent provided")
}
torrentToStream, err = r.findBestTorrentFromManualSelection(opts.Torrent, media, aniDbEpisode, opts.FileIndex)
if err != nil {
r.sendStateEvent(eventLoadingFailed)
return err
}
}
if torrentToStream == nil {
r.sendStateEvent(eventLoadingFailed)
return fmt.Errorf("torrentstream: No torrent selected")
}
//
// Set current file & torrent
//
r.client.currentFile = mo.Some(torrentToStream.File)
r.client.currentTorrent = mo.Some(torrentToStream.Torrent)
r.sendStateEvent(eventLoading, TLSStateSendingStreamToMediaPlayer)
go func() {
// Add the torrent to the history if it is a batch & manually selected
if len(r.client.currentTorrent.MustGet().Files()) > 1 && opts.Torrent != nil {
r.AddBatchHistory(opts.MediaId, opts.Torrent) // ran in goroutine
}
}()
//
// Start the playback
//
go func() {
switch opts.PlaybackType {
case PlaybackTypeNone:
r.logger.Warn().Msg("torrentstream: Playback type is set to 'none'")
// Signal to the client that the torrent has started playing (remove loading status)
// There will be no tracking
r.sendStateEvent(eventTorrentStartedPlaying)
case PlaybackTypeNoneAndAwait:
r.logger.Warn().Msg("torrentstream: Playback type is set to 'noneAndAwait'")
// Signal to the client that the torrent has started playing (remove loading status)
// There will be no tracking
for {
if r.client.readyToStream() {
break
}
time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
}
r.sendStateEvent(eventTorrentStartedPlaying)
//
// External player
//
case PlaybackTypeExternal, PlaybackTypeExternalPlayerLink:
r.sendStreamToExternalPlayer(opts, media, aniDbEpisode)
//
// Direct stream
//
case PlaybackTypeNativePlayer:
readyCh, err := r.directStreamManager.PlayTorrentStream(ctx, directstream.PlayTorrentStreamOptions{
ClientId: opts.ClientId,
EpisodeNumber: opts.EpisodeNumber,
AnidbEpisode: opts.AniDBEpisode,
Media: media.ToBaseAnime(),
Torrent: r.client.currentTorrent.MustGet(),
File: r.client.currentFile.MustGet(),
})
if err != nil {
r.logger.Error().Err(err).Msg("torrentstream: Failed to prepare new stream")
r.sendStateEvent(eventLoadingFailed)
return
}
if opts.PlaybackType == PlaybackTypeNativePlayer {
r.directStreamManager.PrepareNewStream(opts.ClientId, "Downloading metadata...")
}
// Make sure the client is ready and the torrent is partially downloaded
for {
if r.client.readyToStream() {
break
}
// If for some reason the torrent is dropped, we kill the goroutine
if r.client.torrentClient.IsAbsent() || r.client.currentTorrent.IsAbsent() {
return
}
r.logger.Debug().Msg("torrentstream: Waiting for playable threshold to be reached")
time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
}
close(readyCh)
}
}()
r.sendStateEvent(eventTorrentLoaded)
r.logger.Info().Msg("torrentstream: Stream started")
return nil
}
// sendStreamToExternalPlayer sends the stream to the desktop player or external player link.
// It blocks until the some pieces have been downloaded before sending the stream for faster playback.
func (r *Repository) sendStreamToExternalPlayer(opts *StartStreamOptions, completeAnime *anilist.CompleteAnime, aniDbEpisode string) {
baseAnime := completeAnime.ToBaseAnime()
r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
defer func() {
r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
}()
// Make sure the client is ready and the torrent is partially downloaded
for {
if r.client.readyToStream() {
break
}
// If for some reason the torrent is dropped, we kill the goroutine
if r.client.torrentClient.IsAbsent() || r.client.currentTorrent.IsAbsent() {
return
}
r.logger.Debug().Msg("torrentstream: Waiting for playable threshold to be reached")
time.Sleep(3 * time.Second) // Wait for 3 secs before checking again
}
event := &TorrentStreamSendStreamToMediaPlayerEvent{
WindowTitle: "",
StreamURL: r.client.GetStreamingUrl(),
Media: baseAnime,
AniDbEpisode: aniDbEpisode,
PlaybackType: string(opts.PlaybackType),
}
err := hook.GlobalHookManager.OnTorrentStreamSendStreamToMediaPlayer().Trigger(event)
if err != nil {
r.logger.Error().Err(err).Msg("torrentstream: Failed to trigger hook")
return
}
windowTitle := event.WindowTitle
streamURL := event.StreamURL
baseAnime = event.Media
aniDbEpisode = event.AniDbEpisode
playbackType := PlaybackType(event.PlaybackType)
if event.DefaultPrevented {
r.logger.Debug().Msg("torrentstream: Stream prevented by hook")
return
}
switch playbackType {
//
// Desktop player
//
case PlaybackTypeExternal:
r.logger.Debug().Msgf("torrentstream: Starting the media player %s", streamURL)
err = r.playbackManager.StartStreamingUsingMediaPlayer(windowTitle, &playbackmanager.StartPlayingOptions{
Payload: streamURL,
UserAgent: opts.UserAgent,
ClientId: opts.ClientId,
}, baseAnime, aniDbEpisode)
if err != nil {
// Failed to start the stream, we'll drop the torrents and stop the server
r.sendStateEvent(eventLoadingFailed)
_ = r.StopStream()
r.logger.Error().Err(err).Msg("torrentstream: Failed to start the stream")
r.wsEventManager.SendEventTo(opts.ClientId, events.ErrorToast, err.Error())
}
r.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "torrentstream")
defer func() {
r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
}()
r.playbackManager.RegisterMediaPlayerCallback(func(event playbackmanager.PlaybackEvent, cancelFunc func()) {
switch event.(type) {
case playbackmanager.StreamStartedEvent:
r.logger.Debug().Msg("torrentstream: Media player started playing")
r.wsEventManager.SendEvent(events.HideIndefiniteLoader, "torrentstream")
cancelFunc()
}
})
//
// External player link
//
case PlaybackTypeExternalPlayerLink:
r.logger.Debug().Msgf("torrentstream: Sending stream to external player %s", streamURL)
r.wsEventManager.SendEventTo(opts.ClientId, events.ExternalPlayerOpenURL, struct {
Url string `json:"url"`
MediaId int `json:"mediaId"`
EpisodeNumber int `json:"episodeNumber"`
MediaTitle string `json:"mediaTitle"`
}{
Url: r.client.GetExternalPlayerStreamingUrl(),
MediaId: opts.MediaId,
EpisodeNumber: opts.EpisodeNumber,
MediaTitle: baseAnime.GetPreferredTitle(),
})
// Signal to the client that the torrent has started playing (remove loading status)
// We can't know for sure
r.sendStateEvent(eventTorrentStartedPlaying)
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
type StartUntrackedStreamOptions struct {
Magnet string
FileIndex int
WindowTitle string
UserAgent string
ClientId string
PlaybackType PlaybackType
}
func (r *Repository) StopStream() error {
defer func() {
if r := recover(); r != nil {
}
}()
r.logger.Info().Msg("torrentstream: Stopping stream")
// Stop the client
// This will stop the stream and close the server
// This also sends the eventTorrentStopped event
r.client.mu.Lock()
//r.client.stopCh = make(chan struct{})
r.client.repository.logger.Debug().Msg("torrentstream: Handling media player stopped event")
// This is to prevent the client from downloading the whole torrent when the user stops watching
// Also, the torrent might be a batch - so we don't want to download the whole thing
if r.client.currentTorrent.IsPresent() {
if r.client.currentTorrentStatus.ProgressPercentage < 70 {
r.client.repository.logger.Debug().Msg("torrentstream: Dropping torrent, completion is less than 70%")
r.client.dropTorrents()
}
r.client.repository.logger.Debug().Msg("torrentstream: Resetting current torrent and status")
}
r.client.currentTorrent = mo.None[*torrent.Torrent]() // Reset the current torrent
r.client.currentFile = mo.None[*torrent.File]() // Reset the current file
r.client.currentTorrentStatus = TorrentStatus{} // Reset the torrent status
r.client.repository.sendStateEvent(eventTorrentStopped, nil) // Send torrent stopped event
r.client.repository.mediaPlayerRepository.Stop() // Stop the media player gracefully if it's running
r.client.mu.Unlock()
go func() {
r.nativePlayer.Stop()
}()
r.logger.Info().Msg("torrentstream: Stream stopped")
return nil
}
func (r *Repository) DropTorrent() error {
r.logger.Info().Msg("torrentstream: Dropping last torrent")
if r.client.torrentClient.IsAbsent() {
return nil
}
for _, t := range r.client.torrentClient.MustGet().Torrents() {
t.Drop()
}
r.mediaPlayerRepository.Stop()
r.logger.Info().Msg("torrentstream: Dropped last torrent")
return nil
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
func (r *Repository) GetMediaInfo(ctx context.Context, mediaId int) (media *anilist.CompleteAnime, animeMetadata *metadata.AnimeMetadata, err error) {
// Get the media
var found bool
media, found = r.completeAnimeCache.Get(mediaId)
if !found {
// Fetch the media
media, err = r.platform.GetAnimeWithRelations(ctx, mediaId)
if err != nil {
return nil, nil, fmt.Errorf("torrentstream: Failed to fetch media: %w", err)
}
}
// Get the media
animeMetadata, err = r.metadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mediaId)
if err != nil {
//return nil, nil, fmt.Errorf("torrentstream: Could not fetch AniDB media: %w", err)
animeMetadata = &metadata.AnimeMetadata{
Titles: make(map[string]string),
Episodes: make(map[string]*metadata.EpisodeMetadata),
EpisodeCount: 0,
SpecialCount: 0,
Mappings: &metadata.AnimeMappings{
AnilistId: media.GetID(),
},
}
animeMetadata.Titles["en"] = media.GetTitleSafe()
animeMetadata.Titles["x-jat"] = media.GetRomajiTitleSafe()
err = nil
}
return
}