236 lines
6.6 KiB
Go
236 lines
6.6 KiB
Go
package playbackmanager
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"seanime/internal/api/anilist"
|
|
"seanime/internal/events"
|
|
"seanime/internal/library/anime"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type (
|
|
playlistHub struct {
|
|
requestNewFileCh chan string
|
|
endOfPlaylistCh chan struct{}
|
|
|
|
wsEventManager events.WSEventManagerInterface
|
|
logger *zerolog.Logger
|
|
currentPlaylist *anime.Playlist // The current playlist that is being played (can be nil)
|
|
nextLocalFile *anime.LocalFile // The next episode that will be played (can be nil)
|
|
cancel context.CancelFunc // The cancel function for the current playlist
|
|
mu sync.Mutex // The mutex
|
|
|
|
playingLf *anime.LocalFile // The currently playing local file
|
|
playingMediaListEntry *anilist.AnimeListEntry // The currently playing media entry
|
|
completedCurrent atomic.Bool // Whether the current episode has been completed
|
|
|
|
currentState *PlaylistState // This is sent to the client to show the current playlist state
|
|
|
|
playbackManager *PlaybackManager
|
|
}
|
|
|
|
PlaylistState struct {
|
|
Current *PlaylistStateItem `json:"current"`
|
|
Next *PlaylistStateItem `json:"next"`
|
|
Remaining int `json:"remaining"`
|
|
}
|
|
|
|
PlaylistStateItem struct {
|
|
Name string `json:"name"`
|
|
MediaImage string `json:"mediaImage"`
|
|
}
|
|
)
|
|
|
|
func newPlaylistHub(pm *PlaybackManager) *playlistHub {
|
|
ret := &playlistHub{
|
|
logger: pm.Logger,
|
|
wsEventManager: pm.wsEventManager,
|
|
playbackManager: pm,
|
|
requestNewFileCh: make(chan string, 1),
|
|
endOfPlaylistCh: make(chan struct{}, 1),
|
|
completedCurrent: atomic.Bool{},
|
|
}
|
|
|
|
ret.completedCurrent.Store(false)
|
|
|
|
return ret
|
|
}
|
|
|
|
func (h *playlistHub) loadPlaylist(playlist *anime.Playlist) {
|
|
if playlist == nil {
|
|
h.logger.Error().Msg("playlist hub: Playlist is nil")
|
|
return
|
|
}
|
|
h.reset()
|
|
h.currentPlaylist = playlist
|
|
h.logger.Debug().Str("name", playlist.Name).Msg("playlist hub: Playlist loaded")
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) reset() {
|
|
if h.cancel != nil {
|
|
h.cancel()
|
|
}
|
|
h.currentPlaylist = nil
|
|
h.playingLf = nil
|
|
h.playingMediaListEntry = nil
|
|
h.currentState = nil
|
|
h.wsEventManager.SendEvent(events.PlaybackManagerPlaylistState, h.currentState)
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) check(currListEntry *anilist.AnimeListEntry, currLf *anime.LocalFile, ps PlaybackState) bool {
|
|
if h.currentPlaylist == nil || currLf == nil || currListEntry == nil {
|
|
h.currentPlaylist = nil
|
|
h.playingLf = nil
|
|
h.playingMediaListEntry = nil
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (h *playlistHub) findNextFile() (*anime.LocalFile, bool) {
|
|
if h.currentPlaylist == nil || h.playingLf == nil {
|
|
return nil, false
|
|
}
|
|
|
|
for i, lf := range h.currentPlaylist.LocalFiles {
|
|
if lf.GetNormalizedPath() == h.playingLf.GetNormalizedPath() {
|
|
if i+1 < len(h.currentPlaylist.LocalFiles) {
|
|
return h.currentPlaylist.LocalFiles[i+1], true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (h *playlistHub) playNextFile() (*anime.LocalFile, bool) {
|
|
if h.currentPlaylist == nil || h.playingLf == nil || h.nextLocalFile == nil {
|
|
return nil, false
|
|
}
|
|
|
|
h.logger.Debug().Str("path", h.nextLocalFile.Path).Str("cmd", "playNextFile").Msg("playlist hub: Requesting next file")
|
|
h.requestNewFileCh <- h.nextLocalFile.Path
|
|
h.completedCurrent.Store(false)
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (h *playlistHub) onVideoStart(currListEntry *anilist.AnimeListEntry, currLf *anime.LocalFile, ps PlaybackState) {
|
|
if !h.check(currListEntry, currLf, ps) {
|
|
return
|
|
}
|
|
|
|
h.playingLf = currLf
|
|
h.playingMediaListEntry = currListEntry
|
|
|
|
h.nextLocalFile, _ = h.findNextFile()
|
|
|
|
if h.playbackManager.animeCollection.IsAbsent() {
|
|
return
|
|
}
|
|
|
|
// Refresh current playlist state
|
|
playlistState := &PlaylistState{}
|
|
playlistState.Current = &PlaylistStateItem{
|
|
Name: fmt.Sprintf("%s - Episode %d", currListEntry.GetMedia().GetPreferredTitle(), currLf.GetEpisodeNumber()),
|
|
MediaImage: currListEntry.GetMedia().GetCoverImageSafe(),
|
|
}
|
|
if h.nextLocalFile != nil {
|
|
lfe, found := h.playbackManager.animeCollection.MustGet().GetListEntryFromAnimeId(h.nextLocalFile.MediaId)
|
|
if found {
|
|
playlistState.Next = &PlaylistStateItem{
|
|
Name: fmt.Sprintf("%s - Episode %d", lfe.GetMedia().GetPreferredTitle(), h.nextLocalFile.GetEpisodeNumber()),
|
|
MediaImage: lfe.GetMedia().GetCoverImageSafe(),
|
|
}
|
|
}
|
|
}
|
|
remaining := 0
|
|
for i, lf := range h.currentPlaylist.LocalFiles {
|
|
if lf.GetNormalizedPath() == currLf.GetNormalizedPath() {
|
|
remaining = len(h.currentPlaylist.LocalFiles) - 1 - i
|
|
break
|
|
}
|
|
}
|
|
playlistState.Remaining = remaining
|
|
h.currentState = playlistState
|
|
h.completedCurrent.Store(false)
|
|
|
|
h.logger.Debug().Str("path", currLf.Path).Msgf("playlist hub: Video started")
|
|
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) onVideoCompleted(currListEntry *anilist.AnimeListEntry, currLf *anime.LocalFile, ps PlaybackState) {
|
|
if !h.check(currListEntry, currLf, ps) {
|
|
return
|
|
}
|
|
|
|
h.logger.Debug().Str("path", currLf.Path).Msgf("playlist hub: Video completed")
|
|
h.completedCurrent.Store(true)
|
|
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) onPlaybackStatus(currListEntry *anilist.AnimeListEntry, currLf *anime.LocalFile, ps PlaybackState) {
|
|
if !h.check(currListEntry, currLf, ps) {
|
|
return
|
|
}
|
|
|
|
h.wsEventManager.SendEvent(events.PlaybackManagerPlaylistState, h.currentState)
|
|
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) onTrackingStopped() {
|
|
if h.currentPlaylist == nil || h.playingLf == nil { // Return if no playlist
|
|
return
|
|
}
|
|
|
|
// When tracking has stopped, request next file
|
|
//if h.nextLocalFile != nil {
|
|
// h.logger.Debug().Str("path", h.nextLocalFile.Path).Msg("playlist hub: Requesting next file")
|
|
// h.requestNewFileCh <- h.nextLocalFile.Path
|
|
//} else {
|
|
// h.logger.Debug().Msg("playlist hub: End of playlist")
|
|
// h.endOfPlaylistCh <- struct{}{}
|
|
//}
|
|
|
|
h.logger.Debug().Msgf("playlist hub: Tracking stopped, completed current: %v", h.completedCurrent.Load())
|
|
|
|
if !h.completedCurrent.Load() {
|
|
h.reset()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (h *playlistHub) onTrackingError() {
|
|
if h.currentPlaylist == nil { // Return if no playlist
|
|
return
|
|
}
|
|
|
|
// When tracking has stopped, request next file
|
|
h.logger.Debug().Msgf("playlist hub: Tracking error, completed current: %v", h.completedCurrent.Load())
|
|
if h.completedCurrent.Load() {
|
|
h.logger.Debug().Msg("playlist hub: Assuming current episode is completed")
|
|
if h.nextLocalFile != nil {
|
|
h.logger.Debug().Str("path", h.nextLocalFile.Path).Msg("playlist hub: Requesting next file")
|
|
h.requestNewFileCh <- h.nextLocalFile.Path
|
|
//h.completedCurrent.Store(false) do not reset completedCurrent here
|
|
} else {
|
|
h.logger.Debug().Msg("playlist hub: End of playlist")
|
|
h.endOfPlaylistCh <- struct{}{}
|
|
h.completedCurrent.Store(false)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|