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,646 @@
package nakama
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"seanime/internal/database/models"
debrid_client "seanime/internal/debrid/client"
"seanime/internal/events"
"seanime/internal/library/playbackmanager"
"seanime/internal/platforms/platform"
"seanime/internal/torrentstream"
"seanime/internal/util"
"seanime/internal/util/result"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
"github.com/imroc/req/v3"
"github.com/rs/zerolog"
)
type Manager struct {
serverHost string
serverPort int
username string
logger *zerolog.Logger
settings *models.NakamaSettings
wsEventManager events.WSEventManagerInterface
platform platform.Platform
playbackManager *playbackmanager.PlaybackManager
torrentstreamRepository *torrentstream.Repository
debridClientRepository *debrid_client.Repository
peerId string
// Host connections (when acting as host)
peerConnections *result.Map[string, *PeerConnection]
// Host connection (when connecting to a host)
hostConnection *HostConnection
hostConnectionCtx context.Context
hostConnectionCancel context.CancelFunc
hostMu sync.RWMutex
reconnecting bool // Flag to prevent multiple concurrent reconnection attempts
// Connection management
cancel context.CancelFunc
ctx context.Context
// Message handlers
messageHandlers map[MessageType]func(*Message, string) error
handlerMu sync.RWMutex
// Cleanup functions
cleanups []func()
reqClient *req.Client
watchPartyManager *WatchPartyManager
previousPath string // latest file streamed by the peer - real path on the host
}
type NewManagerOptions struct {
Logger *zerolog.Logger
WSEventManager events.WSEventManagerInterface
PlaybackManager *playbackmanager.PlaybackManager
TorrentstreamRepository *torrentstream.Repository
DebridClientRepository *debrid_client.Repository
Platform platform.Platform
ServerHost string
ServerPort int
}
type ConnectionType string
const (
ConnectionTypeHost ConnectionType = "host"
ConnectionTypePeer ConnectionType = "peer"
)
// MessageType represents the type of message being sent
type MessageType string
const (
MessageTypeAuth MessageType = "auth"
MessageTypeAuthReply MessageType = "auth_reply"
MessageTypePing MessageType = "ping"
MessageTypePong MessageType = "pong"
MessageTypeError MessageType = "error"
MessageTypeCustom MessageType = "custom"
)
// Message represents a message sent between Nakama instances
type Message struct {
Type MessageType `json:"type"`
Payload interface{} `json:"payload"`
RequestID string `json:"requestId,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// PeerConnection represents a connection from a peer to this host
type PeerConnection struct {
ID string // Internal connection ID (websocket)
PeerId string // UUID generated by the peer (primary identifier)
Username string // Display name (kept for UI purposes)
Conn *websocket.Conn
ConnectionType ConnectionType
Authenticated bool
LastPing time.Time
mu sync.RWMutex
}
// HostConnection represents this instance's connection to a host
type HostConnection struct {
URL string
PeerId string // UUID generated by this peer instance
Username string
Conn *websocket.Conn
Authenticated bool
LastPing time.Time
reconnectTimer *time.Timer
mu sync.RWMutex
}
// NakamaEvent represents events sent to the client
type NakamaEvent struct {
Type string `json:"type"`
Payload interface{} `json:"payload"`
}
// AuthPayload represents authentication data
type AuthPayload struct {
Password string `json:"password"`
PeerId string `json:"peerId"` // UUID generated by the peer
}
// AuthReplyPayload represents authentication response
type AuthReplyPayload struct {
Success bool `json:"success"`
Message string `json:"message"`
Username string `json:"username"`
PeerId string `json:"peerId"` // Echo back the peer's UUID
}
// ErrorPayload represents error messages
type ErrorPayload struct {
Message string `json:"message"`
Code string `json:"code,omitempty"`
}
// HostConnectionStatus represents the status of the host connection
type HostConnectionStatus struct {
Connected bool `json:"connected"`
Authenticated bool `json:"authenticated"`
URL string `json:"url"`
LastPing time.Time `json:"lastPing"`
PeerId string `json:"peerId"`
Username string `json:"username"`
}
// NakamaStatus represents the overall status of Nakama connections
type NakamaStatus struct {
IsHost bool `json:"isHost"`
ConnectedPeers []string `json:"connectedPeers"`
IsConnectedToHost bool `json:"isConnectedToHost"`
HostConnectionStatus *HostConnectionStatus `json:"hostConnectionStatus"`
CurrentWatchPartySession *WatchPartySession `json:"currentWatchPartySession"`
}
// MessageResponse represents a response to message sending requests
type MessageResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
type ClientEvent struct {
Type string `json:"type"`
Payload interface{} `json:"payload"`
}
func NewManager(opts *NewManagerOptions) *Manager {
ctx, cancel := context.WithCancel(context.Background())
m := &Manager{
username: "",
logger: opts.Logger,
wsEventManager: opts.WSEventManager,
playbackManager: opts.PlaybackManager,
peerConnections: result.NewResultMap[string, *PeerConnection](),
platform: opts.Platform,
ctx: ctx,
cancel: cancel,
messageHandlers: make(map[MessageType]func(*Message, string) error),
cleanups: make([]func(), 0),
reqClient: req.C(),
serverHost: opts.ServerHost,
serverPort: opts.ServerPort,
settings: &models.NakamaSettings{},
torrentstreamRepository: opts.TorrentstreamRepository,
debridClientRepository: opts.DebridClientRepository,
previousPath: "",
}
m.watchPartyManager = NewWatchPartyManager(m)
// Register default message handlers
m.registerDefaultHandlers()
eventListener := m.wsEventManager.SubscribeToClientEvents("nakama")
go func() {
for event := range eventListener.Channel {
if event.Type == events.NakamaStatusRequested {
currSession, _ := m.GetWatchPartyManager().GetCurrentSession()
status := &NakamaStatus{
IsHost: m.IsHost(),
ConnectedPeers: m.GetConnectedPeers(),
IsConnectedToHost: m.IsConnectedToHost(),
HostConnectionStatus: m.GetHostConnectionStatus(),
CurrentWatchPartySession: currSession,
}
m.wsEventManager.SendEvent(events.NakamaStatus, status)
}
if event.Type == events.NakamaWatchPartyEnableRelayMode {
var payload WatchPartyEnableRelayModePayload
marshaledPayload, err := json.Marshal(event.Payload)
if err != nil {
m.logger.Error().Err(err).Msg("nakama: Failed to marshal watch party enable relay mode payload")
continue
}
err = json.Unmarshal(marshaledPayload, &payload)
if err != nil {
m.logger.Error().Err(err).Msg("nakama: Failed to unmarshal watch party enable relay mode payload")
continue
}
m.GetWatchPartyManager().EnableRelayMode(payload.PeerId)
}
}
}()
return m
}
func (m *Manager) SetSettings(settings *models.NakamaSettings) {
var previousSettings *models.NakamaSettings
if m.settings != nil {
previousSettings = &[]models.NakamaSettings{*m.settings}[0]
}
// If the host password has changed, stop host service
// This will cause a restart of the host service
disconnectAsHost := false
if m.settings != nil && m.settings.HostPassword != settings.HostPassword {
disconnectAsHost = true
m.stopHostServices()
}
m.settings = settings
m.username = cmp.Or(settings.Username, "Peer_"+util.RandomStringWithAlphabet(8, "bcdefhijklmnopqrstuvwxyz0123456789"))
m.logger.Debug().Bool("isHost", settings.IsHost).Str("username", m.username).Str("remoteURL", settings.RemoteServerURL).Msg("nakama: Settings updated")
if previousSettings == nil || previousSettings.IsHost != settings.IsHost || previousSettings.Enabled != settings.Enabled || disconnectAsHost {
// Determine if we should stop host services
shouldStopHost := m.IsHost() && (!settings.Enabled || // Nakama disabled
!settings.IsHost || // Switching to peer mode
disconnectAsHost) // Password changed (requires restart)
// Determine if we should start host services
shouldStartHost := settings.IsHost && settings.Enabled
// Always stop first if needed, then start
if shouldStopHost {
m.stopHostServices()
}
if shouldStartHost {
m.startHostServices()
}
}
if previousSettings == nil || previousSettings.RemoteServerURL != settings.RemoteServerURL || previousSettings.RemoteServerPassword != settings.RemoteServerPassword || previousSettings.Enabled != settings.Enabled {
// Determine if we should disconnect from current host
shouldDisconnect := m.IsConnectedToHost() && (!settings.Enabled || // Nakama disabled
settings.IsHost || // Switching to host mode
settings.RemoteServerURL == "" || // No remote URL
settings.RemoteServerPassword == "" || // No password
(previousSettings != nil && previousSettings.RemoteServerURL != settings.RemoteServerURL) || // URL changed
(previousSettings != nil && previousSettings.RemoteServerPassword != settings.RemoteServerPassword)) // Password changed
// Determine if we should connect to a host
shouldConnect := !settings.IsHost &&
settings.Enabled &&
settings.RemoteServerURL != "" &&
settings.RemoteServerPassword != ""
// Always disconnect first if needed, then connect
if shouldDisconnect {
m.disconnectFromHost()
}
if shouldConnect {
m.connectToHost()
}
}
// if previousSettings == nil || previousSettings.Username != settings.Username {
// m.SendMessage(MessageTypeCustom, map[string]interface{}{
// "type": "nakama_username_changed",
// "username": settings.Username,
// })
// }
}
func (m *Manager) GetHostBaseServerURL() string {
url := m.settings.RemoteServerURL
if strings.HasSuffix(url, "/") {
url = strings.TrimSuffix(url, "/")
}
return url
}
func (m *Manager) IsHost() bool {
return m.settings.IsHost
}
func (m *Manager) GetHostConnection() (*HostConnection, bool) {
m.hostMu.RLock()
defer m.hostMu.RUnlock()
return m.hostConnection, m.hostConnection != nil
}
// GetWatchPartyManager returns the watch party manager
func (m *Manager) GetWatchPartyManager() *WatchPartyManager {
return m.watchPartyManager
}
// Cleanup stops all connections and services
func (m *Manager) Cleanup() {
m.logger.Debug().Msg("nakama: Cleaning up")
if m.cancel != nil {
m.cancel()
}
// Cancel any ongoing host connection attempts
m.hostMu.Lock()
if m.hostConnectionCancel != nil {
m.hostConnectionCancel()
m.hostConnectionCancel = nil
}
m.hostMu.Unlock()
// Cleanup host connections
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
conn.Close()
return true
})
m.peerConnections.Clear()
// Cleanup client connection
m.hostMu.Lock()
if m.hostConnection != nil {
m.hostConnection.Close()
m.hostConnection = nil
}
m.hostMu.Unlock()
// Run cleanup functions
for _, cleanup := range m.cleanups {
cleanup()
}
}
// RegisterMessageHandler registers a custom message handler
func (m *Manager) RegisterMessageHandler(msgType MessageType, handler func(*Message, string) error) {
m.handlerMu.Lock()
defer m.handlerMu.Unlock()
m.messageHandlers[msgType] = handler
}
// SendMessage sends a message to all connected peers (when acting as host)
func (m *Manager) SendMessage(msgType MessageType, payload interface{}) error {
if !m.settings.IsHost {
return errors.New("not acting as host")
}
message := &Message{
Type: msgType,
Payload: payload,
Timestamp: time.Now(),
}
var lastError error
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
if err := conn.SendMessage(message); err != nil {
m.logger.Error().Err(err).Str("peerId", conn.PeerId).Msg("nakama: Failed to send message to peer")
lastError = err
}
return true
})
return lastError
}
// SendMessageToPeer sends a message to a specific peer by their PeerID
func (m *Manager) SendMessageToPeer(peerID string, msgType MessageType, payload interface{}) error {
if !m.settings.IsHost {
return errors.New("only hosts can send messages to peers")
}
// Find peer by PeerID
var targetConn *PeerConnection
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
if conn.PeerId == peerID {
targetConn = conn
return false // Stop iteration
}
return true
})
if targetConn == nil {
return errors.New("peer not found: " + peerID)
}
message := &Message{
Type: msgType,
Payload: payload,
Timestamp: time.Now(),
}
return targetConn.SendMessage(message)
}
// SendMessageToHost sends a message to the host (when acting as peer)
func (m *Manager) SendMessageToHost(msgType MessageType, payload interface{}) error {
m.hostMu.RLock()
defer m.hostMu.RUnlock()
if m.hostConnection == nil || !m.hostConnection.Authenticated {
return errors.New("not connected to host")
}
message := &Message{
Type: msgType,
Payload: payload,
Timestamp: time.Now(),
}
return m.hostConnection.SendMessage(message)
}
// GetConnectedPeers returns a list of connected peer IDs
func (m *Manager) GetConnectedPeers() []string {
if !m.settings.IsHost {
return []string{}
}
peers := make([]string, 0)
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
if conn.Authenticated {
// Use PeerID as the primary identifier
peerDisplayName := conn.Username
if peerDisplayName == "" {
peerDisplayName = "Unknown"
}
// Format: "Username (PeerID_short)"
// peers = append(peers, fmt.Sprintf("%s (%s)", peerDisplayName, conn.PeerId[:8]))
peers = append(peers, fmt.Sprintf("%s", peerDisplayName))
}
return true
})
return peers
}
// IsConnectedToHost returns whether this instance is connected to a host
func (m *Manager) IsConnectedToHost() bool {
m.hostMu.RLock()
defer m.hostMu.RUnlock()
return m.hostConnection != nil && m.hostConnection.Authenticated
}
// GetHostConnectionStatus returns the status of the host connection
func (m *Manager) GetHostConnectionStatus() *HostConnectionStatus {
m.hostMu.RLock()
defer m.hostMu.RUnlock()
if m.hostConnection == nil {
return nil
}
return &HostConnectionStatus{
Connected: m.hostConnection != nil,
Authenticated: m.hostConnection != nil && m.hostConnection.Authenticated,
URL: m.hostConnection.URL,
LastPing: m.hostConnection.LastPing,
PeerId: m.hostConnection.PeerId,
Username: m.hostConnection.Username,
}
}
func (pc *PeerConnection) SendMessage(message *Message) error {
pc.mu.Lock()
defer pc.mu.Unlock()
return pc.Conn.WriteJSON(message)
}
func (pc *PeerConnection) Close() {
pc.mu.Lock()
defer pc.mu.Unlock()
_ = pc.Conn.Close()
}
func (hc *HostConnection) SendMessage(message *Message) error {
hc.mu.Lock()
defer hc.mu.Unlock()
return hc.Conn.WriteJSON(message)
}
func (hc *HostConnection) Close() {
hc.mu.Lock()
defer hc.mu.Unlock()
if hc.reconnectTimer != nil {
hc.reconnectTimer.Stop()
}
_ = hc.Conn.Close()
}
// Helper function to generate connection IDs
func generateConnectionID() string {
return fmt.Sprintf("conn_%d", time.Now().UnixNano())
}
// ReconnectToHost attempts to reconnect to the host
func (m *Manager) ReconnectToHost() error {
if m.settings == nil || m.settings.RemoteServerURL == "" || m.settings.RemoteServerPassword == "" {
return errors.New("no host connection configured")
}
// Check if already reconnecting
m.hostMu.Lock()
if m.reconnecting {
m.hostMu.Unlock()
return errors.New("reconnection already in progress")
}
m.hostMu.Unlock()
m.logger.Info().Msg("nakama: Manual reconnection to host requested")
// Disconnect current connection if exists
m.disconnectFromHost()
// Wait a moment before reconnecting
time.Sleep(1 * time.Second)
// Reconnect
m.connectToHost()
return nil
}
// RemoveStaleConnections removes connections that haven't responded to ping in a while
func (m *Manager) RemoveStaleConnections() {
if !m.settings.IsHost {
return
}
staleThreshold := 90 * time.Second // Consider connections stale after 90 seconds of no ping
now := time.Now()
var staleConnections []string
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
conn.mu.RLock()
lastPing := conn.LastPing
authenticated := conn.Authenticated
conn.mu.RUnlock()
// Only check authenticated connections
if !authenticated {
return true
}
// If LastPing is zero, use connection time as reference
if lastPing.IsZero() {
lastPing = now.Add(-staleThreshold - time.Minute)
}
if now.Sub(lastPing) > staleThreshold {
staleConnections = append(staleConnections, id)
}
return true
})
// Remove stale connections
for _, id := range staleConnections {
if conn, exists := m.peerConnections.Get(id); exists {
// Double-check to avoid race conditions
conn.mu.RLock()
lastPing := conn.LastPing
if lastPing.IsZero() {
lastPing = now.Add(-staleThreshold - time.Minute)
}
isStale := now.Sub(lastPing) > staleThreshold
conn.mu.RUnlock()
if isStale {
m.logger.Info().Str("peerId", conn.PeerId).Str("internalConnID", id).Msg("nakama: Removing stale peer connection")
// Remove from map first to prevent re-addition
m.peerConnections.Delete(id)
// Remove peer from watch party if they were participating
m.watchPartyManager.HandlePeerDisconnected(conn.PeerId)
// Then close the connection (this will trigger the defer cleanup in handlePeerConnection)
conn.Close()
// Send event about peer disconnection
m.wsEventManager.SendEvent(events.NakamaPeerDisconnected, map[string]interface{}{
"peerId": conn.PeerId,
"reason": "stale_connection",
})
}
}
}
if len(staleConnections) > 0 {
m.logger.Info().Int("count", len(staleConnections)).Msg("nakama: Removed stale peer connections")
}
}
// FindPeerByPeerID finds a peer connection by their PeerID
func (m *Manager) FindPeerByPeerID(peerID string) (*PeerConnection, bool) {
var found *PeerConnection
m.peerConnections.Range(func(id string, conn *PeerConnection) bool {
if conn.PeerId == peerID {
found = conn
return false // Stop iteration
}
return true
})
return found, found != nil
}