diff --git a/api/index.go b/api/index.go
index a23a4bd..9392acf 100644
--- a/api/index.go
+++ b/api/index.go
@@ -97,6 +97,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/vibix/{imdb_id}", playersHandler.GetVibixPlayer).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")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
diff --git a/pkg/handlers/players.go b/pkg/handlers/players.go
index 9f9e8ac..8a557f2 100644
--- a/pkg/handlers/players.go
+++ b/pkg/handlers/players.go
@@ -7,11 +7,13 @@ import (
"log"
"net/http"
"net/url"
+ "strconv"
"strings"
"time"
"github.com/gorilla/mux"
"neomovies-api/pkg/config"
+ "neomovies-api/pkg/players"
)
type PlayersHandler struct {
@@ -235,3 +237,201 @@ func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request)
log.Printf("Successfully served Vibix player for imdb_id: %s", imdbID)
}
+
+// 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)
+}