node build fixed
This commit is contained in:
489
seanime-2.9.10/internal/torrentstream/client.go
Normal file
489
seanime-2.9.10/internal/torrentstream/client.go
Normal file
@@ -0,0 +1,489 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"seanime/internal/mediaplayers/mediaplayer"
|
||||
"seanime/internal/util"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
alog "github.com/anacrolix/log"
|
||||
"github.com/anacrolix/torrent"
|
||||
"github.com/anacrolix/torrent/storage"
|
||||
"github.com/samber/mo"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
type (
|
||||
Client struct {
|
||||
repository *Repository
|
||||
|
||||
torrentClient mo.Option[*torrent.Client]
|
||||
currentTorrent mo.Option[*torrent.Torrent]
|
||||
currentFile mo.Option[*torrent.File]
|
||||
currentTorrentStatus TorrentStatus
|
||||
cancelFunc context.CancelFunc
|
||||
|
||||
mu sync.Mutex
|
||||
stopCh chan struct{} // Closed when the media player stops
|
||||
mediaPlayerPlaybackStatusCh chan *mediaplayer.PlaybackStatus // Continuously receives playback status
|
||||
timeSinceLoggedSeeding time.Time
|
||||
lastSpeedCheck time.Time // Track the last time we checked speeds
|
||||
lastBytesCompleted int64 // Track the last bytes completed
|
||||
lastBytesWrittenData int64 // Track the last bytes written data
|
||||
}
|
||||
|
||||
TorrentStatus struct {
|
||||
UploadProgress int64 `json:"uploadProgress"`
|
||||
DownloadProgress int64 `json:"downloadProgress"`
|
||||
ProgressPercentage float64 `json:"progressPercentage"`
|
||||
DownloadSpeed string `json:"downloadSpeed"`
|
||||
UploadSpeed string `json:"uploadSpeed"`
|
||||
Size string `json:"size"`
|
||||
Seeders int `json:"seeders"`
|
||||
}
|
||||
|
||||
NewClientOptions struct {
|
||||
Repository *Repository
|
||||
}
|
||||
)
|
||||
|
||||
func NewClient(repository *Repository) *Client {
|
||||
ret := &Client{
|
||||
repository: repository,
|
||||
torrentClient: mo.None[*torrent.Client](),
|
||||
currentFile: mo.None[*torrent.File](),
|
||||
currentTorrent: mo.None[*torrent.Torrent](),
|
||||
stopCh: make(chan struct{}),
|
||||
mediaPlayerPlaybackStatusCh: make(chan *mediaplayer.PlaybackStatus, 1),
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// initializeClient will create and torrent client.
|
||||
// The client is designed to support only one torrent at a time, and seed it.
|
||||
// Upon initialization, the client will drop all torrents.
|
||||
func (c *Client) initializeClient() error {
|
||||
// Fail if no settings
|
||||
if err := c.repository.FailIfNoSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cancel the previous context, terminating the goroutine if it's running
|
||||
if c.cancelFunc != nil {
|
||||
c.cancelFunc()
|
||||
}
|
||||
|
||||
// Context for the client's goroutine
|
||||
var ctx context.Context
|
||||
ctx, c.cancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
// Get the settings
|
||||
settings := c.repository.settings.MustGet()
|
||||
|
||||
// Define torrent client settings
|
||||
cfg := torrent.NewDefaultClientConfig()
|
||||
cfg.Seed = true
|
||||
cfg.DisableIPv6 = settings.DisableIPV6
|
||||
cfg.Logger = alog.Logger{}
|
||||
|
||||
// TEST ONLY: Limit download speed to 1mb/s
|
||||
// cfg.DownloadRateLimiter = rate.NewLimiter(rate.Limit(1<<20), 1<<20)
|
||||
|
||||
if settings.SlowSeeding {
|
||||
cfg.DialRateLimiter = rate.NewLimiter(rate.Limit(1), 1)
|
||||
cfg.UploadRateLimiter = rate.NewLimiter(rate.Limit(1<<20), 2<<20)
|
||||
}
|
||||
|
||||
if settings.TorrentClientHost != "" {
|
||||
cfg.ListenHost = func(network string) string { return settings.TorrentClientHost }
|
||||
}
|
||||
|
||||
if settings.TorrentClientPort == 0 {
|
||||
settings.TorrentClientPort = 43213
|
||||
}
|
||||
cfg.ListenPort = settings.TorrentClientPort
|
||||
// Set the download directory
|
||||
// e.g. /path/to/temp/seanime/torrentstream/{infohash}
|
||||
cfg.DefaultStorage = storage.NewFileByInfoHash(settings.DownloadDir)
|
||||
|
||||
c.mu.Lock()
|
||||
// Create the torrent client
|
||||
client, err := torrent.NewClient(cfg)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
return fmt.Errorf("error creating a new torrent client: %v", err)
|
||||
}
|
||||
c.repository.logger.Info().Msgf("torrentstream: Initialized torrent client on port %d", settings.TorrentClientPort)
|
||||
c.torrentClient = mo.Some(client)
|
||||
c.dropTorrents()
|
||||
c.mu.Unlock()
|
||||
|
||||
go func(ctx context.Context) {
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
c.repository.logger.Debug().Msg("torrentstream: Context cancelled, stopping torrent client")
|
||||
return
|
||||
|
||||
case status := <-c.mediaPlayerPlaybackStatusCh:
|
||||
// DEVNOTE: When this is received, "default" case is executed right after
|
||||
if status != nil && c.currentFile.IsPresent() && c.repository.playback.currentVideoDuration == 0 {
|
||||
// If the stored video duration is 0 but the media player status shows a duration that is not 0
|
||||
// we know that the video has been loaded and is playing
|
||||
if c.repository.playback.currentVideoDuration == 0 && status.Duration > 0 {
|
||||
// The media player has started playing the video
|
||||
c.repository.logger.Debug().Msg("torrentstream: Media player started playing the video, sending event")
|
||||
c.repository.sendStateEvent(eventTorrentStartedPlaying)
|
||||
// Update the stored video duration
|
||||
c.repository.playback.currentVideoDuration = status.Duration
|
||||
}
|
||||
}
|
||||
default:
|
||||
c.mu.Lock()
|
||||
if c.torrentClient.IsPresent() && c.currentTorrent.IsPresent() && c.currentFile.IsPresent() {
|
||||
t := c.currentTorrent.MustGet()
|
||||
f := c.currentFile.MustGet()
|
||||
|
||||
// Get the current time
|
||||
now := time.Now()
|
||||
elapsed := now.Sub(c.lastSpeedCheck).Seconds()
|
||||
|
||||
// downloadProgress is the number of bytes downloaded
|
||||
downloadProgress := t.BytesCompleted()
|
||||
|
||||
downloadSpeed := ""
|
||||
if elapsed > 0 {
|
||||
bytesPerSecond := float64(downloadProgress-c.lastBytesCompleted) / elapsed
|
||||
if bytesPerSecond > 0 {
|
||||
downloadSpeed = fmt.Sprintf("%s/s", util.Bytes(uint64(bytesPerSecond)))
|
||||
}
|
||||
}
|
||||
size := util.Bytes(uint64(f.Length()))
|
||||
|
||||
bytesWrittenData := t.Stats().BytesWrittenData
|
||||
uploadSpeed := ""
|
||||
if elapsed > 0 {
|
||||
bytesPerSecond := float64((&bytesWrittenData).Int64()-c.lastBytesWrittenData) / elapsed
|
||||
if bytesPerSecond > 0 {
|
||||
uploadSpeed = fmt.Sprintf("%s/s", util.Bytes(uint64(bytesPerSecond)))
|
||||
}
|
||||
}
|
||||
|
||||
// Update the stored values for next calculation
|
||||
c.lastBytesCompleted = downloadProgress
|
||||
c.lastBytesWrittenData = (&bytesWrittenData).Int64()
|
||||
c.lastSpeedCheck = now
|
||||
|
||||
if t.PeerConns() != nil {
|
||||
c.currentTorrentStatus.Seeders = len(t.PeerConns())
|
||||
}
|
||||
|
||||
c.currentTorrentStatus = TorrentStatus{
|
||||
Size: size,
|
||||
UploadProgress: (&bytesWrittenData).Int64() - c.currentTorrentStatus.UploadProgress,
|
||||
DownloadSpeed: downloadSpeed,
|
||||
UploadSpeed: uploadSpeed,
|
||||
DownloadProgress: downloadProgress,
|
||||
ProgressPercentage: c.getTorrentPercentage(c.currentTorrent, c.currentFile),
|
||||
Seeders: t.Stats().ConnectedSeeders,
|
||||
}
|
||||
c.repository.sendStateEvent(eventTorrentStatus, c.currentTorrentStatus)
|
||||
// Always log the progress so the user knows what's happening
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Progress: %.2f%%, Download speed: %s, Upload speed: %s, Size: %s",
|
||||
c.currentTorrentStatus.ProgressPercentage,
|
||||
c.currentTorrentStatus.DownloadSpeed,
|
||||
c.currentTorrentStatus.UploadSpeed,
|
||||
c.currentTorrentStatus.Size)
|
||||
c.timeSinceLoggedSeeding = time.Now()
|
||||
}
|
||||
c.mu.Unlock()
|
||||
if c.torrentClient.IsPresent() {
|
||||
if time.Since(c.timeSinceLoggedSeeding) > 20*time.Second {
|
||||
c.timeSinceLoggedSeeding = time.Now()
|
||||
for _, t := range c.torrentClient.MustGet().Torrents() {
|
||||
if t.Seeding() {
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Seeding last torrent, %d peers", t.Stats().ActivePeers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) GetStreamingUrl() string {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return ""
|
||||
}
|
||||
if c.currentFile.IsAbsent() {
|
||||
return ""
|
||||
}
|
||||
settings, ok := c.repository.settings.Get()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
host := settings.Host
|
||||
if host == "0.0.0.0" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
address := fmt.Sprintf("%s:%d", host, settings.Port)
|
||||
if settings.StreamUrlAddress != "" {
|
||||
address = settings.StreamUrlAddress
|
||||
}
|
||||
ret := fmt.Sprintf("http://%s/api/v1/torrentstream/stream/%s", address, url.PathEscape(c.currentFile.MustGet().DisplayPath()))
|
||||
if strings.HasPrefix(ret, "http://http") {
|
||||
ret = strings.Replace(ret, "http://http", "http", 1)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Client) GetExternalPlayerStreamingUrl() string {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return ""
|
||||
}
|
||||
if c.currentFile.IsAbsent() {
|
||||
return ""
|
||||
}
|
||||
|
||||
ret := fmt.Sprintf("{{SCHEME}}://{{HOST}}/api/v1/torrentstream/stream/%s", url.PathEscape(c.currentFile.MustGet().DisplayPath()))
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c *Client) AddTorrent(id string) (*torrent.Torrent, error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return nil, errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
// Drop all torrents
|
||||
for _, t := range c.torrentClient.MustGet().Torrents() {
|
||||
t.Drop()
|
||||
}
|
||||
|
||||
if strings.HasPrefix(id, "magnet") {
|
||||
return c.addTorrentMagnet(id)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(id, "http") {
|
||||
return c.addTorrentFromDownloadURL(id)
|
||||
}
|
||||
|
||||
return c.addTorrentFromFile(id)
|
||||
}
|
||||
|
||||
func (c *Client) addTorrentMagnet(magnet string) (*torrent.Torrent, error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return nil, errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
t, err := c.torrentClient.MustGet().AddMagnet(magnet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Waiting to retrieve torrent info")
|
||||
select {
|
||||
case <-t.GotInfo():
|
||||
break
|
||||
case <-t.Closed():
|
||||
//t.Drop()
|
||||
return nil, errors.New("torrent closed")
|
||||
case <-time.After(1 * time.Minute):
|
||||
t.Drop()
|
||||
return nil, errors.New("timeout waiting for torrent info")
|
||||
}
|
||||
c.repository.logger.Info().Msgf("torrentstream: Torrent added: %s", t.InfoHash().AsString())
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *Client) addTorrentFromFile(fp string) (*torrent.Torrent, error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return nil, errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
t, err := c.torrentClient.MustGet().AddTorrentFromFile(fp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Waiting to retrieve torrent info")
|
||||
<-t.GotInfo()
|
||||
c.repository.logger.Info().Msgf("torrentstream: Torrent added: %s", t.InfoHash().AsString())
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *Client) addTorrentFromDownloadURL(url string) (*torrent.Torrent, error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return nil, errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
filename := path.Base(url)
|
||||
file, err := os.Create(path.Join(os.TempDir(), filename))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(file, resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t, err := c.torrentClient.MustGet().AddTorrentFromFile(file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Waiting to retrieve torrent info")
|
||||
select {
|
||||
case <-t.GotInfo():
|
||||
break
|
||||
case <-t.Closed():
|
||||
t.Drop()
|
||||
return nil, errors.New("torrent closed")
|
||||
case <-time.After(1 * time.Minute):
|
||||
t.Drop()
|
||||
return nil, errors.New("timeout waiting for torrent info")
|
||||
}
|
||||
c.repository.logger.Info().Msgf("torrentstream: Added torrent: %s", t.InfoHash().AsString())
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Shutdown closes the torrent client and drops all torrents.
|
||||
// This SHOULD NOT be called if you don't intend to reinitialize the client.
|
||||
func (c *Client) Shutdown() (errs []error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return
|
||||
}
|
||||
c.dropTorrents()
|
||||
c.currentTorrent = mo.None[*torrent.Torrent]()
|
||||
c.currentTorrentStatus = TorrentStatus{}
|
||||
c.repository.logger.Debug().Msg("torrentstream: Closing torrent client")
|
||||
return c.torrentClient.MustGet().Close()
|
||||
}
|
||||
|
||||
func (c *Client) FindTorrent(infoHash string) (*torrent.Torrent, error) {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return nil, errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
torrents := c.torrentClient.MustGet().Torrents()
|
||||
for _, t := range torrents {
|
||||
if t.InfoHash().AsString() == infoHash {
|
||||
c.repository.logger.Debug().Msgf("torrentstream: Found torrent: %s", infoHash)
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no torrent found")
|
||||
}
|
||||
|
||||
func (c *Client) RemoveTorrent(infoHash string) error {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return errors.New("torrent client is not initialized")
|
||||
}
|
||||
|
||||
c.repository.logger.Trace().Msgf("torrentstream: Removing torrent: %s", infoHash)
|
||||
|
||||
torrents := c.torrentClient.MustGet().Torrents()
|
||||
for _, t := range torrents {
|
||||
if t.InfoHash().AsString() == infoHash {
|
||||
t.Drop()
|
||||
c.repository.logger.Debug().Msgf("torrentstream: Removed torrent: %s", infoHash)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("no torrent found")
|
||||
}
|
||||
|
||||
func (c *Client) dropTorrents() {
|
||||
if c.torrentClient.IsAbsent() {
|
||||
return
|
||||
}
|
||||
c.repository.logger.Trace().Msg("torrentstream: Dropping all torrents")
|
||||
|
||||
for _, t := range c.torrentClient.MustGet().Torrents() {
|
||||
t.Drop()
|
||||
}
|
||||
|
||||
if c.repository.settings.IsPresent() {
|
||||
// Delete all torrents
|
||||
fe, err := os.ReadDir(c.repository.settings.MustGet().DownloadDir)
|
||||
if err == nil {
|
||||
for _, f := range fe {
|
||||
if f.IsDir() {
|
||||
_ = os.RemoveAll(path.Join(c.repository.settings.MustGet().DownloadDir, f.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.repository.logger.Debug().Msg("torrentstream: Dropped all torrents")
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// getTorrentPercentage returns the percentage of the current torrent file
|
||||
// If no torrent is selected, it returns -1
|
||||
func (c *Client) getTorrentPercentage(t mo.Option[*torrent.Torrent], f mo.Option[*torrent.File]) float64 {
|
||||
if t.IsAbsent() || f.IsAbsent() {
|
||||
return -1
|
||||
}
|
||||
|
||||
if f.MustGet().Length() == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
return float64(f.MustGet().BytesCompleted()) / float64(f.MustGet().Length()) * 100
|
||||
}
|
||||
|
||||
// readyToStream determines if enough of the file has been downloaded to begin streaming
|
||||
// Uses both absolute size (minimum buffer) and a percentage-based approach
|
||||
func (c *Client) readyToStream() bool {
|
||||
if c.currentTorrent.IsAbsent() || c.currentFile.IsAbsent() {
|
||||
return false
|
||||
}
|
||||
|
||||
file := c.currentFile.MustGet()
|
||||
|
||||
// Always need at least 1MB to start playback (typical header size for many formats)
|
||||
const minimumBufferBytes int64 = 1 * 1024 * 1024 // 1MB
|
||||
|
||||
// For large files, use a smaller percentage
|
||||
var percentThreshold float64
|
||||
fileSize := file.Length()
|
||||
|
||||
switch {
|
||||
case fileSize > 5*1024*1024*1024: // > 5GB
|
||||
percentThreshold = 0.1 // 0.1% for very large files
|
||||
case fileSize > 1024*1024*1024: // > 1GB
|
||||
percentThreshold = 0.5 // 0.5% for large files
|
||||
default:
|
||||
percentThreshold = 0.5 // 0.5% for smaller files
|
||||
}
|
||||
|
||||
bytesCompleted := file.BytesCompleted()
|
||||
percentCompleted := float64(bytesCompleted) / float64(fileSize) * 100
|
||||
|
||||
// Ready when both minimum buffer is met AND percentage threshold is reached
|
||||
return bytesCompleted >= minimumBufferBytes && percentCompleted >= percentThreshold
|
||||
}
|
||||
235
seanime-2.9.10/internal/torrentstream/collection.go
Normal file
235
seanime-2.9.10/internal/torrentstream/collection.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/hook"
|
||||
"seanime/internal/library/anime"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type (
|
||||
// StreamCollection is used to "complete" the anime.LibraryCollection if the user chooses
|
||||
// to include torrent streams in the library view.
|
||||
StreamCollection struct {
|
||||
ContinueWatchingList []*anime.Episode `json:"continueWatchingList"`
|
||||
Anime []*anilist.BaseAnime `json:"anime"`
|
||||
ListData map[int]*anime.EntryListData `json:"listData"`
|
||||
}
|
||||
|
||||
HydrateStreamCollectionOptions struct {
|
||||
AnimeCollection *anilist.AnimeCollection
|
||||
LibraryCollection *anime.LibraryCollection
|
||||
MetadataProvider metadata.Provider
|
||||
}
|
||||
)
|
||||
|
||||
func (r *Repository) HydrateStreamCollection(opts *HydrateStreamCollectionOptions) {
|
||||
|
||||
reqEvent := new(anime.AnimeLibraryStreamCollectionRequestedEvent)
|
||||
reqEvent.AnimeCollection = opts.AnimeCollection
|
||||
reqEvent.LibraryCollection = opts.LibraryCollection
|
||||
err := hook.GlobalHookManager.OnAnimeLibraryStreamCollectionRequested().Trigger(reqEvent)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opts.AnimeCollection = reqEvent.AnimeCollection
|
||||
opts.LibraryCollection = reqEvent.LibraryCollection
|
||||
|
||||
lists := opts.AnimeCollection.MediaListCollection.GetLists()
|
||||
// Get the anime that are currently being watched
|
||||
var currentlyWatching *anilist.AnimeCollection_MediaListCollection_Lists
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.Status == anilist.MediaListStatusCurrent {
|
||||
//currentlyWatching = list.CopyT()
|
||||
currentlyWatching = &anilist.AnimeCollection_MediaListCollection_Lists{
|
||||
Status: lo.ToPtr(anilist.MediaListStatusCurrent),
|
||||
Name: lo.ToPtr("CURRENT"),
|
||||
IsCustomList: lo.ToPtr(false),
|
||||
Entries: list.Entries,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, list := range lists {
|
||||
if list.Status == nil {
|
||||
continue
|
||||
}
|
||||
if *list.Status == anilist.MediaListStatusRepeating {
|
||||
if currentlyWatching == nil {
|
||||
currentlyWatching = list
|
||||
} else {
|
||||
for _, entry := range list.Entries {
|
||||
currentlyWatching.Entries = append(currentlyWatching.Entries, entry)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if currentlyWatching == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ret := &StreamCollection{
|
||||
ContinueWatchingList: make([]*anime.Episode, 0),
|
||||
Anime: make([]*anilist.BaseAnime, 0),
|
||||
ListData: make(map[int]*anime.EntryListData),
|
||||
}
|
||||
|
||||
visitedMediaIds := make(map[int]struct{})
|
||||
|
||||
animeAdded := make(map[int]*anilist.AnimeListEntry)
|
||||
|
||||
// Go through each entry in the currently watching list
|
||||
wg := sync.WaitGroup{}
|
||||
mu := sync.Mutex{}
|
||||
wg.Add(len(currentlyWatching.Entries))
|
||||
for _, entry := range currentlyWatching.Entries {
|
||||
go func(entry *anilist.AnimeListEntry) {
|
||||
defer wg.Done()
|
||||
|
||||
mu.Lock()
|
||||
if _, found := visitedMediaIds[entry.GetMedia().GetID()]; found {
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
// Get the next episode to watch
|
||||
// i.e. if the user has watched episode 1, the next episode to watch is 2
|
||||
nextEpisodeToWatch := entry.GetProgressSafe() + 1
|
||||
if nextEpisodeToWatch > entry.GetMedia().GetCurrentEpisodeCount() {
|
||||
mu.Unlock()
|
||||
return // Skip this entry if the user has watched all episodes
|
||||
}
|
||||
mediaId := entry.GetMedia().GetID()
|
||||
visitedMediaIds[mediaId] = struct{}{}
|
||||
mu.Unlock()
|
||||
// Check if the anime's "next episode to watch" is already in the library collection
|
||||
// If it is, we don't need to add it to the stream collection
|
||||
for _, libraryEp := range opts.LibraryCollection.ContinueWatchingList {
|
||||
if libraryEp.BaseAnime.ID == mediaId && libraryEp.GetProgressNumber() == nextEpisodeToWatch {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if *entry.GetMedia().GetStatus() == anilist.MediaStatusNotYetReleased {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the media info
|
||||
animeMetadata, err := opts.MetadataProvider.GetAnimeMetadata(metadata.AnilistPlatform, mediaId)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msg("torrentstream: could not fetch AniDB media")
|
||||
return
|
||||
}
|
||||
|
||||
_, found := animeMetadata.FindEpisode(strconv.Itoa(nextEpisodeToWatch))
|
||||
//if !found {
|
||||
// r.logger.Error().Msg("torrentstream: could not find episode in AniDB")
|
||||
// return
|
||||
//}
|
||||
|
||||
progressOffset := 0
|
||||
anidbEpisode := strconv.Itoa(nextEpisodeToWatch)
|
||||
if anime.FindDiscrepancy(entry.GetMedia(), animeMetadata) == anime.DiscrepancyAniListCountsEpisodeZero {
|
||||
progressOffset = 1
|
||||
if nextEpisodeToWatch == 1 {
|
||||
anidbEpisode = "S1"
|
||||
}
|
||||
}
|
||||
|
||||
// Add the anime & episode
|
||||
episode := anime.NewEpisode(&anime.NewEpisodeOptions{
|
||||
LocalFile: nil,
|
||||
OptionalAniDBEpisode: anidbEpisode,
|
||||
AnimeMetadata: animeMetadata,
|
||||
Media: entry.GetMedia(),
|
||||
ProgressOffset: progressOffset,
|
||||
IsDownloaded: false,
|
||||
MetadataProvider: r.metadataProvider,
|
||||
})
|
||||
if !found {
|
||||
episode.EpisodeTitle = entry.GetMedia().GetPreferredTitle()
|
||||
episode.DisplayTitle = fmt.Sprintf("Episode %d", nextEpisodeToWatch)
|
||||
episode.ProgressNumber = nextEpisodeToWatch
|
||||
episode.EpisodeNumber = nextEpisodeToWatch
|
||||
episode.EpisodeMetadata = &anime.EpisodeMetadata{
|
||||
Image: entry.GetMedia().GetBannerImageSafe(),
|
||||
}
|
||||
}
|
||||
|
||||
if episode == nil {
|
||||
r.logger.Error().Msg("torrentstream: could not get anime entry episode")
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
ret.ContinueWatchingList = append(ret.ContinueWatchingList, episode)
|
||||
animeAdded[mediaId] = entry
|
||||
mu.Unlock()
|
||||
}(entry)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
libraryAnimeMap := make(map[int]struct{})
|
||||
|
||||
// Remove anime that are already in the library collection
|
||||
for _, list := range opts.LibraryCollection.Lists {
|
||||
if list.Status == anilist.MediaListStatusCurrent {
|
||||
for _, entry := range list.Entries {
|
||||
libraryAnimeMap[entry.MediaId] = struct{}{}
|
||||
if _, found := animeAdded[entry.MediaId]; found {
|
||||
delete(animeAdded, entry.MediaId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, entry := range currentlyWatching.Entries {
|
||||
if _, found := libraryAnimeMap[entry.GetMedia().GetID()]; found {
|
||||
continue
|
||||
}
|
||||
if *entry.GetMedia().GetStatus() == anilist.MediaStatusNotYetReleased {
|
||||
continue
|
||||
}
|
||||
animeAdded[entry.GetMedia().GetID()] = entry
|
||||
}
|
||||
|
||||
for _, a := range animeAdded {
|
||||
ret.Anime = append(ret.Anime, a.GetMedia())
|
||||
ret.ListData[a.GetMedia().GetID()] = &anime.EntryListData{
|
||||
Progress: a.GetProgressSafe(),
|
||||
Score: a.GetScoreSafe(),
|
||||
Status: a.GetStatus(),
|
||||
Repeat: a.GetRepeatSafe(),
|
||||
StartedAt: anilist.FuzzyDateToString(a.StartedAt),
|
||||
CompletedAt: anilist.FuzzyDateToString(a.CompletedAt),
|
||||
}
|
||||
}
|
||||
|
||||
if len(ret.ContinueWatchingList) == 0 && len(ret.Anime) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
sc := &anime.StreamCollection{
|
||||
ContinueWatchingList: ret.ContinueWatchingList,
|
||||
Anime: ret.Anime,
|
||||
ListData: ret.ListData,
|
||||
}
|
||||
|
||||
event := new(anime.AnimeLibraryStreamCollectionEvent)
|
||||
event.StreamCollection = sc
|
||||
err = hook.GlobalHookManager.OnAnimeLibraryStreamCollection().Trigger(event)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
opts.LibraryCollection.Stream = event.StreamCollection
|
||||
}
|
||||
82
seanime-2.9.10/internal/torrentstream/collection_test.go
Normal file
82
seanime-2.9.10/internal/torrentstream/collection_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/events"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/platforms/anilist_platform"
|
||||
"seanime/internal/test_utils"
|
||||
"seanime/internal/util"
|
||||
"testing"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStreamCollection(t *testing.T) {
|
||||
t.Skip("Incomplete")
|
||||
test_utils.SetTwoLevelDeep()
|
||||
test_utils.InitTestProvider(t, test_utils.Anilist())
|
||||
|
||||
logger := util.NewLogger()
|
||||
metadataProvider := metadata.GetMockProvider(t)
|
||||
anilistClient := anilist.TestGetMockAnilistClient()
|
||||
anilistPlatform := anilist_platform.NewAnilistPlatform(anilistClient, logger)
|
||||
anilistPlatform.SetUsername(test_utils.ConfigData.Provider.AnilistUsername)
|
||||
animeCollection, err := anilistPlatform.GetAnimeCollection(t.Context(), false)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, animeCollection)
|
||||
|
||||
repo := NewRepository(&NewRepositoryOptions{
|
||||
Logger: logger,
|
||||
BaseAnimeCache: anilist.NewBaseAnimeCache(),
|
||||
CompleteAnimeCache: anilist.NewCompleteAnimeCache(),
|
||||
Platform: anilistPlatform,
|
||||
MetadataProvider: metadataProvider,
|
||||
WSEventManager: events.NewMockWSEventManager(logger),
|
||||
TorrentRepository: nil,
|
||||
PlaybackManager: nil,
|
||||
Database: nil,
|
||||
})
|
||||
|
||||
// Mock Anilist collection and local files
|
||||
// User is currently watching Sousou no Frieren and One Piece
|
||||
lfs := make([]*anime.LocalFile, 0)
|
||||
|
||||
// Sousou no Frieren
|
||||
// 7 episodes downloaded, 4 watched
|
||||
mediaId := 154587
|
||||
lfs = append(lfs, anime.MockHydratedLocalFiles(
|
||||
anime.MockGenerateHydratedLocalFileGroupOptions("E:/Anime", "E:\\Anime\\Sousou no Frieren\\[SubsPlease] Sousou no Frieren - %ep (1080p) [F02B9CEE].mkv", mediaId, []anime.MockHydratedLocalFileWrapperOptionsMetadata{
|
||||
{MetadataEpisode: 1, MetadataAniDbEpisode: "1", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 2, MetadataAniDbEpisode: "2", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 3, MetadataAniDbEpisode: "3", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 4, MetadataAniDbEpisode: "4", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 5, MetadataAniDbEpisode: "5", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 6, MetadataAniDbEpisode: "6", MetadataType: anime.LocalFileTypeMain},
|
||||
{MetadataEpisode: 7, MetadataAniDbEpisode: "7", MetadataType: anime.LocalFileTypeMain},
|
||||
}),
|
||||
)...)
|
||||
anilist.TestModifyAnimeCollectionEntry(animeCollection, mediaId, anilist.TestModifyAnimeCollectionEntryInput{
|
||||
Status: lo.ToPtr(anilist.MediaListStatusCurrent),
|
||||
Progress: lo.ToPtr(4), // Mock progress
|
||||
})
|
||||
|
||||
libraryCollection, err := anime.NewLibraryCollection(t.Context(), &anime.NewLibraryCollectionOptions{
|
||||
AnimeCollection: animeCollection,
|
||||
LocalFiles: lfs,
|
||||
Platform: anilistPlatform,
|
||||
MetadataProvider: metadataProvider,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the stream collection
|
||||
repo.HydrateStreamCollection(&HydrateStreamCollectionOptions{
|
||||
AnimeCollection: animeCollection,
|
||||
LibraryCollection: libraryCollection,
|
||||
})
|
||||
spew.Dump(libraryCollection)
|
||||
|
||||
}
|
||||
50
seanime-2.9.10/internal/torrentstream/events.go
Normal file
50
seanime-2.9.10/internal/torrentstream/events.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package torrentstream
|
||||
|
||||
import "seanime/internal/events"
|
||||
|
||||
const (
|
||||
eventLoading = "loading"
|
||||
eventLoadingFailed = "loading-failed"
|
||||
eventTorrentLoaded = "loaded"
|
||||
eventTorrentStartedPlaying = "started-playing"
|
||||
eventTorrentStatus = "status"
|
||||
eventTorrentStopped = "stopped"
|
||||
)
|
||||
|
||||
type TorrentLoadingStatusState string
|
||||
|
||||
const (
|
||||
TLSStateLoading TorrentLoadingStatusState = "LOADING"
|
||||
TLSStateSearchingTorrents TorrentLoadingStatusState = "SEARCHING_TORRENTS"
|
||||
TLSStateCheckingTorrent TorrentLoadingStatusState = "CHECKING_TORRENT"
|
||||
TLSStateAddingTorrent TorrentLoadingStatusState = "ADDING_TORRENT"
|
||||
TLSStateSelectingFile TorrentLoadingStatusState = "SELECTING_FILE"
|
||||
TLSStateStartingServer TorrentLoadingStatusState = "STARTING_SERVER"
|
||||
TLSStateSendingStreamToMediaPlayer TorrentLoadingStatusState = "SENDING_STREAM_TO_MEDIA_PLAYER"
|
||||
)
|
||||
|
||||
type TorrentStreamState struct {
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
func (r *Repository) sendStateEvent(event string, data ...interface{}) {
|
||||
var dataToSend interface{}
|
||||
|
||||
if len(data) > 0 {
|
||||
dataToSend = data[0]
|
||||
}
|
||||
r.wsEventManager.SendEvent(events.TorrentStreamState, struct {
|
||||
State string `json:"state"`
|
||||
Data interface{} `json:"data"`
|
||||
}{
|
||||
State: event,
|
||||
Data: dataToSend,
|
||||
})
|
||||
}
|
||||
|
||||
//func (r *Repository) sendTorrentLoadingStatus(event TorrentLoadingStatusState, checking string) {
|
||||
// r.wsEventManager.SendEvent(eventTorrentLoadingStatus, &TorrentLoadingStatus{
|
||||
// TorrentBeingChecked: checking,
|
||||
// State: event,
|
||||
// })
|
||||
//}
|
||||
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
|
||||
}
|
||||
82
seanime-2.9.10/internal/torrentstream/handler.go
Normal file
82
seanime-2.9.10/internal/torrentstream/handler.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"seanime/internal/util/torrentutil"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/anacrolix/torrent"
|
||||
)
|
||||
|
||||
var _ = http.Handler(&handler{})
|
||||
|
||||
type (
|
||||
// handler serves the torrent stream
|
||||
handler struct {
|
||||
repository *Repository
|
||||
}
|
||||
)
|
||||
|
||||
func newHandler(repository *Repository) *handler {
|
||||
return &handler{
|
||||
repository: repository,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.repository.logger.Trace().Str("range", r.Header.Get("Range")).Msg("torrentstream: Stream endpoint hit")
|
||||
|
||||
if h.repository.client.currentFile.IsAbsent() || h.repository.client.currentTorrent.IsAbsent() {
|
||||
h.repository.logger.Error().Msg("torrentstream: No torrent to stream")
|
||||
http.Error(w, "No torrent to stream", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == http.MethodHead {
|
||||
r.Response.Header.Set("Content-Type", "video/mp4")
|
||||
r.Response.Header.Set("Content-Length", strconv.Itoa(int(h.repository.client.currentFile.MustGet().Length())))
|
||||
r.Response.Header.Set("Content-Disposition", "inline; filename="+h.repository.client.currentFile.MustGet().DisplayPath())
|
||||
r.Response.Header.Set("Accept-Ranges", "bytes")
|
||||
r.Response.Header.Set("Cache-Control", "no-cache")
|
||||
r.Response.Header.Set("Pragma", "no-cache")
|
||||
r.Response.Header.Set("Expires", "0")
|
||||
r.Response.Header.Set("X-Content-Type-Options", "nosniff")
|
||||
|
||||
// No content, just headers
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
file := h.repository.client.currentFile.MustGet()
|
||||
h.repository.logger.Trace().Str("file", file.DisplayPath()).Msg("torrentstream: New reader")
|
||||
tr := file.NewReader()
|
||||
defer func(tr torrent.Reader) {
|
||||
h.repository.logger.Trace().Msg("torrentstream: Closing reader")
|
||||
_ = tr.Close()
|
||||
}(tr)
|
||||
|
||||
tr.SetResponsive()
|
||||
// Read ahead 5MB for better streaming performance
|
||||
// DEVNOTE: Not sure if dynamic prioritization overwrites this but whatever
|
||||
tr.SetReadahead(5 * 1024 * 1024)
|
||||
|
||||
// If this is a range request for a later part of the file, prioritize those pieces
|
||||
rangeHeader := r.Header.Get("Range")
|
||||
if rangeHeader != "" && h.repository.client.currentTorrent.IsPresent() {
|
||||
t := h.repository.client.currentTorrent.MustGet()
|
||||
// Attempt to prioritize the pieces requested in the range
|
||||
torrentutil.PrioritizeRangeRequestPieces(rangeHeader, t, file, h.repository.logger)
|
||||
}
|
||||
|
||||
h.repository.logger.Trace().Str("file", file.DisplayPath()).Msg("torrentstream: Serving file content")
|
||||
w.Header().Set("Content-Type", "video/mp4")
|
||||
http.ServeContent(
|
||||
w,
|
||||
r,
|
||||
file.DisplayPath(),
|
||||
time.Now(),
|
||||
tr,
|
||||
)
|
||||
h.repository.logger.Trace().Msg("torrentstream: File content served")
|
||||
}
|
||||
34
seanime-2.9.10/internal/torrentstream/history.go
Normal file
34
seanime-2.9.10/internal/torrentstream/history.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"seanime/internal/database/db_bridge"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/util"
|
||||
)
|
||||
|
||||
type BatchHistoryResponse struct {
|
||||
Torrent *hibiketorrent.AnimeTorrent `json:"torrent"`
|
||||
}
|
||||
|
||||
func (r *Repository) GetBatchHistory(mId int) (ret *BatchHistoryResponse) {
|
||||
defer util.HandlePanicInModuleThen("torrentstream/GetBatchHistory", func() {
|
||||
ret = &BatchHistoryResponse{}
|
||||
})
|
||||
|
||||
torrent, err := db_bridge.GetTorrentstreamHistory(r.db, mId)
|
||||
if err != nil {
|
||||
return &BatchHistoryResponse{}
|
||||
}
|
||||
|
||||
return &BatchHistoryResponse{
|
||||
torrent,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) AddBatchHistory(mId int, torrent *hibiketorrent.AnimeTorrent) {
|
||||
go func() {
|
||||
defer util.HandlePanicInModuleThen("torrentstream/AddBatchHistory", func() {})
|
||||
|
||||
_ = db_bridge.InsertTorrentstreamHistory(r.db, mId, torrent)
|
||||
}()
|
||||
}
|
||||
26
seanime-2.9.10/internal/torrentstream/hook_events.go
Normal file
26
seanime-2.9.10/internal/torrentstream/hook_events.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"seanime/internal/api/anilist"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/hook_resolver"
|
||||
)
|
||||
|
||||
// TorrentStreamAutoSelectTorrentsFetchedEvent is triggered when the torrents are fetched for auto select.
|
||||
// The torrents are sorted by seeders from highest to lowest.
|
||||
// This event is triggered before the top 3 torrents are analyzed.
|
||||
type TorrentStreamAutoSelectTorrentsFetchedEvent struct {
|
||||
hook_resolver.Event
|
||||
Torrents []*hibiketorrent.AnimeTorrent
|
||||
}
|
||||
|
||||
// TorrentStreamSendStreamToMediaPlayerEvent is triggered when the torrent stream is about to send a stream to the media player.
|
||||
// Prevent default to skip the default playback and override the playback.
|
||||
type TorrentStreamSendStreamToMediaPlayerEvent struct {
|
||||
hook_resolver.Event
|
||||
WindowTitle string `json:"windowTitle"`
|
||||
StreamURL string `json:"streamURL"`
|
||||
Media *anilist.BaseAnime `json:"media"`
|
||||
AniDbEpisode string `json:"aniDbEpisode"`
|
||||
PlaybackType string `json:"playbackType"`
|
||||
}
|
||||
112
seanime-2.9.10/internal/torrentstream/playback.go
Normal file
112
seanime-2.9.10/internal/torrentstream/playback.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"seanime/internal/mediaplayers/mediaplayer"
|
||||
"seanime/internal/nativeplayer"
|
||||
)
|
||||
|
||||
type (
|
||||
playback struct {
|
||||
mediaPlayerCtxCancelFunc context.CancelFunc
|
||||
// Stores the video duration returned by the media player
|
||||
// When this is greater than 0, the video is considered to be playing
|
||||
currentVideoDuration int
|
||||
}
|
||||
)
|
||||
|
||||
func (r *Repository) listenToMediaPlayerEvents() {
|
||||
r.mediaPlayerRepositorySubscriber = r.mediaPlayerRepository.Subscribe("torrentstream")
|
||||
|
||||
if r.playback.mediaPlayerCtxCancelFunc != nil {
|
||||
r.playback.mediaPlayerCtxCancelFunc()
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, r.playback.mediaPlayerCtxCancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
go func(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.logger.Debug().Msg("torrentstream: Media player context cancelled")
|
||||
return
|
||||
case event := <-r.mediaPlayerRepositorySubscriber.EventCh:
|
||||
switch e := event.(type) {
|
||||
case mediaplayer.StreamingTrackingStartedEvent:
|
||||
// Reset the current video duration, as the video has stopped
|
||||
// DEVNOTE: This is changed in client.go as well when the duration is updated over 0
|
||||
r.playback.currentVideoDuration = 0
|
||||
case mediaplayer.StreamingVideoCompletedEvent:
|
||||
case mediaplayer.StreamingTrackingStoppedEvent:
|
||||
if r.client.currentTorrent.IsPresent() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
}
|
||||
}()
|
||||
r.logger.Debug().Msg("torrentstream: Media player stopped event received")
|
||||
// Stop the stream
|
||||
_ = r.StopStream()
|
||||
}()
|
||||
}
|
||||
case mediaplayer.StreamingPlaybackStatusEvent:
|
||||
go func() {
|
||||
if e.Status != nil && r.client.currentTorrent.IsPresent() {
|
||||
r.client.mediaPlayerPlaybackStatusCh <- e.Status
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}(ctx)
|
||||
}
|
||||
|
||||
func (r *Repository) listenToNativePlayerEvents() {
|
||||
r.nativePlayerSubscriber = r.nativePlayer.Subscribe("torrentstream")
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-r.nativePlayerSubscriber.Events():
|
||||
if !ok { // shouldn't happen
|
||||
r.logger.Debug().Msg("torrentstream: Native player subscriber channel closed")
|
||||
return
|
||||
}
|
||||
|
||||
switch event := event.(type) {
|
||||
case *nativeplayer.VideoLoadedMetadataEvent:
|
||||
go func() {
|
||||
if r.client.currentFile.IsPresent() && r.playback.currentVideoDuration == 0 {
|
||||
// If the stored video duration is 0 but the media player status shows a duration that is not 0
|
||||
// we know that the video has been loaded and is playing
|
||||
if r.playback.currentVideoDuration == 0 && event.Duration > 0 {
|
||||
// The media player has started playing the video
|
||||
r.logger.Debug().Msg("torrentstream: Media player started playing the video, sending event")
|
||||
r.sendStateEvent(eventTorrentStartedPlaying)
|
||||
// Update the stored video duration
|
||||
r.playback.currentVideoDuration = int(event.Duration)
|
||||
}
|
||||
}
|
||||
}()
|
||||
case *nativeplayer.VideoTerminatedEvent:
|
||||
r.logger.Debug().Msg("torrentstream: Native player terminated event received")
|
||||
r.playback.currentVideoDuration = 0
|
||||
// Only handle the event if we actually have a current torrent to avoid unnecessary cleanup
|
||||
if r.client.currentTorrent.IsPresent() {
|
||||
go func() {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
r.logger.Error().Msg("torrentstream: Recovered from panic in VideoTerminatedEvent handler")
|
||||
}
|
||||
}()
|
||||
r.logger.Debug().Msg("torrentstream: Stopping stream due to native player termination")
|
||||
// Stop the stream
|
||||
_ = r.StopStream()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
130
seanime-2.9.10/internal/torrentstream/previews.go
Normal file
130
seanime-2.9.10/internal/torrentstream/previews.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/5rahim/habari"
|
||||
"github.com/anacrolix/torrent"
|
||||
"seanime/internal/api/anilist"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/comparison"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
FilePreview struct {
|
||||
Path string `json:"path"`
|
||||
DisplayPath string `json:"displayPath"`
|
||||
DisplayTitle string `json:"displayTitle"`
|
||||
EpisodeNumber int `json:"episodeNumber"`
|
||||
RelativeEpisodeNumber int `json:"relativeEpisodeNumber"`
|
||||
IsLikely bool `json:"isLikely"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
GetTorrentFilePreviewsOptions struct {
|
||||
Torrent *hibiketorrent.AnimeTorrent
|
||||
Magnet string
|
||||
EpisodeNumber int
|
||||
AbsoluteOffset int
|
||||
Media *anilist.BaseAnime
|
||||
}
|
||||
)
|
||||
|
||||
func (r *Repository) GetTorrentFilePreviewsFromManualSelection(opts *GetTorrentFilePreviewsOptions) (ret []*FilePreview, err error) {
|
||||
defer util.HandlePanicInModuleWithError("torrentstream/GetTorrentFilePreviewsFromManualSelection", &err)
|
||||
|
||||
if opts.Torrent == nil || opts.Magnet == "" || opts.Media == nil {
|
||||
return nil, fmt.Errorf("torrentstream: Invalid options")
|
||||
}
|
||||
|
||||
r.logger.Trace().Str("hash", opts.Torrent.InfoHash).Msg("torrentstream: Getting file previews for torrent selection")
|
||||
|
||||
selectedTorrent, err := r.client.AddTorrent(opts.Magnet)
|
||||
if err != nil {
|
||||
r.logger.Error().Err(err).Msgf("torrentstream: Error adding torrent %s", opts.Magnet)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileMetadataMap := make(map[string]*habari.Metadata)
|
||||
wg := sync.WaitGroup{}
|
||||
mu := sync.RWMutex{}
|
||||
wg.Add(len(selectedTorrent.Files()))
|
||||
for _, file := range selectedTorrent.Files() {
|
||||
go func(file *torrent.File) {
|
||||
defer wg.Done()
|
||||
defer util.HandlePanicInModuleThen("debridstream/GetTorrentFilePreviewsFromManualSelection", func() {})
|
||||
|
||||
metadata := habari.Parse(file.DisplayPath())
|
||||
mu.Lock()
|
||||
fileMetadataMap[file.Path()] = metadata
|
||||
mu.Unlock()
|
||||
}(file)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
containsAbsoluteEps := false
|
||||
for _, metadata := range fileMetadataMap {
|
||||
if len(metadata.EpisodeNumber) == 1 {
|
||||
ep := util.StringToIntMust(metadata.EpisodeNumber[0])
|
||||
if ep > opts.Media.GetTotalEpisodeCount() {
|
||||
containsAbsoluteEps = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wg = sync.WaitGroup{}
|
||||
mu2 := sync.Mutex{}
|
||||
|
||||
for i, file := range selectedTorrent.Files() {
|
||||
wg.Add(1)
|
||||
go func(i int, file *torrent.File) {
|
||||
defer wg.Done()
|
||||
defer util.HandlePanicInModuleThen("torrentstream/GetTorrentFilePreviewsFromManualSelection", func() {})
|
||||
|
||||
mu.RLock()
|
||||
metadata := fileMetadataMap[file.Path()]
|
||||
mu.RUnlock()
|
||||
|
||||
displayTitle := file.DisplayPath()
|
||||
|
||||
isLikely := false
|
||||
parsedEpisodeNumber := -1
|
||||
|
||||
if metadata != nil && !comparison.ValueContainsSpecial(displayTitle) && !comparison.ValueContainsNC(displayTitle) {
|
||||
if len(metadata.EpisodeNumber) == 1 {
|
||||
ep := util.StringToIntMust(metadata.EpisodeNumber[0])
|
||||
parsedEpisodeNumber = ep
|
||||
displayTitle = fmt.Sprintf("Episode %d", ep)
|
||||
if metadata.EpisodeTitle != "" {
|
||||
displayTitle = fmt.Sprintf("%s - %s", displayTitle, metadata.EpisodeTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !containsAbsoluteEps {
|
||||
isLikely = parsedEpisodeNumber == opts.EpisodeNumber
|
||||
}
|
||||
|
||||
mu2.Lock()
|
||||
// Get the file preview
|
||||
ret = append(ret, &FilePreview{
|
||||
Path: file.Path(),
|
||||
DisplayPath: file.DisplayPath(),
|
||||
DisplayTitle: displayTitle,
|
||||
EpisodeNumber: parsedEpisodeNumber,
|
||||
IsLikely: isLikely,
|
||||
Index: i,
|
||||
})
|
||||
mu2.Unlock()
|
||||
}(i, file)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
r.logger.Debug().Str("hash", opts.Torrent.InfoHash).Msg("torrentstream: Got file previews for torrent selection, dropping torrent")
|
||||
go selectedTorrent.Drop()
|
||||
|
||||
return
|
||||
}
|
||||
231
seanime-2.9.10/internal/torrentstream/repository.go
Normal file
231
seanime-2.9.10/internal/torrentstream/repository.go
Normal file
@@ -0,0 +1,231 @@
|
||||
package torrentstream
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"seanime/internal/api/anilist"
|
||||
"seanime/internal/api/metadata"
|
||||
"seanime/internal/database/db"
|
||||
"seanime/internal/database/models"
|
||||
"seanime/internal/directstream"
|
||||
"seanime/internal/events"
|
||||
hibiketorrent "seanime/internal/extension/hibike/torrent"
|
||||
"seanime/internal/library/anime"
|
||||
"seanime/internal/library/playbackmanager"
|
||||
"seanime/internal/mediaplayers/mediaplayer"
|
||||
"seanime/internal/nativeplayer"
|
||||
"seanime/internal/platforms/platform"
|
||||
"seanime/internal/torrents/torrent"
|
||||
"seanime/internal/util"
|
||||
"seanime/internal/util/result"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/mo"
|
||||
)
|
||||
|
||||
type (
|
||||
Repository struct {
|
||||
client *Client
|
||||
handler *handler
|
||||
playback playback
|
||||
settings mo.Option[Settings] // None by default, set and refreshed by [SetSettings]
|
||||
|
||||
selectionHistoryMap *result.Map[int, *hibiketorrent.AnimeTorrent] // Key: AniList media ID
|
||||
|
||||
// Injected dependencies
|
||||
torrentRepository *torrent.Repository
|
||||
baseAnimeCache *anilist.BaseAnimeCache
|
||||
completeAnimeCache *anilist.CompleteAnimeCache
|
||||
platform platform.Platform
|
||||
wsEventManager events.WSEventManagerInterface
|
||||
metadataProvider metadata.Provider
|
||||
playbackManager *playbackmanager.PlaybackManager
|
||||
mediaPlayerRepository *mediaplayer.Repository
|
||||
mediaPlayerRepositorySubscriber *mediaplayer.RepositorySubscriber
|
||||
nativePlayerSubscriber *nativeplayer.Subscriber
|
||||
directStreamManager *directstream.Manager
|
||||
nativePlayer *nativeplayer.NativePlayer
|
||||
logger *zerolog.Logger
|
||||
db *db.Database
|
||||
|
||||
onEpisodeCollectionChanged func(ec *anime.EpisodeCollection)
|
||||
|
||||
previousStreamOptions mo.Option[*StartStreamOptions]
|
||||
}
|
||||
|
||||
Settings struct {
|
||||
models.TorrentstreamSettings
|
||||
Host string
|
||||
Port int
|
||||
}
|
||||
|
||||
NewRepositoryOptions struct {
|
||||
Logger *zerolog.Logger
|
||||
TorrentRepository *torrent.Repository
|
||||
BaseAnimeCache *anilist.BaseAnimeCache
|
||||
CompleteAnimeCache *anilist.CompleteAnimeCache
|
||||
Platform platform.Platform
|
||||
MetadataProvider metadata.Provider
|
||||
PlaybackManager *playbackmanager.PlaybackManager
|
||||
WSEventManager events.WSEventManagerInterface
|
||||
Database *db.Database
|
||||
DirectStreamManager *directstream.Manager
|
||||
NativePlayer *nativeplayer.NativePlayer
|
||||
}
|
||||
)
|
||||
|
||||
// NewRepository creates a new injectable Repository instance
|
||||
func NewRepository(opts *NewRepositoryOptions) *Repository {
|
||||
ret := &Repository{
|
||||
client: nil,
|
||||
handler: nil,
|
||||
settings: mo.Option[Settings]{},
|
||||
selectionHistoryMap: result.NewResultMap[int, *hibiketorrent.AnimeTorrent](),
|
||||
torrentRepository: opts.TorrentRepository,
|
||||
baseAnimeCache: opts.BaseAnimeCache,
|
||||
completeAnimeCache: opts.CompleteAnimeCache,
|
||||
platform: opts.Platform,
|
||||
wsEventManager: opts.WSEventManager,
|
||||
metadataProvider: opts.MetadataProvider,
|
||||
playbackManager: opts.PlaybackManager,
|
||||
mediaPlayerRepository: nil,
|
||||
mediaPlayerRepositorySubscriber: nil,
|
||||
logger: opts.Logger,
|
||||
db: opts.Database,
|
||||
directStreamManager: opts.DirectStreamManager,
|
||||
nativePlayer: opts.NativePlayer,
|
||||
previousStreamOptions: mo.None[*StartStreamOptions](),
|
||||
}
|
||||
ret.client = NewClient(ret)
|
||||
ret.handler = newHandler(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *Repository) IsEnabled() bool {
|
||||
return r.settings.IsPresent() && r.settings.MustGet().Enabled && r.client != nil
|
||||
}
|
||||
|
||||
func (r *Repository) GetPreviousStreamOptions() (*StartStreamOptions, bool) {
|
||||
return r.previousStreamOptions.OrElse(nil), r.previousStreamOptions.IsPresent()
|
||||
}
|
||||
|
||||
// SetMediaPlayerRepository sets the mediaplayer repository and listens to events.
|
||||
// This MUST be called after instantiating the repository and will run even if the module is disabled.
|
||||
//
|
||||
// // Note: This is also used for Debrid streaming
|
||||
func (r *Repository) SetMediaPlayerRepository(mediaPlayerRepository *mediaplayer.Repository) {
|
||||
r.mediaPlayerRepository = mediaPlayerRepository
|
||||
r.listenToMediaPlayerEvents()
|
||||
}
|
||||
|
||||
// InitModules sets the settings for the torrentstream module.
|
||||
// It should be called before any other method, to ensure the module is active.
|
||||
func (r *Repository) InitModules(settings *models.TorrentstreamSettings, host string, port int) (err error) {
|
||||
r.client.Shutdown()
|
||||
|
||||
defer util.HandlePanicInModuleWithError("torrentstream/InitModules", &err)
|
||||
|
||||
if settings == nil {
|
||||
r.logger.Error().Msg("torrentstream: Cannot initialize module, no settings provided")
|
||||
r.settings = mo.None[Settings]()
|
||||
return errors.New("torrentstream: Cannot initialize module, no settings provided")
|
||||
}
|
||||
|
||||
s := *settings
|
||||
|
||||
if s.Enabled == false {
|
||||
r.logger.Info().Msg("torrentstream: Module is disabled")
|
||||
r.Shutdown()
|
||||
r.settings = mo.None[Settings]()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set default download directory, which is a temporary directory
|
||||
if s.DownloadDir == "" {
|
||||
s.DownloadDir = r.getDefaultDownloadPath()
|
||||
_ = os.MkdirAll(s.DownloadDir, os.ModePerm) // Create the directory if it doesn't exist
|
||||
}
|
||||
|
||||
// DEVNOTE: Commented code below causes error log after initializing the client
|
||||
//// Empty the download directory
|
||||
//_ = os.RemoveAll(s.DownloadDir)
|
||||
|
||||
if s.StreamingServerPort == 0 {
|
||||
s.StreamingServerPort = 43214
|
||||
}
|
||||
if s.TorrentClientPort == 0 {
|
||||
s.TorrentClientPort = 43213
|
||||
}
|
||||
if s.StreamingServerHost == "" {
|
||||
s.StreamingServerHost = "127.0.0.1"
|
||||
}
|
||||
|
||||
// Set the settings
|
||||
r.settings = mo.Some(Settings{
|
||||
TorrentstreamSettings: s,
|
||||
Host: host,
|
||||
Port: port,
|
||||
})
|
||||
|
||||
// Initialize the torrent client
|
||||
err = r.client.initializeClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start listening to native player events
|
||||
r.listenToNativePlayerEvents()
|
||||
|
||||
r.logger.Info().Msg("torrentstream: Module initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) HTTPStreamHandler() http.Handler {
|
||||
return r.handler
|
||||
}
|
||||
|
||||
func (r *Repository) FailIfNoSettings() error {
|
||||
if r.settings.IsAbsent() {
|
||||
return errors.New("torrentstream: no settings provided, the module is dormant")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown closes the torrent client and streaming server
|
||||
// TEST-ONLY
|
||||
func (r *Repository) Shutdown() {
|
||||
r.logger.Debug().Msg("torrentstream: Shutting down module")
|
||||
r.client.Shutdown()
|
||||
}
|
||||
|
||||
//// Cleanup shuts down the module and removes the download directory
|
||||
//func (r *Repository) Cleanup() {
|
||||
// if r.settings.IsAbsent() {
|
||||
// return
|
||||
// }
|
||||
// r.client.Close()
|
||||
//
|
||||
// // Remove the download directory
|
||||
// downloadDir := r.GetDownloadDir()
|
||||
// _ = os.RemoveAll(downloadDir)
|
||||
//}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
func (r *Repository) GetDownloadDir() string {
|
||||
if r.settings.IsAbsent() {
|
||||
return r.getDefaultDownloadPath()
|
||||
}
|
||||
if r.settings.MustGet().DownloadDir == "" {
|
||||
return r.getDefaultDownloadPath()
|
||||
}
|
||||
return r.settings.MustGet().DownloadDir
|
||||
}
|
||||
|
||||
func (r *Repository) getDefaultDownloadPath() string {
|
||||
tempDir := os.TempDir()
|
||||
downloadDirPath := filepath.Join(tempDir, "seanime", "torrentstream")
|
||||
return downloadDirPath
|
||||
}
|
||||
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