package api import ( "encoding/json" "log" "net/http" "sync" "tuner/m3u" "tuner/models" "tuner/process" ) // App holds shared application state for all handlers. type App struct { mu sync.RWMutex Channels []models.Channel Status models.StreamStatus PlaylistPath string MediaMTX *process.MediaMTXManager FFmpeg *process.FFmpegManager } // HandleChannels lists, searches, and filters channels. // GET /api/admin/channels?search=&group= func (a *App) HandleChannels(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } search := r.URL.Query().Get("search") group := r.URL.Query().Get("group") a.mu.RLock() channels := m3u.FilterChannels(a.Channels, search, group) a.mu.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(channels) } // HandleGroups returns the list of channel groups. // GET /api/admin/groups func (a *App) HandleGroups(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } a.mu.RLock() groups := m3u.GetGroups(a.Channels) a.mu.RUnlock() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(groups) } // HandleSetSource sets the active source (obs or iptv). // POST /api/admin/source {"source": "obs"|"iptv"} func (a *App) HandleSetSource(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req struct { Source string `json:"source"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } if req.Source != "obs" && req.Source != "iptv" { http.Error(w, `source must be "obs" or "iptv"`, http.StatusBadRequest) return } a.mu.Lock() a.Status.Source = req.Source a.mu.Unlock() // If switching to OBS, stop FFmpeg relay if req.Source == "obs" { if err := a.FFmpeg.Stop(); err != nil { log.Printf("[admin] error stopping ffmpeg: %v", err) } a.mu.Lock() a.Status.ChannelName = "" a.mu.Unlock() } log.Printf("[admin] source set to %s", req.Source) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok", "source": req.Source}) } // HandleSetChannel selects an IPTV channel and starts FFmpeg relay. // POST /api/admin/channel {"channel_id": "..."} func (a *App) HandleSetChannel(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req struct { ChannelID string `json:"channel_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid json", http.StatusBadRequest) return } // Find the channel a.mu.RLock() var found *models.Channel for i := range a.Channels { if a.Channels[i].ID == req.ChannelID { found = &a.Channels[i] break } } a.mu.RUnlock() if found == nil { http.Error(w, "channel not found", http.StatusNotFound) return } // Start FFmpeg with the channel's stream URL if err := a.FFmpeg.Start(found.StreamURL); err != nil { http.Error(w, "failed to start stream: "+err.Error(), http.StatusInternalServerError) return } a.mu.Lock() a.Status.Source = "iptv" a.Status.ChannelName = found.Name a.Status.Live = true a.mu.Unlock() log.Printf("[admin] channel set to %s (%s)", found.Name, found.ID) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{"status": "ok", "channel": found.Name}) } // HandleReloadPlaylist re-parses the M3U playlist from disk. // POST /api/admin/playlist/reload func (a *App) HandleReloadPlaylist(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } channels, err := m3u.ParseFile(a.PlaylistPath) if err != nil { http.Error(w, "failed to reload playlist: "+err.Error(), http.StatusInternalServerError) return } a.mu.Lock() a.Channels = channels a.mu.Unlock() log.Printf("[admin] playlist reloaded: %d channels", len(channels)) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok", "channels": len(channels)}) } // HandleProcessStatus returns the status of managed processes. // GET /api/admin/process/status func (a *App) HandleProcessStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } status := models.SystemStatus{ MediaMTX: a.MediaMTX.Status(), FFmpeg: a.FFmpeg.Status(), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(status) }