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