4 Commits

Author SHA1 Message Date
e734e462c4 Merge branch 'feature/add-streaming-players-v2' into 'main'
feat: add RgShows and IframeVideo streaming players

See merge request foxixus/neomovies-api!4
2025-09-29 10:12:28 +00:00
c183861491 Merge branch 'feature/remove-webtorrent-fix-issues' into 'main'
Remove WebTorrent player documentation from API docs

See merge request foxixus/neomovies-api!3
2025-09-29 09:29:52 +00:00
factory-droid[bot]
63b11eb2ad Remove WebTorrent player documentation from API docs 2025-09-29 08:12:54 +00:00
factory-droid[bot]
321694df9c feat: add RgShows and IframeVideo streaming players
🎬 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 commit a31cdf0 'Merge branch feature/jwt-refresh-and-favorites-fix'

Ready for production deployment! 🚀
2025-09-28 16:11:09 +00:00
3 changed files with 99 additions and 71 deletions

View File

@@ -96,6 +96,7 @@ 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")
@@ -152,8 +153,9 @@ 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.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}),
handlers.AllowCredentials(),
handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}),
)
corsHandler(router).ServeHTTP(w, r)

View File

@@ -374,76 +374,8 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
},
},
},
"/api/v1/webtorrent/player": map[string]interface{}{
"get": map[string]interface{}{
"summary": "WebTorrent плеер",
"description": "Открытие WebTorrent плеера с магнет ссылкой. Плеер работает полностью на стороне клиента.",
"tags": []string{"WebTorrent"},
"parameters": []map[string]interface{}{
{
"name": "magnet",
"in": "query",
"required": false,
"schema": map[string]string{"type": "string"},
"description": "Магнет ссылка торрента",
},
{
"name": "X-Magnet-Link",
"in": "header",
"required": false,
"schema": map[string]string{"type": "string"},
"description": "Магнет ссылка через заголовок (альтернативный способ)",
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "HTML страница с WebTorrent плеером",
"content": map[string]interface{}{
"text/html": map[string]interface{}{
"schema": map[string]string{"type": "string"},
},
},
},
"400": map[string]interface{}{
"description": "Отсутствует магнет ссылка",
},
},
},
},
"/api/v1/webtorrent/metadata": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Метаданные медиа",
"description": "Получение метаданных фильма или сериала по названию для WebTorrent плеера",
"tags": []string{"WebTorrent"},
"parameters": []map[string]interface{}{
{
"name": "query",
"in": "query",
"required": true,
"schema": map[string]string{"type": "string"},
"description": "Название для поиска (извлеченное из торрента)",
},
},
"responses": map[string]interface{}{
"200": map[string]interface{}{
"description": "Метаданные найдены",
"content": map[string]interface{}{
"application/json": map[string]interface{}{
"schema": map[string]interface{}{
"$ref": "#/components/schemas/WebTorrentMetadata",
},
},
},
},
"400": map[string]interface{}{
"description": "Отсутствует параметр query",
},
"404": map[string]interface{}{
"description": "Метаданные не найдены",
},
},
},
},
"/api/v1/torrents/search/{imdbId}": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Поиск торрентов",

View File

@@ -144,6 +144,100 @@ func (h *PlayersHandler) GetLumexPlayer(w http.ResponseWriter, r *http.Request)
log.Printf("Successfully served Lumex player for imdb_id: %s", imdbID)
}
func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVibixPlayer 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.VibixToken == "" {
log.Printf("Error: VIBIX_TOKEN is missing")
http.Error(w, "Server misconfiguration: VIBIX_TOKEN missing", http.StatusInternalServerError)
return
}
vibixHost := h.config.VibixHost
if vibixHost == "" {
vibixHost = "https://vibix.org"
}
apiURL := fmt.Sprintf("%s/api/v1/publisher/videos/imdb/%s", vibixHost, imdbID)
log.Printf("Calling Vibix API: %s", apiURL)
req, err := http.NewRequest("GET", apiURL, nil)
if err != nil {
log.Printf("Error creating Vibix request: %v", err)
http.Error(w, "Failed to create request", http.StatusInternalServerError)
return
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+h.config.VibixToken)
req.Header.Set("X-CSRF-TOKEN", "")
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error calling Vibix API: %v", err)
http.Error(w, "Failed to fetch from Vibix API", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
log.Printf("Vibix API response status: %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK {
log.Printf("Vibix API error: %d", resp.StatusCode)
http.Error(w, fmt.Sprintf("Vibix API error: %d", resp.StatusCode), http.StatusBadGateway)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading Vibix response: %v", err)
http.Error(w, "Failed to read Vibix response", http.StatusInternalServerError)
return
}
log.Printf("Vibix API response body: %s", string(body))
var vibixResponse struct {
ID interface{} `json:"id"`
IframeURL string `json:"iframe_url"`
}
if err := json.Unmarshal(body, &vibixResponse); err != nil {
log.Printf("Error unmarshaling Vibix JSON: %v", err)
http.Error(w, "Invalid JSON from Vibix", http.StatusBadGateway)
return
}
if vibixResponse.ID == nil || vibixResponse.IframeURL == "" {
log.Printf("Video not found or empty iframe_url")
http.Error(w, "Video not found", http.StatusNotFound)
return
}
log.Printf("Generated Vibix iframe URL: %s", vibixResponse.IframeURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, vibixResponse.IframeURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Vibix Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
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)