package nakama import ( "encoding/base64" "encoding/json" "errors" "fmt" "seanime/internal/api/anilist" "seanime/internal/api/metadata" "seanime/internal/events" "seanime/internal/library/anime" "seanime/internal/library/playbackmanager" "seanime/internal/util" "strconv" "strings" "time" "github.com/imroc/req/v3" ) type ( HydrateHostAnimeLibraryOptions struct { AnimeCollection *anilist.AnimeCollection LibraryCollection *anime.LibraryCollection MetadataProvider metadata.Provider } NakamaAnimeLibrary struct { LocalFiles []*anime.LocalFile `json:"localFiles"` AnimeCollection *anilist.AnimeCollection `json:"animeCollection"` } ) // generateHMACToken generates an HMAC token for stream authentication func (m *Manager) generateHMACToken(endpoint string) (string, error) { // Use the Nakama password as the base secret - HostPassword for hosts, RemoteServerPassword for peers var secret string if m.settings.IsHost { secret = m.settings.HostPassword } else { secret = m.settings.RemoteServerPassword } hmacAuth := util.NewHMACAuth(secret, 24*time.Hour) return hmacAuth.GenerateToken(endpoint) } func (m *Manager) GetHostAnimeLibraryFiles(mId ...int) (lfs []*anime.LocalFile, hydrated bool) { if !m.settings.Enabled || !m.settings.IncludeNakamaAnimeLibrary || !m.IsConnectedToHost() { return nil, false } var response *req.Response var err error if len(mId) > 0 { response, err = m.reqClient.R(). SetHeader("X-Seanime-Nakama-Token", m.settings.RemoteServerPassword). Get(m.GetHostBaseServerURL() + "/api/v1/nakama/host/anime/library/files/" + strconv.Itoa(mId[0])) if err != nil { return nil, false } } else { response, err = m.reqClient.R(). SetHeader("X-Seanime-Nakama-Token", m.settings.RemoteServerPassword). Get(m.GetHostBaseServerURL() + "/api/v1/nakama/host/anime/library/files") if err != nil { return nil, false } } if !response.IsSuccessState() { return nil, false } body := response.Bytes() var entryResponse struct { Data []*anime.LocalFile `json:"data"` } err = json.Unmarshal(body, &entryResponse) if err != nil { return nil, false } return entryResponse.Data, true } func (m *Manager) GetHostAnimeLibrary() (ac *NakamaAnimeLibrary, hydrated bool) { if !m.settings.Enabled || !m.settings.IncludeNakamaAnimeLibrary || !m.IsConnectedToHost() { return nil, false } var response *req.Response var err error response, err = m.reqClient.R(). SetHeader("X-Seanime-Nakama-Token", m.settings.RemoteServerPassword). Get(m.GetHostBaseServerURL() + "/api/v1/nakama/host/anime/library") if err != nil { return nil, false } if !response.IsSuccessState() { return nil, false } body := response.Bytes() var entryResponse struct { Data *NakamaAnimeLibrary `json:"data"` } err = json.Unmarshal(body, &entryResponse) if err != nil { return nil, false } if entryResponse.Data == nil { return nil, false } return entryResponse.Data, true } func (m *Manager) getBaseServerURL() string { ret := "" host := m.serverHost if host == "0.0.0.0" { host = "127.0.0.1" } ret = fmt.Sprintf("http://%s:%d", host, m.serverPort) if strings.HasPrefix(ret, "http://http") { ret = strings.Replace(ret, "http://http", "http", 1) } return ret } func (m *Manager) PlayHostAnimeLibraryFile(path string, userAgent string, media *anilist.BaseAnime, aniDBEpisode string) error { if !m.settings.Enabled || !m.IsConnectedToHost() { return errors.New("not connected to host") } m.previousPath = path m.logger.Debug().Int("mediaId", media.ID).Msg("nakama: Playing host anime library file") m.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "nakama-file") m.wsEventManager.SendEvent(events.InfoToast, "Sending stream to player...") // Send a HTTP request to the host to get the anime library // If we can access it then the host is sharing its anime library response, err := m.reqClient.R(). SetHeader("X-Seanime-Nakama-Token", m.settings.RemoteServerPassword). Get(m.GetHostBaseServerURL() + "/api/v1/nakama/host/anime/library/collection") if err != nil { return fmt.Errorf("cannot access host's anime library: %w", err) } if !response.IsSuccessState() { body := response.Bytes() code := response.StatusCode return fmt.Errorf("cannot access host's anime library: %d, %s", code, string(body)) } host := m.serverHost if host == "0.0.0.0" { host = "127.0.0.1" } address := fmt.Sprintf("%s:%d", host, m.serverPort) ret := fmt.Sprintf("http://%s/api/v1/nakama/stream?type=file&path=%s", address, base64.StdEncoding.EncodeToString([]byte(path))) if strings.HasPrefix(ret, "http://http") { ret = strings.Replace(ret, "http://http", "http", 1) } windowTitle := media.GetPreferredTitle() if !media.IsMovieOrSingleEpisode() { windowTitle += " - Episode " + aniDBEpisode } err = m.playbackManager.StartStreamingUsingMediaPlayer(windowTitle, &playbackmanager.StartPlayingOptions{ Payload: ret, UserAgent: userAgent, ClientId: "", }, media, aniDBEpisode) if err != nil { m.wsEventManager.SendEvent(events.HideIndefiniteLoader, "nakama-file") go m.playbackManager.UnsubscribeFromPlaybackStatus("nakama-file") return err } m.playbackManager.RegisterMediaPlayerCallback(func(event playbackmanager.PlaybackEvent, cancel func()) { switch event.(type) { case playbackmanager.StreamStartedEvent: m.wsEventManager.SendEvent(events.HideIndefiniteLoader, "nakama-file") cancel() } }) return nil } func (m *Manager) PlayHostAnimeStream(streamType string, userAgent string, media *anilist.BaseAnime, aniDBEpisode string) error { if !m.settings.Enabled || !m.IsConnectedToHost() { return errors.New("not connected to host") } m.logger.Debug().Int("mediaId", media.ID).Msg("nakama: Playing host anime stream") m.wsEventManager.SendEvent(events.ShowIndefiniteLoader, "nakama-stream") m.wsEventManager.SendEvent(events.InfoToast, "Sending stream to player...") host := m.serverHost if host == "0.0.0.0" { host = "127.0.0.1" } address := fmt.Sprintf("%s:%d", host, m.serverPort) ret := fmt.Sprintf("http://%s/api/v1/nakama/stream?type=%s", address, streamType) if strings.HasPrefix(ret, "http://http") { ret = strings.Replace(ret, "http://http", "http", 1) } windowTitle := media.GetPreferredTitle() if !media.IsMovieOrSingleEpisode() { windowTitle += " - Episode " + aniDBEpisode } err := m.playbackManager.StartStreamingUsingMediaPlayer(windowTitle, &playbackmanager.StartPlayingOptions{ Payload: ret, UserAgent: userAgent, ClientId: "", }, media, aniDBEpisode) if err != nil { m.wsEventManager.SendEvent(events.HideIndefiniteLoader, "nakama-stream") go m.playbackManager.UnsubscribeFromPlaybackStatus("nakama-stream") return err } m.playbackManager.RegisterMediaPlayerCallback(func(event playbackmanager.PlaybackEvent, cancel func()) { switch event.(type) { case playbackmanager.StreamStartedEvent: m.wsEventManager.SendEvent(events.HideIndefiniteLoader, "nakama-stream") cancel() } }) return nil }