node build fixed

This commit is contained in:
ra_ma
2025-09-20 14:08:38 +01:00
parent c6ebbe069d
commit 3d298fa434
1516 changed files with 535727 additions and 2 deletions

View File

@@ -0,0 +1,783 @@
package handlers
import (
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
"seanime/internal/database/db_bridge"
"seanime/internal/library/anime"
"seanime/internal/nakama"
"seanime/internal/util"
"strconv"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/samber/lo"
)
// HandleNakamaWebSocket handles WebSocket connections for Nakama peers
//
// @summary handles WebSocket connections for Nakama peers.
// @desc This endpoint handles WebSocket connections from Nakama peers when this instance is acting as a host.
// @route /api/v1/nakama/ws [GET]
func (h *Handler) HandleNakamaWebSocket(c echo.Context) error {
// Use the standard library HTTP ResponseWriter and Request
w := c.Response().Writer
r := c.Request()
// Let the Nakama manager handle the WebSocket connection
h.App.NakamaManager.HandlePeerConnection(w, r)
return nil
}
// HandleSendNakamaMessage
//
// @summary sends a custom message through Nakama.
// @desc This allows sending custom messages to connected peers or the host.
// @route /api/v1/nakama/message [POST]
// @returns nakama.MessageResponse
func (h *Handler) HandleSendNakamaMessage(c echo.Context) error {
type body struct {
MessageType string `json:"messageType"`
Payload interface{} `json:"payload"`
PeerID string `json:"peerId,omitempty"` // If specified, send to specific peer (host only)
}
var b body
if err := c.Bind(&b); err != nil {
return h.RespondWithError(c, err)
}
var err error
if b.PeerID != "" && h.App.Settings.GetNakama().IsHost {
// Send to specific peer
err = h.App.NakamaManager.SendMessageToPeer(b.PeerID, nakama.MessageType(b.MessageType), b.Payload)
} else if h.App.Settings.GetNakama().IsHost {
// Send to all peers
err = h.App.NakamaManager.SendMessage(nakama.MessageType(b.MessageType), b.Payload)
} else {
// Send to host
err = h.App.NakamaManager.SendMessageToHost(nakama.MessageType(b.MessageType), b.Payload)
}
if err != nil {
return h.RespondWithError(c, err)
}
response := &nakama.MessageResponse{
Success: true,
Message: "Message sent successfully",
}
return h.RespondWithData(c, response)
}
// HandleGetNakamaAnimeLibrary
//
// @summary shares the local anime collection with Nakama clients.
// @desc This creates a new LibraryCollection struct and returns it.
// @desc This is used to share the local anime collection with Nakama clients.
// @route /api/v1/nakama/host/anime/library/collection [GET]
// @returns nakama.NakamaAnimeLibrary
func (h *Handler) HandleGetNakamaAnimeLibrary(c echo.Context) error {
if !h.App.Settings.GetNakama().HostShareLocalAnimeLibrary {
return h.RespondWithError(c, errors.New("host is not sharing its anime library"))
}
animeCollection, err := h.App.GetAnimeCollection(false)
if err != nil {
return h.RespondWithError(c, err)
}
if animeCollection == nil {
return h.RespondWithData(c, &anime.LibraryCollection{})
}
lfs, _, err := db_bridge.GetLocalFiles(h.App.Database)
if err != nil {
return h.RespondWithError(c, err)
}
unsharedAnimeIds := h.App.Settings.GetNakama().HostUnsharedAnimeIds
unsharedAnimeIdsMap := make(map[int]struct{})
unsharedAnimeIdsMap[0] = struct{}{} // Do not share unmatched files
for _, id := range unsharedAnimeIds {
unsharedAnimeIdsMap[id] = struct{}{}
}
lfs = lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
_, ok := unsharedAnimeIdsMap[lf.MediaId]
return !ok
})
libraryCollection, err := anime.NewLibraryCollection(c.Request().Context(), &anime.NewLibraryCollectionOptions{
AnimeCollection: animeCollection,
Platform: h.App.AnilistPlatform,
LocalFiles: lfs,
MetadataProvider: h.App.MetadataProvider,
})
if err != nil {
return h.RespondWithError(c, err)
}
// Hydrate total library size
if libraryCollection != nil && libraryCollection.Stats != nil {
libraryCollection.Stats.TotalSize = util.Bytes(h.App.TotalLibrarySize)
}
return h.RespondWithData(c, &nakama.NakamaAnimeLibrary{
LocalFiles: lfs,
AnimeCollection: animeCollection,
})
}
// HandleGetNakamaAnimeLibraryCollection
//
// @summary shares the local anime collection with Nakama clients.
// @desc This creates a new LibraryCollection struct and returns it.
// @desc This is used to share the local anime collection with Nakama clients.
// @route /api/v1/nakama/host/anime/library/collection [GET]
// @returns anime.LibraryCollection
func (h *Handler) HandleGetNakamaAnimeLibraryCollection(c echo.Context) error {
if !h.App.Settings.GetNakama().HostShareLocalAnimeLibrary {
return h.RespondWithError(c, errors.New("host is not sharing its anime library"))
}
animeCollection, err := h.App.GetAnimeCollection(false)
if err != nil {
return h.RespondWithError(c, err)
}
if animeCollection == nil {
return h.RespondWithData(c, &anime.LibraryCollection{})
}
lfs, _, err := db_bridge.GetLocalFiles(h.App.Database)
if err != nil {
return h.RespondWithError(c, err)
}
unsharedAnimeIds := h.App.Settings.GetNakama().HostUnsharedAnimeIds
unsharedAnimeIdsMap := make(map[int]struct{})
unsharedAnimeIdsMap[0] = struct{}{}
for _, id := range unsharedAnimeIds {
unsharedAnimeIdsMap[id] = struct{}{}
}
lfs = lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
_, ok := unsharedAnimeIdsMap[lf.MediaId]
return !ok
})
libraryCollection, err := anime.NewLibraryCollection(c.Request().Context(), &anime.NewLibraryCollectionOptions{
AnimeCollection: animeCollection,
Platform: h.App.AnilistPlatform,
LocalFiles: lfs,
MetadataProvider: h.App.MetadataProvider,
})
if err != nil {
return h.RespondWithError(c, err)
}
// Hydrate total library size
if libraryCollection != nil && libraryCollection.Stats != nil {
libraryCollection.Stats.TotalSize = util.Bytes(h.App.TotalLibrarySize)
}
return h.RespondWithData(c, libraryCollection)
}
// HandleGetNakamaAnimeLibraryFiles
//
// @summary return the local files for the given AniList anime media id.
// @desc This is used by the anime media entry pages to get all the data about the anime.
// @route /api/v1/nakama/host/anime/library/files/{id} [POST]
// @param id - int - true - "AniList anime media ID"
// @returns []anime.LocalFile
func (h *Handler) HandleGetNakamaAnimeLibraryFiles(c echo.Context) error {
if !h.App.Settings.GetNakama().HostShareLocalAnimeLibrary {
return h.RespondWithError(c, errors.New("host is not sharing its anime library"))
}
mId, err := strconv.Atoi(c.Param("id"))
if err != nil {
return h.RespondWithError(c, err)
}
// Get all the local files
lfs, _, err := db_bridge.GetLocalFiles(h.App.Database)
if err != nil {
return h.RespondWithError(c, err)
}
unsharedAnimeIds := h.App.Settings.GetNakama().HostUnsharedAnimeIds
unsharedAnimeIdsMap := make(map[int]struct{})
unsharedAnimeIdsMap[0] = struct{}{}
for _, id := range unsharedAnimeIds {
unsharedAnimeIdsMap[id] = struct{}{}
}
retLfs := lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
if _, ok := unsharedAnimeIdsMap[lf.MediaId]; ok {
return false
}
return lf.MediaId == mId
})
return h.RespondWithData(c, retLfs)
}
// HandleGetNakamaAnimeAllLibraryFiles
//
// @summary return all the local files for the host.
// @desc This is used to share the local anime collection with Nakama clients.
// @route /api/v1/nakama/host/anime/library/files [POST]
// @returns []anime.LocalFile
func (h *Handler) HandleGetNakamaAnimeAllLibraryFiles(c echo.Context) error {
if !h.App.Settings.GetNakama().HostShareLocalAnimeLibrary {
return h.RespondWithError(c, errors.New("host is not sharing its anime library"))
}
// Get all the local files
lfs, _, err := db_bridge.GetLocalFiles(h.App.Database)
if err != nil {
return h.RespondWithError(c, err)
}
unsharedAnimeIds := h.App.Settings.GetNakama().HostUnsharedAnimeIds
unsharedAnimeIdsMap := make(map[int]struct{})
unsharedAnimeIdsMap[0] = struct{}{}
for _, id := range unsharedAnimeIds {
unsharedAnimeIdsMap[id] = struct{}{}
}
lfs = lo.Filter(lfs, func(lf *anime.LocalFile, _ int) bool {
_, ok := unsharedAnimeIdsMap[lf.MediaId]
return !ok
})
return h.RespondWithData(c, lfs)
}
// HandleNakamaPlayVideo
//
// @summary plays the media from the host.
// @route /api/v1/nakama/play [POST]
// @returns bool
func (h *Handler) HandleNakamaPlayVideo(c echo.Context) error {
type body struct {
Path string `json:"path"`
MediaId int `json:"mediaId"`
AniDBEpisode string `json:"anidbEpisode"`
}
b := new(body)
if err := c.Bind(b); err != nil {
return h.RespondWithError(c, err)
}
if !h.App.NakamaManager.IsConnectedToHost() {
return h.RespondWithError(c, errors.New("not connected to host"))
}
media, err := h.App.AnilistPlatform.GetAnime(c.Request().Context(), b.MediaId)
if err != nil {
return h.RespondWithError(c, err)
}
err = h.App.NakamaManager.PlayHostAnimeLibraryFile(b.Path, c.Request().Header.Get("User-Agent"), media, b.AniDBEpisode)
if err != nil {
return h.RespondWithError(c, err)
}
return h.RespondWithData(c, true)
}
// Note: This is not used anymore. Each peer will independently stream the torrent.
// route /api/v1/nakama/host/torrentstream/stream
// Allows peers to stream the currently playing torrent.
func (h *Handler) HandleNakamaHostTorrentstreamServeStream(c echo.Context) error {
h.App.TorrentstreamRepository.HTTPStreamHandler().ServeHTTP(c.Response().Writer, c.Request())
return nil
}
var videoProxyClient = &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: false, // Fixes issues on Linux
},
Timeout: 60 * time.Second,
}
// route /api/v1/nakama/host/debridstream/stream
// Allows peers to stream the currently playing torrent.
func (h *Handler) HandleNakamaHostDebridstreamServeStream(c echo.Context) error {
streamUrl, ok := h.App.DebridClientRepository.GetStreamURL()
if !ok {
return echo.NewHTTPError(http.StatusNotFound, "no stream url")
}
// Proxy the stream to the peer
// The debrid stream URL directly comes from the debrid service
req, err := http.NewRequest(c.Request().Method, streamUrl, c.Request().Body)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create request")
}
// Copy original request headers to the proxied request
for key, values := range c.Request().Header {
for _, value := range values {
req.Header.Add(key, value)
}
}
resp, err := videoProxyClient.Do(req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to proxy request")
}
defer resp.Body.Close()
// Copy response headers
for key, values := range resp.Header {
for _, value := range values {
c.Response().Header().Add(key, value)
}
}
// Set the status code
c.Response().WriteHeader(resp.StatusCode)
// Stream the response body
_, err = io.Copy(c.Response().Writer, resp.Body)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to stream response body")
}
return nil
}
// route /api/v1/nakama/host/debridstream/url
// Returns the debrid stream URL for direct access by peers to avoid host bandwidth usage
func (h *Handler) HandleNakamaHostGetDebridstreamURL(c echo.Context) error {
streamUrl, ok := h.App.DebridClientRepository.GetStreamURL()
if !ok {
return echo.NewHTTPError(http.StatusNotFound, "no stream url")
}
return h.RespondWithData(c, map[string]string{
"streamUrl": streamUrl,
})
}
// route /api/v1/nakama/host/anime/library/stream?path={base64_encoded_path}
func (h *Handler) HandleNakamaHostAnimeLibraryServeStream(c echo.Context) error {
filepath := c.QueryParam("path")
decodedPath, err := base64.StdEncoding.DecodeString(filepath)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid path")
}
h.App.Logger.Info().Msgf("nakama: Serving anime library file: %s", string(decodedPath))
// Make sure file is in library
isInLibrary := false
libraryPaths := h.App.Settings.GetLibrary().GetLibraryPaths()
for _, libraryPath := range libraryPaths {
if util.IsFileUnderDir(string(decodedPath), libraryPath) {
isInLibrary = true
break
}
}
if !isInLibrary {
return echo.NewHTTPError(http.StatusNotFound, "file not in library")
}
return c.File(string(decodedPath))
}
// route /api/v1/nakama/stream
// Proxies stream requests to the host. It inserts the Nakama password in the headers.
// It checks if the password is valid.
// For debrid streams, it redirects directly to the debrid service to avoid host bandwidth usage.
func (h *Handler) HandleNakamaProxyStream(c echo.Context) error {
streamType := c.QueryParam("type") // "file", "torrent", "debrid"
if streamType == "" {
return echo.NewHTTPError(http.StatusBadRequest, "type is required")
}
hostServerUrl := h.App.Settings.GetNakama().RemoteServerURL
hostServerUrl = strings.TrimSuffix(hostServerUrl, "/")
if streamType == "debrid" {
// Get the debrid stream URL from the host
urlEndpoint := hostServerUrl + "/api/v1/nakama/host/debridstream/url"
req, err := http.NewRequest(http.MethodGet, urlEndpoint, nil)
if err != nil {
h.App.Logger.Error().Err(err).Str("url", urlEndpoint).Msg("nakama: Failed to create debrid URL request")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create request")
}
// Add Nakama password for authentication
req.Header.Set("X-Seanime-Nakama-Token", h.App.Settings.GetNakama().RemoteServerPassword)
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Do(req)
if err != nil {
h.App.Logger.Error().Err(err).Str("url", urlEndpoint).Msg("nakama: Failed to get debrid stream URL")
return echo.NewHTTPError(http.StatusBadGateway, "failed to get stream URL")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
h.App.Logger.Warn().Int("status", resp.StatusCode).Str("url", urlEndpoint).Msg("nakama: Failed to get debrid stream URL")
return echo.NewHTTPError(resp.StatusCode, "failed to get stream URL")
}
// Parse the response to get the stream URL
type urlResponse struct {
Data struct {
StreamUrl string `json:"streamUrl"`
} `json:"data"`
}
var urlResp urlResponse
body, err := io.ReadAll(resp.Body)
if err != nil {
h.App.Logger.Error().Err(err).Msg("nakama: Failed to read debrid URL response")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to read response")
}
if err := json.Unmarshal(body, &urlResp); err != nil {
h.App.Logger.Error().Err(err).Msg("nakama: Failed to parse debrid URL response")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to parse response")
}
if urlResp.Data.StreamUrl == "" {
h.App.Logger.Error().Msg("nakama: Empty debrid stream URL")
return echo.NewHTTPError(http.StatusNotFound, "no stream URL available")
}
req, err = http.NewRequest(c.Request().Method, urlResp.Data.StreamUrl, c.Request().Body)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create request")
}
// Copy original request headers to the proxied request
for key, values := range c.Request().Header {
for _, value := range values {
req.Header.Add(key, value)
}
}
resp, err = videoProxyClient.Do(req)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to proxy request")
}
defer resp.Body.Close()
// Copy response headers
for key, values := range resp.Header {
for _, value := range values {
c.Response().Header().Add(key, value)
}
}
// Set the status code
c.Response().WriteHeader(resp.StatusCode)
// Stream the response body
_, err = io.Copy(c.Response().Writer, resp.Body)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "failed to stream response body")
}
return nil
}
requestUrl := ""
switch streamType {
case "file":
// Path should be base64 encoded
filepath := c.QueryParam("path")
if filepath == "" {
return echo.NewHTTPError(http.StatusBadRequest, "path is required")
}
requestUrl = hostServerUrl + "/api/v1/nakama/host/anime/library/stream?path=" + filepath
case "torrent":
requestUrl = hostServerUrl + "/api/v1/nakama/host/torrentstream/stream"
default:
return echo.NewHTTPError(http.StatusBadRequest, "invalid type")
}
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 10,
MaxIdleConnsPerHost: 2,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: true, // Disable keep-alive to prevent connection reuse issues
ForceAttemptHTTP2: false,
},
Timeout: 120 * time.Second,
}
if c.Request().Method == http.MethodHead {
req, err := http.NewRequest(http.MethodHead, requestUrl, nil)
if err != nil {
h.App.Logger.Error().Err(err).Str("url", requestUrl).Msg("nakama: Failed to create HEAD request")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create request")
}
// Add Nakama password for authentication
req.Header.Set("X-Seanime-Nakama-Token", h.App.Settings.GetNakama().RemoteServerPassword)
// Add User-Agent from original request
if userAgent := c.Request().Header.Get("User-Agent"); userAgent != "" {
req.Header.Set("User-Agent", userAgent)
}
resp, err := client.Do(req)
if err != nil {
h.App.Logger.Error().Err(err).Str("url", requestUrl).Msg("nakama: Failed to proxy HEAD request")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to proxy request")
}
defer resp.Body.Close()
// Log authentication failures
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
h.App.Logger.Warn().Int("status", resp.StatusCode).Str("url", requestUrl).Msg("nakama: Authentication failed - check password configuration")
}
// Copy response headers
for key, values := range resp.Header {
for _, value := range values {
c.Response().Header().Add(key, value)
}
}
return c.NoContent(resp.StatusCode)
}
// Create request with timeout context
ctx := c.Request().Context()
req, err := http.NewRequestWithContext(ctx, c.Request().Method, requestUrl, c.Request().Body)
if err != nil {
h.App.Logger.Error().Err(err).Str("url", requestUrl).Msg("nakama: Failed to create request")
return echo.NewHTTPError(http.StatusInternalServerError, "failed to create request")
}
// Copy request headers but skip problematic ones
for key, values := range c.Request().Header {
// Skip headers that should not be forwarded or might cause errors
if key == "Host" || key == "Content-Length" || key == "Connection" ||
key == "Transfer-Encoding" || key == "Accept-Encoding" ||
key == "Upgrade" || key == "Proxy-Connection" ||
strings.HasPrefix(key, "Sec-") { // Skip WebSocket and security headers
continue
}
for _, value := range values {
req.Header.Add(key, value)
}
}
req.Header.Set("Accept", "*/*")
// req.Header.Set("Accept-Encoding", "identity") // Disable compression to avoid issues
// Add Nakama password for authentication
req.Header.Set("X-Seanime-Nakama-Token", h.App.Settings.GetNakama().RemoteServerPassword)
h.App.Logger.Debug().Str("url", requestUrl).Str("method", c.Request().Method).Msg("nakama: Proxying request")
// Add retry mechanism for intermittent network issues
var resp *http.Response
maxRetries := 3
for attempt := 0; attempt < maxRetries; attempt++ {
resp, err = client.Do(req)
if err == nil {
break
}
if attempt < maxRetries-1 {
h.App.Logger.Warn().Err(err).Int("attempt", attempt+1).Str("url", requestUrl).Msg("nakama: request failed, retrying")
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond) // Exponential backoff
continue
}
h.App.Logger.Error().Err(err).Str("url", requestUrl).Msg("nakama: failed to proxy request after retries")
return echo.NewHTTPError(http.StatusBadGateway, "failed to proxy request after retries")
}
defer resp.Body.Close()
// Log authentication failures with more detail
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
h.App.Logger.Warn().Int("status", resp.StatusCode).Str("url", requestUrl).Msg("nakama: authentication failed - verify RemoteServerPassword matches host's HostPassword")
}
// Log and handle 406 Not Acceptable errors
if resp.StatusCode == http.StatusNotAcceptable {
h.App.Logger.Error().Int("status", resp.StatusCode).Str("url", requestUrl).Str("content-type", resp.Header.Get("Content-Type")).Msg("nakama: 406 Not Acceptable - content negotiation failed")
}
// Handle range request errors
if resp.StatusCode == http.StatusRequestedRangeNotSatisfiable {
h.App.Logger.Warn().Int("status", resp.StatusCode).Str("url", requestUrl).Str("range", c.Request().Header.Get("Range")).Msg("nakama: range request not satisfiable")
}
// Copy response headers
for key, values := range resp.Header {
for _, value := range values {
c.Response().Header().Add(key, value)
}
}
// Set the status code
c.Response().WriteHeader(resp.StatusCode)
// Stream the response body with better error handling
bytesWritten, err := io.Copy(c.Response().Writer, resp.Body)
if err != nil {
// Check if it's a network-related error
if strings.Contains(err.Error(), "connection") || strings.Contains(err.Error(), "broken pipe") ||
strings.Contains(err.Error(), "wsasend") || strings.Contains(err.Error(), "reset by peer") {
h.App.Logger.Warn().Err(err).Int64("bytes_written", bytesWritten).Str("url", requestUrl).Msg("nakama: network connection error during streaming")
} else {
h.App.Logger.Error().Err(err).Int64("bytes_written", bytesWritten).Str("url", requestUrl).Msg("nakama: error streaming response body")
}
// Don't return error here as response has already started
} else {
h.App.Logger.Debug().Int64("bytes_written", bytesWritten).Str("url", requestUrl).Msg("nakama: successfully streamed response")
}
return nil
}
// HandleNakamaReconnectToHost
//
// @summary reconnects to the Nakama host.
// @desc This attempts to reconnect to the configured Nakama host if the connection was lost.
// @route /api/v1/nakama/reconnect [POST]
// @returns nakama.MessageResponse
func (h *Handler) HandleNakamaReconnectToHost(c echo.Context) error {
err := h.App.NakamaManager.ReconnectToHost()
if err != nil {
return h.RespondWithError(c, err)
}
response := &nakama.MessageResponse{
Success: true,
Message: "Reconnection initiated",
}
return h.RespondWithData(c, response)
}
// HandleNakamaRemoveStaleConnections
//
// @summary removes stale peer connections.
// @desc This removes peer connections that haven't responded to ping messages for a while.
// @route /api/v1/nakama/cleanup [POST]
// @returns nakama.MessageResponse
func (h *Handler) HandleNakamaRemoveStaleConnections(c echo.Context) error {
if !h.App.Settings.GetNakama().IsHost {
return h.RespondWithError(c, errors.New("not acting as host"))
}
h.App.NakamaManager.RemoveStaleConnections()
response := &nakama.MessageResponse{
Success: true,
Message: "Stale connections cleaned up",
}
return h.RespondWithData(c, response)
}
// HandleNakamaCreateWatchParty
//
// @summary creates a new watch party session.
// @desc This creates a new watch party that peers can join to watch content together in sync.
// @route /api/v1/nakama/watch-party/create [POST]
// @returns bool
func (h *Handler) HandleNakamaCreateWatchParty(c echo.Context) error {
type body struct {
Settings *nakama.WatchPartySessionSettings `json:"settings"`
}
var b body
if err := c.Bind(&b); err != nil {
return h.RespondWithError(c, err)
}
if !h.App.Settings.GetNakama().IsHost {
return h.RespondWithError(c, errors.New("only hosts can create watch parties"))
}
// Set default settings if not provided
if b.Settings == nil {
b.Settings = &nakama.WatchPartySessionSettings{
SyncThreshold: 2.0,
MaxBufferWaitTime: 10,
}
}
_, err := h.App.NakamaManager.GetWatchPartyManager().CreateWatchParty(&nakama.CreateWatchOptions{
Settings: b.Settings,
})
if err != nil {
return h.RespondWithError(c, err)
}
return h.RespondWithData(c, true)
}
// HandleNakamaJoinWatchParty
//
// @summary joins an existing watch party.
// @desc This allows a peer to join an active watch party session.
// @route /api/v1/nakama/watch-party/join [POST]
// @returns bool
func (h *Handler) HandleNakamaJoinWatchParty(c echo.Context) error {
if h.App.Settings.GetNakama().IsHost {
return h.RespondWithError(c, errors.New("hosts cannot join watch parties"))
}
if !h.App.NakamaManager.IsConnectedToHost() {
return h.RespondWithError(c, errors.New("not connected to host"))
}
// Send join request to host
err := h.App.NakamaManager.GetWatchPartyManager().JoinWatchParty()
if err != nil {
return h.RespondWithError(c, err)
}
return h.RespondWithData(c, true)
}
// HandleNakamaLeaveWatchParty
//
// @summary leaves the current watch party.
// @desc This removes the user from the active watch party session.
// @route /api/v1/nakama/watch-party/leave [POST]
// @returns bool
func (h *Handler) HandleNakamaLeaveWatchParty(c echo.Context) error {
if h.App.Settings.GetNakama().IsHost {
// Host stopping the watch party
h.App.NakamaManager.GetWatchPartyManager().StopWatchParty()
} else {
// Peer leaving the watch party
if !h.App.NakamaManager.IsConnectedToHost() {
return h.RespondWithError(c, errors.New("not connected to host"))
}
err := h.App.NakamaManager.GetWatchPartyManager().LeaveWatchParty()
if err != nil {
return h.RespondWithError(c, err)
}
}
return h.RespondWithData(c, true)
}