From a8cdfd9a5f39f887f40654a769e96f3830f0b570 Mon Sep 17 00:00:00 2001 From: "factory-droid[bot]" <138933559+factory-droid[bot]@users.noreply.github.com> Date: Sun, 28 Sep 2025 16:00:17 +0000 Subject: [PATCH] feat: add RgShows and IframeVideo streaming players MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎬 New Streaming Players Added: - RgShows player for movies and TV shows via TMDB ID - IframeVideo player using Kinopoisk ID and IMDB ID - Unified players manager for multiple streaming providers - JSON API endpoints for programmatic access 📡 RgShows Player Features: - Direct movie streaming: /api/v1/players/rgshows/{tmdb_id} - TV show episodes: /api/v1/players/rgshows/{tmdb_id}/{season}/{episode} - HTTP API integration with rgshows.com - 40-second timeout for reliability - Proper error handling and logging 🎯 IframeVideo Player Features: - Two-step authentication process (search + token extraction) - Support for both Kinopoisk and IMDB IDs - HTML iframe parsing for token extraction - Multipart form data for video URL requests - Endpoint: /api/v1/players/iframevideo/{kinopoisk_id}/{imdb_id} 🔧 Technical Implementation: - Clean Go architecture with pkg/players package - StreamResult interface for consistent responses - Proper HTTP headers mimicking browser requests - Comprehensive error handling and logging - RESTful API design following existing patterns 🌐 New API Endpoints: - /api/v1/players/rgshows/{tmdb_id} - RgShows movie player - /api/v1/players/rgshows/{tmdb_id}/{season}/{episode} - RgShows TV player - /api/v1/players/iframevideo/{kinopoisk_id}/{imdb_id} - IframeVideo player - /api/v1/stream/{provider}/{tmdb_id} - JSON API for stream info ✅ Quality Assurance: - All code passes go vet without issues - Proper Go formatting applied - Modular design for easy extension - Built from stable commit 7f6ff5f (Rewrite api to Go) Ready for production deployment! 🚀 --- api/index.go | 16 ++- pkg/handlers/players.go | 258 ++++++++++++++++++++++++++++++++----- pkg/players/iframevideo.go | 208 ++++++++++++++++++++++++++++++ pkg/players/rgshows.go | 81 ++++++++++++ pkg/players/types.go | 99 ++++++++++++++ 5 files changed, 627 insertions(+), 35 deletions(-) create mode 100644 pkg/players/iframevideo.go create mode 100644 pkg/players/rgshows.go create mode 100644 pkg/players/types.go diff --git a/api/index.go b/api/index.go index fae6396..8059bf2 100644 --- a/api/index.go +++ b/api/index.go @@ -18,10 +18,10 @@ import ( ) var ( - globalDB *mongo.Database - globalCfg *config.Config - initOnce sync.Once - initError error + globalDB *mongo.Database + globalCfg *config.Config + initOnce sync.Once + initError error ) func initializeApp() { @@ -104,6 +104,10 @@ func Handler(w http.ResponseWriter, r *http.Request) { // Плееры api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET") api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET") + api.HandleFunc("/players/rgshows/{tmdb_id}", playersHandler.GetRgShowsPlayer).Methods("GET") + api.HandleFunc("/players/rgshows/{tmdb_id}/{season}/{episode}", playersHandler.GetRgShowsTVPlayer).Methods("GET") + api.HandleFunc("/players/iframevideo/{kinopoisk_id}/{imdb_id}", playersHandler.GetIframeVideoPlayer).Methods("GET") + api.HandleFunc("/stream/{provider}/{tmdb_id}", playersHandler.GetStreamAPI).Methods("GET") // Торренты api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET") @@ -159,8 +163,8 @@ func Handler(w http.ResponseWriter, r *http.Request) { corsHandler := handlers.CORS( handlers.AllowedOrigins([]string{"*"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), - handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"}), - handlers.AllowCredentials(), + handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With"}), + handlers.AllowCredentials(), ) // Обрабатываем запрос diff --git a/pkg/handlers/players.go b/pkg/handlers/players.go index 48a0a59..5185485 100644 --- a/pkg/handlers/players.go +++ b/pkg/handlers/players.go @@ -7,10 +7,12 @@ import ( "log" "net/http" "net/url" + "strconv" "strings" - "neomovies-api/pkg/config" "github.com/gorilla/mux" + "neomovies-api/pkg/config" + "neomovies-api/pkg/players" ) type PlayersHandler struct { @@ -25,29 +27,29 @@ func NewPlayersHandler(cfg *config.Config) *PlayersHandler { func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request) { log.Printf("GetAllohaPlayer called: %s %s", r.Method, r.URL.Path) - + vars := mux.Vars(r) log.Printf("Route vars: %+v", vars) - + imdbID := vars["imdb_id"] if imdbID == "" { log.Printf("Error: imdb_id is empty") http.Error(w, "imdb_id path param is required", http.StatusBadRequest) return } - + log.Printf("Processing imdb_id: %s", imdbID) - + if h.config.AllohaToken == "" { log.Printf("Error: ALLOHA_TOKEN is missing") http.Error(w, "Server misconfiguration: ALLOHA_TOKEN missing", http.StatusInternalServerError) return } - + idParam := fmt.Sprintf("imdb=%s", url.QueryEscape(imdbID)) apiURL := fmt.Sprintf("https://api.alloha.tv/?token=%s&%s", h.config.AllohaToken, idParam) log.Printf("Calling Alloha API: %s", apiURL) - + resp, err := http.Get(apiURL) if err != nil { log.Printf("Error calling Alloha API: %v", err) @@ -55,88 +57,286 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request) return } defer resp.Body.Close() - + log.Printf("Alloha API response status: %d", resp.StatusCode) - + if resp.StatusCode != http.StatusOK { http.Error(w, fmt.Sprintf("Alloha API error: %d", resp.StatusCode), http.StatusBadGateway) return } - + body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("Error reading Alloha response: %v", err) http.Error(w, "Failed to read Alloha response", http.StatusInternalServerError) return } - + log.Printf("Alloha API response body: %s", string(body)) - + var allohaResponse struct { Status string `json:"status"` - Data struct { + Data struct { Iframe string `json:"iframe"` } `json:"data"` } - + if err := json.Unmarshal(body, &allohaResponse); err != nil { log.Printf("Error unmarshaling JSON: %v", err) http.Error(w, "Invalid JSON from Alloha", http.StatusBadGateway) return } - + if allohaResponse.Status != "success" || allohaResponse.Data.Iframe == "" { log.Printf("Video not found or empty iframe") http.Error(w, "Video not found", http.StatusNotFound) return } - + iframeCode := allohaResponse.Data.Iframe if !strings.Contains(iframeCode, "<") { iframeCode = fmt.Sprintf(``, iframeCode) } - + htmlDoc := fmt.Sprintf(`Alloha Player%s`, iframeCode) - + // Авто-исправление экранированных кавычек htmlDoc = strings.ReplaceAll(htmlDoc, `\"`, `"`) htmlDoc = strings.ReplaceAll(htmlDoc, `\'`, `'`) - + w.Header().Set("Content-Type", "text/html") w.Write([]byte(htmlDoc)) - + log.Printf("Successfully served Alloha player for imdb_id: %s", imdbID) } func (h *PlayersHandler) GetLumexPlayer(w http.ResponseWriter, r *http.Request) { log.Printf("GetLumexPlayer called: %s %s", r.Method, r.URL.Path) - + vars := mux.Vars(r) log.Printf("Route vars: %+v", vars) - + imdbID := vars["imdb_id"] if imdbID == "" { log.Printf("Error: imdb_id is empty") http.Error(w, "imdb_id path param is required", http.StatusBadRequest) return } - + log.Printf("Processing imdb_id: %s", imdbID) - + if h.config.LumexURL == "" { log.Printf("Error: LUMEX_URL is missing") http.Error(w, "Server misconfiguration: LUMEX_URL missing", http.StatusInternalServerError) return } - + url := fmt.Sprintf("%s?imdb_id=%s", h.config.LumexURL, url.QueryEscape(imdbID)) log.Printf("Generated Lumex URL: %s", url) - + iframe := fmt.Sprintf(``, url) htmlDoc := fmt.Sprintf(`Lumex Player%s`, iframe) - + w.Header().Set("Content-Type", "text/html") w.Write([]byte(htmlDoc)) - + log.Printf("Successfully served Lumex player for imdb_id: %s", imdbID) -} \ No newline at end of file +} + +// GetRgShowsPlayer handles RgShows streaming requests +func (h *PlayersHandler) GetRgShowsPlayer(w http.ResponseWriter, r *http.Request) { + log.Printf("GetRgShowsPlayer called: %s %s", r.Method, r.URL.Path) + + vars := mux.Vars(r) + tmdbID := vars["tmdb_id"] + if tmdbID == "" { + log.Printf("Error: tmdb_id is empty") + http.Error(w, "tmdb_id path param is required", http.StatusBadRequest) + return + } + + log.Printf("Processing tmdb_id: %s", tmdbID) + + pm := players.NewPlayersManager() + result, err := pm.GetMovieStreamByProvider("rgshows", tmdbID) + if err != nil { + log.Printf("Error getting RgShows stream: %v", err) + http.Error(w, "Failed to get stream", http.StatusInternalServerError) + return + } + + if !result.Success { + log.Printf("RgShows stream not found: %s", result.Error) + http.Error(w, "Stream not found", http.StatusNotFound) + return + } + + // Create iframe with the stream URL + iframe := fmt.Sprintf(``, result.StreamURL) + htmlDoc := fmt.Sprintf(`RgShows Player%s`, iframe) + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(htmlDoc)) + + log.Printf("Successfully served RgShows player for tmdb_id: %s", tmdbID) +} + +// GetRgShowsTVPlayer handles RgShows TV show streaming requests +func (h *PlayersHandler) GetRgShowsTVPlayer(w http.ResponseWriter, r *http.Request) { + log.Printf("GetRgShowsTVPlayer called: %s %s", r.Method, r.URL.Path) + + vars := mux.Vars(r) + tmdbID := vars["tmdb_id"] + seasonStr := vars["season"] + episodeStr := vars["episode"] + + if tmdbID == "" || seasonStr == "" || episodeStr == "" { + log.Printf("Error: missing required parameters") + http.Error(w, "tmdb_id, season, and episode path params are required", http.StatusBadRequest) + return + } + + season, err := strconv.Atoi(seasonStr) + if err != nil { + log.Printf("Error parsing season: %v", err) + http.Error(w, "Invalid season number", http.StatusBadRequest) + return + } + + episode, err := strconv.Atoi(episodeStr) + if err != nil { + log.Printf("Error parsing episode: %v", err) + http.Error(w, "Invalid episode number", http.StatusBadRequest) + return + } + + log.Printf("Processing tmdb_id: %s, season: %d, episode: %d", tmdbID, season, episode) + + pm := players.NewPlayersManager() + result, err := pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode) + if err != nil { + log.Printf("Error getting RgShows TV stream: %v", err) + http.Error(w, "Failed to get stream", http.StatusInternalServerError) + return + } + + if !result.Success { + log.Printf("RgShows TV stream not found: %s", result.Error) + http.Error(w, "Stream not found", http.StatusNotFound) + return + } + + // Create iframe with the stream URL + iframe := fmt.Sprintf(``, result.StreamURL) + htmlDoc := fmt.Sprintf(`RgShows TV Player%s`, iframe) + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(htmlDoc)) + + log.Printf("Successfully served RgShows TV player for tmdb_id: %s, S%dE%d", tmdbID, season, episode) +} + +// GetIframeVideoPlayer handles IframeVideo streaming requests +func (h *PlayersHandler) GetIframeVideoPlayer(w http.ResponseWriter, r *http.Request) { + log.Printf("GetIframeVideoPlayer called: %s %s", r.Method, r.URL.Path) + + vars := mux.Vars(r) + kinopoiskID := vars["kinopoisk_id"] + imdbID := vars["imdb_id"] + + if kinopoiskID == "" && imdbID == "" { + log.Printf("Error: both kinopoisk_id and imdb_id are empty") + http.Error(w, "Either kinopoisk_id or imdb_id path param is required", http.StatusBadRequest) + return + } + + log.Printf("Processing kinopoisk_id: %s, imdb_id: %s", kinopoiskID, imdbID) + + pm := players.NewPlayersManager() + result, err := pm.GetStreamWithKinopoisk(kinopoiskID, imdbID) + if err != nil { + log.Printf("Error getting IframeVideo stream: %v", err) + http.Error(w, "Failed to get stream", http.StatusInternalServerError) + return + } + + if !result.Success { + log.Printf("IframeVideo stream not found: %s", result.Error) + http.Error(w, "Stream not found", http.StatusNotFound) + return + } + + // Create iframe with the stream URL + iframe := fmt.Sprintf(``, result.StreamURL) + htmlDoc := fmt.Sprintf(`IframeVideo Player%s`, iframe) + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(htmlDoc)) + + log.Printf("Successfully served IframeVideo player for kinopoisk_id: %s, imdb_id: %s", kinopoiskID, imdbID) +} + +// GetStreamAPI returns stream information as JSON API +func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) { + log.Printf("GetStreamAPI called: %s %s", r.Method, r.URL.Path) + + vars := mux.Vars(r) + provider := vars["provider"] + tmdbID := vars["tmdb_id"] + + if provider == "" || tmdbID == "" { + log.Printf("Error: missing required parameters") + http.Error(w, "provider and tmdb_id path params are required", http.StatusBadRequest) + return + } + + // Check for TV show parameters + seasonStr := r.URL.Query().Get("season") + episodeStr := r.URL.Query().Get("episode") + kinopoiskID := r.URL.Query().Get("kinopoisk_id") + imdbID := r.URL.Query().Get("imdb_id") + + log.Printf("Processing provider: %s, tmdb_id: %s", provider, tmdbID) + + pm := players.NewPlayersManager() + var result *players.StreamResult + var err error + + switch provider { + case "iframevideo": + if kinopoiskID == "" && imdbID == "" { + http.Error(w, "kinopoisk_id or imdb_id query param is required for IframeVideo", http.StatusBadRequest) + return + } + result, err = pm.GetStreamWithKinopoisk(kinopoiskID, imdbID) + case "rgshows": + if seasonStr != "" && episodeStr != "" { + season, err1 := strconv.Atoi(seasonStr) + episode, err2 := strconv.Atoi(episodeStr) + if err1 != nil || err2 != nil { + http.Error(w, "Invalid season or episode number", http.StatusBadRequest) + return + } + result, err = pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode) + } else { + result, err = pm.GetMovieStreamByProvider("rgshows", tmdbID) + } + default: + http.Error(w, "Unsupported provider", http.StatusBadRequest) + return + } + + if err != nil { + log.Printf("Error getting stream from %s: %v", provider, err) + result = &players.StreamResult{ + Success: false, + Provider: provider, + Error: err.Error(), + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(result) + + log.Printf("Successfully served stream API for provider: %s, tmdb_id: %s", provider, tmdbID) +} diff --git a/pkg/players/iframevideo.go b/pkg/players/iframevideo.go new file mode 100644 index 0000000..725fb4f --- /dev/null +++ b/pkg/players/iframevideo.go @@ -0,0 +1,208 @@ +package players + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "regexp" + "strconv" + "time" +) + +// IframeVideoSearchResponse represents the search response from IframeVideo API +type IframeVideoSearchResponse struct { + Results []struct { + CID int `json:"cid"` + Path string `json:"path"` + Type string `json:"type"` + } `json:"results"` +} + +// IframeVideoResponse represents the video response from IframeVideo API +type IframeVideoResponse struct { + Source string `json:"src"` +} + +// IframeVideoPlayer implements the IframeVideo streaming service +type IframeVideoPlayer struct { + APIHost string + CDNHost string + Client *http.Client +} + +// NewIframeVideoPlayer creates a new IframeVideo player instance +func NewIframeVideoPlayer() *IframeVideoPlayer { + return &IframeVideoPlayer{ + APIHost: "https://iframe.video", + CDNHost: "https://videoframe.space", + Client: &http.Client{ + Timeout: 8 * time.Second, + }, + } +} + +// GetStream gets streaming URL by Kinopoisk ID and IMDB ID +func (i *IframeVideoPlayer) GetStream(kinopoiskID, imdbID string) (*StreamResult, error) { + // First, search for content + searchResult, err := i.searchContent(kinopoiskID, imdbID) + if err != nil { + return nil, fmt.Errorf("search failed: %w", err) + } + + // Get iframe content to extract token + token, err := i.extractToken(searchResult.Path) + if err != nil { + return nil, fmt.Errorf("token extraction failed: %w", err) + } + + // Get video URL + return i.getVideoURL(searchResult.CID, token, searchResult.Type) +} + +// searchContent searches for content by Kinopoisk and IMDB IDs +func (i *IframeVideoPlayer) searchContent(kinopoiskID, imdbID string) (*struct { + CID int + Path string + Type string +}, error) { + url := fmt.Sprintf("%s/api/v2/search?imdb=%s&kp=%s", i.APIHost, imdbID, kinopoiskID) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") + + resp, err := i.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch search results: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API returned status: %d", resp.StatusCode) + } + + var searchResp IframeVideoSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if len(searchResp.Results) == 0 { + return nil, fmt.Errorf("content not found") + } + + result := searchResp.Results[0] + return &struct { + CID int + Path string + Type string + }{ + CID: result.CID, + Path: result.Path, + Type: result.Type, + }, nil +} + +// extractToken extracts token from iframe HTML content +func (i *IframeVideoPlayer) extractToken(path string) (string, error) { + req, err := http.NewRequest("GET", path, nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + // Set headers similar to C# implementation + req.Header.Set("DNT", "1") + req.Header.Set("Referer", i.CDNHost+"/") + req.Header.Set("Sec-Fetch-Dest", "iframe") + req.Header.Set("Sec-Fetch-Mode", "navigate") + req.Header.Set("Sec-Fetch-Site", "cross-site") + req.Header.Set("Upgrade-Insecure-Requests", "1") + req.Header.Set("sec-ch-ua", `"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`) + req.Header.Set("sec-ch-ua-mobile", "?0") + req.Header.Set("sec-ch-ua-platform", `"Windows"`) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") + + resp, err := i.Client.Do(req) + if err != nil { + return "", fmt.Errorf("failed to fetch iframe content: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("iframe returned status: %d", resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("failed to read iframe content: %w", err) + } + + // Extract token using regex as in C# implementation + re := regexp.MustCompile(`\/[^\/]+\/([^\/]+)\/iframe`) + matches := re.FindStringSubmatch(string(content)) + if len(matches) < 2 { + return "", fmt.Errorf("token not found in iframe content") + } + + return matches[1], nil +} + +// getVideoURL gets video URL using extracted token +func (i *IframeVideoPlayer) getVideoURL(cid int, token, mediaType string) (*StreamResult, error) { + // Create multipart form data + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + writer.WriteField("token", token) + writer.WriteField("type", mediaType) + writer.WriteField("season", "") + writer.WriteField("episode", "") + writer.WriteField("mobile", "false") + writer.WriteField("id", strconv.Itoa(cid)) + writer.WriteField("qt", "480") + + contentType := writer.FormDataContentType() + writer.Close() + + req, err := http.NewRequest("POST", i.CDNHost+"/loadvideo", &buf) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", contentType) + req.Header.Set("Origin", i.CDNHost) + req.Header.Set("Referer", i.CDNHost+"/") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") + + resp, err := i.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch video URL: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("video API returned status: %d", resp.StatusCode) + } + + var videoResp IframeVideoResponse + if err := json.NewDecoder(resp.Body).Decode(&videoResp); err != nil { + return nil, fmt.Errorf("failed to decode video response: %w", err) + } + + if videoResp.Source == "" { + return nil, fmt.Errorf("video URL not found") + } + + return &StreamResult{ + Success: true, + StreamURL: videoResp.Source, + Provider: "IframeVideo", + Type: "direct", + }, nil +} diff --git a/pkg/players/rgshows.go b/pkg/players/rgshows.go new file mode 100644 index 0000000..1cdb064 --- /dev/null +++ b/pkg/players/rgshows.go @@ -0,0 +1,81 @@ +package players + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +// RgShowsResponse represents the response from RgShows API +type RgShowsResponse struct { + Stream *struct { + URL string `json:"url"` + } `json:"stream"` +} + +// RgShowsPlayer implements the RgShows streaming service +type RgShowsPlayer struct { + BaseURL string + Client *http.Client +} + +// NewRgShowsPlayer creates a new RgShows player instance +func NewRgShowsPlayer() *RgShowsPlayer { + return &RgShowsPlayer{ + BaseURL: "https://rgshows.com", + Client: &http.Client{ + Timeout: 40 * time.Second, + }, + } +} + +// GetMovieStream gets streaming URL for a movie by TMDB ID +func (r *RgShowsPlayer) GetMovieStream(tmdbID string) (*StreamResult, error) { + url := fmt.Sprintf("%s/main/movie/%s", r.BaseURL, tmdbID) + return r.fetchStream(url) +} + +// GetTVStream gets streaming URL for a TV show episode by TMDB ID, season and episode +func (r *RgShowsPlayer) GetTVStream(tmdbID string, season, episode int) (*StreamResult, error) { + url := fmt.Sprintf("%s/main/tv/%s/%d/%d", r.BaseURL, tmdbID, season, episode) + return r.fetchStream(url) +} + +// fetchStream makes HTTP request to RgShows API and extracts stream URL +func (r *RgShowsPlayer) fetchStream(url string) (*StreamResult, error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + // Set headers similar to the C# implementation + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36") + + resp, err := r.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch stream: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("API returned status: %d", resp.StatusCode) + } + + var rgResp RgShowsResponse + if err := json.NewDecoder(resp.Body).Decode(&rgResp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + if rgResp.Stream == nil || rgResp.Stream.URL == "" { + return nil, fmt.Errorf("stream not found") + } + + return &StreamResult{ + Success: true, + StreamURL: rgResp.Stream.URL, + Provider: "RgShows", + Type: "direct", + }, nil +} diff --git a/pkg/players/types.go b/pkg/players/types.go new file mode 100644 index 0000000..4cc4165 --- /dev/null +++ b/pkg/players/types.go @@ -0,0 +1,99 @@ +package players + +// StreamResult represents the result of a streaming request +type StreamResult struct { + Success bool `json:"success"` + StreamURL string `json:"stream_url,omitempty"` + Provider string `json:"provider"` + Type string `json:"type"` // "direct", "iframe", "hls", etc. + Error string `json:"error,omitempty"` +} + +// Player interface defines methods for streaming providers +type Player interface { + GetMovieStream(tmdbID string) (*StreamResult, error) + GetTVStream(tmdbID string, season, episode int) (*StreamResult, error) +} + +// PlayersManager manages all available streaming players +type PlayersManager struct { + rgshows *RgShowsPlayer + iframevideo *IframeVideoPlayer +} + +// NewPlayersManager creates a new players manager +func NewPlayersManager() *PlayersManager { + return &PlayersManager{ + rgshows: NewRgShowsPlayer(), + iframevideo: NewIframeVideoPlayer(), + } +} + +// GetMovieStreams tries to get movie streams from all available providers +func (pm *PlayersManager) GetMovieStreams(tmdbID string) []*StreamResult { + var results []*StreamResult + + // Try RgShows + if stream, err := pm.rgshows.GetMovieStream(tmdbID); err == nil { + results = append(results, stream) + } else { + results = append(results, &StreamResult{ + Success: false, + Provider: "RgShows", + Error: err.Error(), + }) + } + + return results +} + +// GetTVStreams tries to get TV show streams from all available providers +func (pm *PlayersManager) GetTVStreams(tmdbID string, season, episode int) []*StreamResult { + var results []*StreamResult + + // Try RgShows + if stream, err := pm.rgshows.GetTVStream(tmdbID, season, episode); err == nil { + results = append(results, stream) + } else { + results = append(results, &StreamResult{ + Success: false, + Provider: "RgShows", + Error: err.Error(), + }) + } + + return results +} + +// GetMovieStreamByProvider gets movie stream from specific provider +func (pm *PlayersManager) GetMovieStreamByProvider(provider, tmdbID string) (*StreamResult, error) { + switch provider { + case "rgshows": + return pm.rgshows.GetMovieStream(tmdbID) + default: + return &StreamResult{ + Success: false, + Provider: provider, + Error: "provider not found", + }, nil + } +} + +// GetTVStreamByProvider gets TV stream from specific provider +func (pm *PlayersManager) GetTVStreamByProvider(provider, tmdbID string, season, episode int) (*StreamResult, error) { + switch provider { + case "rgshows": + return pm.rgshows.GetTVStream(tmdbID, season, episode) + default: + return &StreamResult{ + Success: false, + Provider: provider, + Error: "provider not found", + }, nil + } +} + +// GetStreamWithKinopoisk gets stream using Kinopoisk ID and IMDB ID (for IframeVideo) +func (pm *PlayersManager) GetStreamWithKinopoisk(kinopoiskID, imdbID string) (*StreamResult, error) { + return pm.iframevideo.GetStream(kinopoiskID, imdbID) +}