node build fixed
This commit is contained in:
391
seanime-2.9.10/internal/torrentstream/stream.go
Normal file
391
seanime-2.9.10/internal/torrentstream/stream.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user