This commit is contained in:
2025-11-22 19:52:58 +02:00
parent 5a4b6a9396
commit 2928c3417b
9 changed files with 3353 additions and 1145 deletions

View File

@@ -18,6 +18,12 @@ ALLOHA_TOKEN=your_alloha_token
REDAPI_BASE_URL=http://redapi.cfhttp.top REDAPI_BASE_URL=http://redapi.cfhttp.top
REDAPI_KEY=your_redapi_key REDAPI_KEY=your_redapi_key
COLLAPS_API_HOST=https://api.bhcesh.me
COLLAPS_TOKEN=your_collaps_token
VEOVEO_HOST=https://api.veoveo.app
VEOVEO_TOKEN=
JWT_SECRET=your_jwt_secret_key JWT_SECRET=your_jwt_secret_key
GMAIL_USER=your_gmail@gmail.com GMAIL_USER=your_gmail@gmail.com
@@ -28,7 +34,7 @@ GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback
BASE_URL=http://localhost:3000 BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001 FRONTEND_URL=http://localhost:5173
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"log"
"net/http" "net/http"
"sync" "sync"
@@ -34,19 +33,15 @@ func initializeApp() {
var err error var err error
globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName) globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName)
if err != nil { if err != nil {
log.Printf("Failed to connect to database: %v", err)
initError = err initError = err
return return
} }
log.Println("Successfully connected to database")
} }
func Handler(w http.ResponseWriter, r *http.Request) { func Handler(w http.ResponseWriter, r *http.Request) {
initOnce.Do(initializeApp) initOnce.Do(initializeApp)
if initError != nil { if initError != nil {
log.Printf("Initialization error: %v", initError)
http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError) http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError)
return return
} }
@@ -114,6 +109,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET") api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET")
api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET") api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET")
api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET") api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET")
api.HandleFunc("/torrents/by-title", torrentsHandler.SearchByTitle).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET") api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET") api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET")

18
main.go
View File

@@ -2,6 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os" "os"
@@ -18,6 +20,18 @@ import (
) )
func main() { func main() {
// Инициализация логирования в файл
logFile, err := os.OpenFile("neomovies-api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("Failed to open log file: %v\n", err)
} else {
// Пишем логи одновременно в файл и в консоль
multiWriter := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(multiWriter)
log.SetFlags(log.LstdFlags | log.Lshortfile)
defer logFile.Close()
}
if err := godotenv.Load(); err != nil { if err := godotenv.Load(); err != nil {
_ = err _ = err
} }
@@ -84,13 +98,15 @@ func main() {
api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET") api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET")
api.HandleFunc("/players/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET") api.HandleFunc("/players/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET") api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET")
api.HandleFunc("/players/collaps/{id_type}/{id}", playersHandler.GetCollapsPlayer).Methods("GET")
api.HandleFunc("/torrents/search/by-title", torrentsHandler.SearchByTitle).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET") api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET") api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET")
api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET") api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET")
api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET") api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET") api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET")

1988
neomovies-api.log Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,6 @@
package config package config
import ( import (
"log"
"os" "os"
) )
@@ -28,6 +27,10 @@ type Config struct {
KPAPIKey string KPAPIKey string
HDVBToken string HDVBToken string
KPAPIBaseURL string KPAPIBaseURL string
CollapsAPIHost string
CollapsToken string
VeoVeoHost string
VeoVeoToken string
} }
func New() *Config { func New() *Config {
@@ -52,21 +55,22 @@ func New() *Config {
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""), GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
FrontendURL: getEnv(EnvFrontendURL, ""), FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost), VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
KPAPIKey: getEnv(EnvKPAPIKey, ""), KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""), HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase), KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
CollapsAPIHost: getEnv("COLLAPS_API_HOST", ""),
CollapsToken: getEnv("COLLAPS_TOKEN", ""),
VeoVeoHost: getEnv("VEOVEO_HOST", ""),
VeoVeoToken: getEnv("VEOVEO_TOKEN", ""),
} }
} }
func getMongoURI() string { func getMongoURI() string {
for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} { for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} {
if value := os.Getenv(envVar); value != "" { if value := os.Getenv(envVar); value != "" {
log.Printf("DEBUG: Using %s for MongoDB connection", envVar)
return value return value
} }
} }
log.Printf("DEBUG: No MongoDB URI environment variable found")
return "" return ""
} }

View File

@@ -4,17 +4,18 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"net/url" "net/url"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/gorilla/mux"
"neomovies-api/pkg/config" "neomovies-api/pkg/config"
"neomovies-api/pkg/players" "neomovies-api/pkg/players"
"github.com/gorilla/mux"
) )
type PlayersHandler struct { type PlayersHandler struct {
@@ -28,48 +29,39 @@ func NewPlayersHandler(cfg *config.Config) *PlayersHandler {
} }
func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
idType := vars["id_type"] idType := vars["id_type"]
id := vars["id"] id := vars["id"]
if idType == "" || id == "" { if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest) http.Error(w, "id_type and id are required", http.StatusBadRequest)
return return
} }
if idType == "kinopoisk_id" { idType = "kp" } if idType == "kinopoisk_id" {
idType = "kp"
}
if idType != "kp" && idType != "imdb" { if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' (kinopoisk_id) or 'imdb'", http.StatusBadRequest) http.Error(w, "id_type must be 'kp' (kinopoisk_id) or 'imdb'", http.StatusBadRequest)
return return
} }
log.Printf("Processing %s ID: %s", idType, id)
if h.config.AllohaToken == "" { if h.config.AllohaToken == "" {
log.Printf("Error: ALLOHA_TOKEN is missing")
http.Error(w, "Server misconfiguration: ALLOHA_TOKEN missing", http.StatusInternalServerError) http.Error(w, "Server misconfiguration: ALLOHA_TOKEN missing", http.StatusInternalServerError)
return return
} }
idParam := fmt.Sprintf("%s=%s", idType, url.QueryEscape(id)) idParam := fmt.Sprintf("%s=%s", idType, url.QueryEscape(id))
apiURL := fmt.Sprintf("https://api.alloha.tv/?token=%s&%s", h.config.AllohaToken, idParam) apiURL := fmt.Sprintf("https://api.alloha.tv/?token=%s&%s", h.config.AllohaToken, idParam)
log.Printf("Calling Alloha API: %s", apiURL)
client := &http.Client{Timeout: 8 * time.Second} client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL) resp, err := client.Get(apiURL)
if err != nil { if err != nil {
log.Printf("Error calling Alloha API: %v", err)
http.Error(w, "Failed to fetch from Alloha API", http.StatusInternalServerError) http.Error(w, "Failed to fetch from Alloha API", http.StatusInternalServerError)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
log.Printf("Alloha API response status: %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
http.Error(w, fmt.Sprintf("Alloha API error: %d", resp.StatusCode), http.StatusBadGateway) http.Error(w, fmt.Sprintf("Alloha API error: %d", resp.StatusCode), http.StatusBadGateway)
return return
@@ -77,13 +69,10 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Printf("Error reading Alloha response: %v", err)
http.Error(w, "Failed to read Alloha response", http.StatusInternalServerError) http.Error(w, "Failed to read Alloha response", http.StatusInternalServerError)
return return
} }
log.Printf("Alloha API response body: %s", string(body))
var allohaResponse struct { var allohaResponse struct {
Status string `json:"status"` Status string `json:"status"`
Data struct { Data struct {
@@ -92,13 +81,11 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
} }
if err := json.Unmarshal(body, &allohaResponse); err != nil { if err := json.Unmarshal(body, &allohaResponse); err != nil {
log.Printf("Error unmarshaling JSON: %v", err)
http.Error(w, "Invalid JSON from Alloha", http.StatusBadGateway) http.Error(w, "Invalid JSON from Alloha", http.StatusBadGateway)
return return
} }
if allohaResponse.Status != "success" || allohaResponse.Data.Iframe == "" { if allohaResponse.Status != "success" || allohaResponse.Data.Iframe == "" {
log.Printf("Video not found or empty iframe")
http.Error(w, "Video not found", http.StatusNotFound) http.Error(w, "Video not found", http.StatusNotFound)
return return
} }
@@ -126,7 +113,7 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
} }
playerURL = fmt.Sprintf("%s%sseason=%s&episode=%s&translation=%s", playerURL, separator, season, episode, translation) playerURL = fmt.Sprintf("%s%sseason=%s&episode=%s&translation=%s", playerURL, separator, season, episode, translation)
} }
iframeCode = fmt.Sprintf(`<iframe src="%s" allowfullscreen style="border:none;width:100%%;height:100%%"></iframe>`, playerURL) iframeCode = fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" style="border:none;width:100%%;height:100%%"></iframe>`, playerURL)
} }
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Alloha Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframeCode) htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Alloha Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframeCode)
@@ -137,8 +124,6 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served Alloha player for %s: %s", idType, id)
} }
// GetAllohaMetaByKP returns seasons/episodes meta for Alloha by kinopoisk_id // GetAllohaMetaByKP returns seasons/episodes meta for Alloha by kinopoisk_id
@@ -256,30 +241,25 @@ func (h *PlayersHandler) GetAllohaMetaByKP(w http.ResponseWriter, r *http.Reques
} }
func (h *PlayersHandler) GetLumexPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
idType := vars["id_type"] idType := vars["id_type"]
id := vars["id"] id := vars["id"]
if idType == "" || id == "" { if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest) http.Error(w, "id_type and id are required", http.StatusBadRequest)
return return
} }
// Поддержка алиаса // Поддержка алиаса
if idType == "kinopoisk_id" { idType = "kp" } if idType == "kinopoisk_id" {
idType = "kp"
}
if idType != "kp" && idType != "imdb" { if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest) http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return return
} }
log.Printf("Processing %s ID: %s", idType, id)
if h.config.LumexURL == "" { if h.config.LumexURL == "" {
log.Printf("Error: LUMEX_URL is missing")
http.Error(w, "Server misconfiguration: LUMEX_URL missing", http.StatusInternalServerError) http.Error(w, "Server misconfiguration: LUMEX_URL missing", http.StatusInternalServerError)
return return
} }
@@ -298,40 +278,30 @@ func (h *PlayersHandler) GetLumexPlayer(w http.ResponseWriter, r *http.Request)
separator = "&" separator = "&"
} }
playerURL := fmt.Sprintf("%s%s%s=%s", h.config.LumexURL, separator, paramName, url.QueryEscape(id)) playerURL := fmt.Sprintf("%s%s%s=%s", h.config.LumexURL, separator, paramName, url.QueryEscape(id))
log.Printf("Lumex URL: %s", playerURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL) iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Lumex Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe) htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Lumex Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served Lumex player for %s: %s", idType, id)
} }
func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
idType := vars["id_type"] idType := vars["id_type"]
id := vars["id"] id := vars["id"]
if idType == "" || id == "" { if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest) http.Error(w, "id_type and id are required", http.StatusBadRequest)
return return
} }
if idType != "kp" && idType != "imdb" { if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest) http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return return
} }
log.Printf("Processing %s ID: %s", idType, id)
if h.config.VibixToken == "" { if h.config.VibixToken == "" {
log.Printf("Error: VIBIX_TOKEN is missing")
http.Error(w, "Server misconfiguration: VIBIX_TOKEN missing", http.StatusInternalServerError) http.Error(w, "Server misconfiguration: VIBIX_TOKEN missing", http.StatusInternalServerError)
return return
} }
@@ -349,11 +319,9 @@ func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request)
} }
apiURL := fmt.Sprintf("%s/api/v1/publisher/videos/%s/%s", vibixHost, endpoint, id) apiURL := fmt.Sprintf("%s/api/v1/publisher/videos/%s/%s", vibixHost, endpoint, id)
log.Printf("Calling Vibix API: %s", apiURL)
req, err := http.NewRequest("GET", apiURL, nil) req, err := http.NewRequest("GET", apiURL, nil)
if err != nil { if err != nil {
log.Printf("Error creating Vibix request: %v", err)
http.Error(w, "Failed to create request", http.StatusInternalServerError) http.Error(w, "Failed to create request", http.StatusInternalServerError)
return return
} }
@@ -365,202 +333,158 @@ func (h *PlayersHandler) GetVibixPlayer(w http.ResponseWriter, r *http.Request)
client := &http.Client{Timeout: 8 * time.Second} client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("Error calling Vibix API: %v", err)
http.Error(w, "Failed to fetch from Vibix API", http.StatusInternalServerError) http.Error(w, "Failed to fetch from Vibix API", http.StatusInternalServerError)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
log.Printf("Vibix API response status: %d", resp.StatusCode)
if resp.StatusCode != http.StatusOK { 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) http.Error(w, fmt.Sprintf("Vibix API error: %d", resp.StatusCode), http.StatusBadGateway)
return return
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
log.Printf("Error reading Vibix response: %v", err)
http.Error(w, "Failed to read Vibix response", http.StatusInternalServerError) http.Error(w, "Failed to read Vibix response", http.StatusInternalServerError)
return return
} }
log.Printf("Vibix API response body: %s", string(body))
var vibixResponse struct { var vibixResponse struct {
ID interface{} `json:"id"` ID interface{} `json:"id"`
IframeURL string `json:"iframe_url"` IframeURL string `json:"iframe_url"`
} }
if err := json.Unmarshal(body, &vibixResponse); err != nil { 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) http.Error(w, "Invalid JSON from Vibix", http.StatusBadGateway)
return return
} }
if vibixResponse.ID == nil || vibixResponse.IframeURL == "" { if vibixResponse.ID == nil || vibixResponse.IframeURL == "" {
log.Printf("Video not found or empty iframe_url")
http.Error(w, "Video not found", http.StatusNotFound) http.Error(w, "Video not found", http.StatusNotFound)
return return
} }
// Vibix использует только iframe_url без season/episode // Vibix использует только iframe_url без season/episode
playerURL := vibixResponse.IframeURL playerURL := vibixResponse.IframeURL
log.Printf("🔗 Vibix iframe URL: %s", playerURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL) iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL)
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) 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.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vibix player for %s: %s", idType, id)
} }
// GetRgShowsPlayer handles RgShows streaming requests // GetRgShowsPlayer handles RgShows streaming requests
func (h *PlayersHandler) GetRgShowsPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
tmdbID := vars["tmdb_id"] tmdbID := vars["tmdb_id"]
if tmdbID == "" { if tmdbID == "" {
log.Printf("Error: tmdb_id is empty")
http.Error(w, "tmdb_id path param is required", http.StatusBadRequest) http.Error(w, "tmdb_id path param is required", http.StatusBadRequest)
return return
} }
log.Printf("Processing tmdb_id: %s", tmdbID)
pm := players.NewPlayersManager() pm := players.NewPlayersManager()
result, err := pm.GetMovieStreamByProvider("rgshows", tmdbID) result, err := pm.GetMovieStreamByProvider("rgshows", tmdbID)
if err != nil { if err != nil {
log.Printf("Error getting RgShows stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError) http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return return
} }
if !result.Success { if !result.Success {
log.Printf("RgShows stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound) http.Error(w, "Stream not found", http.StatusNotFound)
return return
} }
// Create iframe with the stream URL // Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL) iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe) htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served RgShows player for tmdb_id: %s", tmdbID)
} }
// GetRgShowsTVPlayer handles RgShows TV show streaming requests // GetRgShowsTVPlayer handles RgShows TV show streaming requests
func (h *PlayersHandler) GetRgShowsTVPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
tmdbID := vars["tmdb_id"] tmdbID := vars["tmdb_id"]
seasonStr := vars["season"] seasonStr := vars["season"]
episodeStr := vars["episode"] episodeStr := vars["episode"]
if tmdbID == "" || seasonStr == "" || episodeStr == "" { if tmdbID == "" || seasonStr == "" || episodeStr == "" {
log.Printf("Error: missing required parameters")
http.Error(w, "tmdb_id, season, and episode path params are required", http.StatusBadRequest) http.Error(w, "tmdb_id, season, and episode path params are required", http.StatusBadRequest)
return return
} }
season, err := strconv.Atoi(seasonStr) season, err := strconv.Atoi(seasonStr)
if err != nil { if err != nil {
log.Printf("Error parsing season: %v", err)
http.Error(w, "Invalid season number", http.StatusBadRequest) http.Error(w, "Invalid season number", http.StatusBadRequest)
return return
} }
episode, err := strconv.Atoi(episodeStr) episode, err := strconv.Atoi(episodeStr)
if err != nil { if err != nil {
log.Printf("Error parsing episode: %v", err)
http.Error(w, "Invalid episode number", http.StatusBadRequest) http.Error(w, "Invalid episode number", http.StatusBadRequest)
return return
} }
log.Printf("Processing tmdb_id: %s, season: %d, episode: %d", tmdbID, season, episode)
pm := players.NewPlayersManager() pm := players.NewPlayersManager()
result, err := pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode) result, err := pm.GetTVStreamByProvider("rgshows", tmdbID, season, episode)
if err != nil { if err != nil {
log.Printf("Error getting RgShows TV stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError) http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return return
} }
if !result.Success { if !result.Success {
log.Printf("RgShows TV stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound) http.Error(w, "Stream not found", http.StatusNotFound)
return return
} }
// Create iframe with the stream URL // Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL) iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows TV Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe) htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>RgShows TV Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) 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 // GetIframeVideoPlayer handles IframeVideo streaming requests
func (h *PlayersHandler) GetIframeVideoPlayer(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
kinopoiskID := vars["kinopoisk_id"] kinopoiskID := vars["kinopoisk_id"]
imdbID := vars["imdb_id"] imdbID := vars["imdb_id"]
if kinopoiskID == "" && imdbID == "" { 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) http.Error(w, "Either kinopoisk_id or imdb_id path param is required", http.StatusBadRequest)
return return
} }
log.Printf("Processing kinopoisk_id: %s, imdb_id: %s", kinopoiskID, imdbID)
pm := players.NewPlayersManager() pm := players.NewPlayersManager()
result, err := pm.GetStreamWithKinopoisk(kinopoiskID, imdbID) result, err := pm.GetStreamWithKinopoisk(kinopoiskID, imdbID)
if err != nil { if err != nil {
log.Printf("Error getting IframeVideo stream: %v", err)
http.Error(w, "Failed to get stream", http.StatusInternalServerError) http.Error(w, "Failed to get stream", http.StatusInternalServerError)
return return
} }
if !result.Success { if !result.Success {
log.Printf("IframeVideo stream not found: %s", result.Error)
http.Error(w, "Stream not found", http.StatusNotFound) http.Error(w, "Stream not found", http.StatusNotFound)
return return
} }
// Create iframe with the stream URL // Create iframe with the stream URL
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL) iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, result.StreamURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>IframeVideo Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe) htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>IframeVideo Player</title><style>html,body{margin:0;height:100%%;}</style></head><body>%s</body></html>`, iframe)
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) 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 // GetStreamAPI returns stream information as JSON API
func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) { 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) vars := mux.Vars(r)
provider := vars["provider"] provider := vars["provider"]
tmdbID := vars["tmdb_id"] tmdbID := vars["tmdb_id"]
if provider == "" || tmdbID == "" { if provider == "" || tmdbID == "" {
log.Printf("Error: missing required parameters")
http.Error(w, "provider and tmdb_id path params are required", http.StatusBadRequest) http.Error(w, "provider and tmdb_id path params are required", http.StatusBadRequest)
return return
} }
@@ -571,8 +495,6 @@ func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) {
kinopoiskID := r.URL.Query().Get("kinopoisk_id") kinopoiskID := r.URL.Query().Get("kinopoisk_id")
imdbID := r.URL.Query().Get("imdb_id") imdbID := r.URL.Query().Get("imdb_id")
log.Printf("Processing provider: %s, tmdb_id: %s", provider, tmdbID)
pm := players.NewPlayersManager() pm := players.NewPlayersManager()
var result *players.StreamResult var result *players.StreamResult
var err error var err error
@@ -602,7 +524,6 @@ func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) {
} }
if err != nil { if err != nil {
log.Printf("Error getting stream from %s: %v", provider, err)
result = &players.StreamResult{ result = &players.StreamResult{
Success: false, Success: false,
Provider: provider, Provider: provider,
@@ -612,14 +533,10 @@ func (h *PlayersHandler) GetStreamAPI(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result) json.NewEncoder(w).Encode(result)
log.Printf("Successfully served stream API for provider: %s, tmdb_id: %s", provider, tmdbID)
} }
// GetVidsrcPlayer handles Vidsrc.to player (uses IMDb ID for both movies and TV shows) // GetVidsrcPlayer handles Vidsrc.to player (uses IMDb ID for both movies and TV shows)
func (h *PlayersHandler) GetVidsrcPlayer(w http.ResponseWriter, r *http.Request) { func (h *PlayersHandler) GetVidsrcPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidsrcPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r) vars := mux.Vars(r)
imdbId := vars["imdb_id"] imdbId := vars["imdb_id"]
mediaType := vars["media_type"] // "movie" or "tv" mediaType := vars["media_type"] // "movie" or "tv"
@@ -645,21 +562,15 @@ func (h *PlayersHandler) GetVidsrcPlayer(w http.ResponseWriter, r *http.Request)
return return
} }
log.Printf("Generated Vidsrc URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами // Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidsrc Player") htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidsrc Player")
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vidsrc player for %s: %s", mediaType, imdbId)
} }
// GetVidlinkMoviePlayer handles vidlink.pro player for movies (uses IMDb ID) // GetVidlinkMoviePlayer handles vidlink.pro player for movies (uses IMDb ID)
func (h *PlayersHandler) GetVidlinkMoviePlayer(w http.ResponseWriter, r *http.Request) { func (h *PlayersHandler) GetVidlinkMoviePlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidlinkMoviePlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r) vars := mux.Vars(r)
imdbId := vars["imdb_id"] imdbId := vars["imdb_id"]
@@ -670,103 +581,15 @@ func (h *PlayersHandler) GetVidlinkMoviePlayer(w http.ResponseWriter, r *http.Re
playerURL := fmt.Sprintf("https://vidlink.pro/movie/%s", imdbId) playerURL := fmt.Sprintf("https://vidlink.pro/movie/%s", imdbId)
log.Printf("Generated Vidlink Movie URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами // Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player") htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player")
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
log.Printf("Successfully served Vidlink movie player: %s", imdbId)
} }
// GetVidlinkTVPlayer handles vidlink.pro player for TV shows (uses TMDB ID) // GetVidlinkTVPlayer handles vidlink.pro player for TV shows (uses TMDB ID)
func (h *PlayersHandler) GetHDVBPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetHDVBPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
log.Printf("Error: id_type or id is empty")
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
if idType != "kp" && idType != "imdb" {
log.Printf("Error: invalid id_type: %s", idType)
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
log.Printf("Processing %s ID: %s", idType, id)
if h.config.HDVBToken == "" {
log.Printf("Error: HDVB_TOKEN is missing")
http.Error(w, "Server misconfiguration: HDVB_TOKEN missing", http.StatusInternalServerError)
return
}
var apiURL string
if idType == "kp" {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?id_kp=%s&token=%s", id, h.config.HDVBToken)
} else {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?imdb_id=%s&token=%s", id, h.config.HDVBToken)
}
log.Printf("HDVB API URL: %s", apiURL)
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL)
if err != nil {
log.Printf("Error fetching HDVB data: %v", err)
http.Error(w, "Failed to fetch player data", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading HDVB response: %v", err)
http.Error(w, "Failed to read player data", http.StatusInternalServerError)
return
}
var hdvbData []map[string]interface{}
if err := json.Unmarshal(body, &hdvbData); err != nil {
log.Printf("Error parsing HDVB JSON: %v, body: %s", err, string(body))
http.Error(w, "Failed to parse player data", http.StatusInternalServerError)
return
}
if len(hdvbData) == 0 {
log.Printf("No HDVB data found for ID: %s", id)
http.Error(w, "No player data found", http.StatusNotFound)
return
}
iframeURL, ok := hdvbData[0]["iframe_url"].(string)
if !ok || iframeURL == "" {
log.Printf("No iframe_url in HDVB response for ID: %s", id)
http.Error(w, "No player URL found", http.StatusNotFound)
return
}
log.Printf("HDVB iframe URL: %s", iframeURL)
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, iframeURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>HDVB 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 HDVB player for %s: %s", idType, id)
}
func (h *PlayersHandler) GetVidlinkTVPlayer(w http.ResponseWriter, r *http.Request) { func (h *PlayersHandler) GetVidlinkTVPlayer(w http.ResponseWriter, r *http.Request) {
log.Printf("GetVidlinkTVPlayer called: %s %s", r.Method, r.URL.Path)
vars := mux.Vars(r) vars := mux.Vars(r)
tmdbId := vars["tmdb_id"] tmdbId := vars["tmdb_id"]
@@ -784,15 +607,76 @@ func (h *PlayersHandler) GetVidlinkTVPlayer(w http.ResponseWriter, r *http.Reque
playerURL := fmt.Sprintf("https://vidlink.pro/tv/%s/%s/%s", tmdbId, season, episode) playerURL := fmt.Sprintf("https://vidlink.pro/tv/%s/%s/%s", tmdbId, season, episode)
log.Printf("Generated Vidlink TV URL: %s", playerURL)
// Используем общий шаблон с кастомными контролами // Используем общий шаблон с кастомными контролами
htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player") htmlDoc := getPlayerWithControlsHTML(playerURL, "Vidlink Player")
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc)) w.Write([]byte(htmlDoc))
}
log.Printf("Successfully served Vidlink TV player: %s S%sE%s", tmdbId, season, episode) // GetHDVBPlayer handles HDVB streaming requests
func (h *PlayersHandler) GetHDVBPlayer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idType := vars["id_type"]
id := vars["id"]
if idType == "" || id == "" {
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
if idType != "kp" && idType != "imdb" {
http.Error(w, "id_type must be 'kp' or 'imdb'", http.StatusBadRequest)
return
}
if h.config.HDVBToken == "" {
http.Error(w, "Server misconfiguration: HDVB_TOKEN missing", http.StatusInternalServerError)
return
}
var apiURL string
if idType == "kp" {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?id_kp=%s&token=%s", id, h.config.HDVBToken)
} else {
apiURL = fmt.Sprintf("https://apivb.com/api/videos.json?imdb_id=%s&token=%s", id, h.config.HDVBToken)
}
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL)
if err != nil {
http.Error(w, "Failed to fetch player data", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read player data", http.StatusInternalServerError)
return
}
var hdvbData []map[string]interface{}
if err := json.Unmarshal(body, &hdvbData); err != nil {
http.Error(w, "Failed to parse player data", http.StatusInternalServerError)
return
}
if len(hdvbData) == 0 {
http.Error(w, "No player data found", http.StatusNotFound)
return
}
iframeURL, ok := hdvbData[0]["iframe_url"].(string)
if !ok || iframeURL == "" {
http.Error(w, "No player URL found", http.StatusNotFound)
return
}
htmlDoc := getPlayerWithControlsHTML(iframeURL, "HDVB Player")
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(htmlDoc))
} }
// getPlayerWithControlsHTML возвращает HTML с плеером и overlay для блокировки кликов // getPlayerWithControlsHTML возвращает HTML с плеером и overlay для блокировки кликов
@@ -823,13 +707,6 @@ html,body{margin:0;height:100%%;overflow:hidden;background:#000;font-family:Aria
</div> </div>
</div> </div>
<script> <script>
const overlay=document.getElementById('overlay');
// Блокируем клики на iframe (защита от рекламы)
overlay.addEventListener('click',(e)=>{e.preventDefault();e.stopPropagation();});
overlay.addEventListener('mousedown',(e)=>{e.preventDefault();e.stopPropagation();});
// Fullscreen
document.getElementById('btn-fullscreen').addEventListener('click',()=>{ document.getElementById('btn-fullscreen').addEventListener('click',()=>{
if(!document.fullscreenElement){ if(!document.fullscreenElement){
document.getElementById('container').requestFullscreen(); document.getElementById('container').requestFullscreen();
@@ -841,3 +718,190 @@ document.getElementById('btn-fullscreen').addEventListener('click',()=>{
</body> </body>
</html>`, title, playerURL) </html>`, title, playerURL)
} }
// GetCollapsPlayer handles Collaps streaming requests
func (h *PlayersHandler) GetCollapsPlayer(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idType := vars["id_type"]
id := vars["id"]
season := r.URL.Query().Get("season")
episode := r.URL.Query().Get("episode")
if idType == "" || id == "" {
http.Error(w, "id_type and id are required", http.StatusBadRequest)
return
}
if idType == "kinopoisk_id" {
idType = "kp"
}
if idType != "kp" && idType != "imdb" && idType != "orid" {
http.Error(w, "id_type must be 'kp', 'imdb', or 'orid'", http.StatusBadRequest)
return
}
if h.config.CollapsAPIHost == "" || h.config.CollapsToken == "" {
http.Error(w, "Server misconfiguration: COLLAPS_API_HOST or COLLAPS_TOKEN missing", http.StatusInternalServerError)
return
}
client := &http.Client{Timeout: 8 * time.Second}
// Используем /list endpoint для получения iframe_url
var listURL string
if idType == "kp" {
listURL = fmt.Sprintf("%s/list?token=%s&kinopoisk_id=%s", h.config.CollapsAPIHost, h.config.CollapsToken, id)
} else if idType == "imdb" {
listURL = fmt.Sprintf("%s/list?token=%s&imdb_id=%s", h.config.CollapsAPIHost, h.config.CollapsToken, id)
} else {
// Для orid используем прямой embed
listURL = fmt.Sprintf("%s/embed/movie/%s?token=%s", h.config.CollapsAPIHost, id, h.config.CollapsToken)
}
resp, err := client.Get(listURL)
if err != nil {
http.Error(w, "Failed to fetch from Collaps API", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, fmt.Sprintf("Collaps API error: %d", resp.StatusCode), http.StatusBadGateway)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read Collaps response", http.StatusInternalServerError)
return
}
content := string(body)
// Если это прямой embed (для orid), проверяем на сезоны
if idType == "orid" {
if strings.Contains(content, "seasons:") {
w.Header().Set("Content-Type", "text/html")
w.Write(body)
return
}
// Парсим HLS для фильмов
hlsMatch := regexp.MustCompile(`hls:\s*"(https?://[^"]+\.m3u[^"]*)`).FindStringSubmatch(content)
if len(hlsMatch) == 0 {
http.Error(w, "Video not found", http.StatusNotFound)
return
}
playerURL := hlsMatch[1]
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, playerURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Collaps 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))
return
}
// Парсим JSON ответ от /list
var listResponse struct {
Results []struct {
ID int `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
IframeURL string `json:"iframe_url"`
Seasons []struct {
Season int `json:"season"`
Episodes []struct {
Episode interface{} `json:"episode"` // может быть string или int
IframeURL string `json:"iframe_url"`
} `json:"episodes"`
} `json:"seasons"`
} `json:"results"`
}
if err := json.Unmarshal(body, &listResponse); err != nil {
http.Error(w, "Invalid JSON from Collaps", http.StatusBadGateway)
return
}
if len(listResponse.Results) == 0 {
http.Error(w, "Video not found on Collaps", http.StatusNotFound)
return
}
result := listResponse.Results[0]
var iframeURL string
// Helper функция для конвертации episode в число
episodeToInt := func(ep interface{}) int {
switch v := ep.(type) {
case float64:
return int(v)
case string:
num := 0
fmt.Sscanf(v, "%d", &num)
return num
default:
return 0
}
}
// Если это сериал и запрошены сезон/эпизод
if result.Type == "series" && season != "" && episode != "" {
seasonNum := 0
episodeNum := 0
fmt.Sscanf(season, "%d", &seasonNum)
fmt.Sscanf(episode, "%d", &episodeNum)
for _, s := range result.Seasons {
if s.Season == seasonNum {
for _, e := range s.Episodes {
if episodeToInt(e.Episode) == episodeNum {
iframeURL = e.IframeURL
break
}
}
break
}
}
if iframeURL == "" {
http.Error(w, "Episode not found", http.StatusNotFound)
return
}
} else if result.Type == "series" && season != "" {
// Только сезон
seasonNum := 0
fmt.Sscanf(season, "%d", &seasonNum)
for _, s := range result.Seasons {
if s.Season == seasonNum {
iframeURL = s.Episodes[0].IframeURL
break
}
}
if iframeURL == "" {
http.Error(w, "Season not found", http.StatusNotFound)
return
}
} else {
// Фильм или первый эпизод сериала
iframeURL = result.IframeURL
}
// Возвращаем HTML с iframe напрямую
iframe := fmt.Sprintf(`<iframe src="%s" allowfullscreen controlsList="nodownload" loading="lazy" style="border:none;width:100%%;height:100%%;"></iframe>`, iframeURL)
htmlDoc := fmt.Sprintf(`<!DOCTYPE html><html><head><meta charset='utf-8'/><title>Collaps 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))
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -365,3 +365,57 @@ func (h *TorrentsHandler) SearchByQuery(w http.ResponseWriter, r *http.Request)
Data: response, Data: response,
}) })
} }
// SearchByTitle - поиск торрентов по названию фильма/сериала (новый метод)
func (h *TorrentsHandler) SearchByTitle(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
yearStr := r.URL.Query().Get("year")
contentType := r.URL.Query().Get("type")
if title == "" && originalTitle == "" {
http.Error(w, "Title or originalTitle is required", http.StatusBadRequest)
return
}
if contentType == "" {
contentType = "movie"
}
// Парсим год
year := 0
if yearStr != "" {
if y, err := strconv.Atoi(yearStr); err == nil {
year = y
}
}
// Определяем является ли это сериалом
isSerial := contentType == "series" || contentType == "tv"
// Поиск торрентов
results, err := h.torrentService.SearchTorrentsByTitle(title, originalTitle, year, isSerial)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Применяем фильтрацию по типу контента
results.Results = h.torrentService.FilterByContentType(results.Results, contentType)
results.Total = len(results.Results)
response := map[string]interface{}{
"title": title,
"originalTitle": originalTitle,
"year": year,
"type": contentType,
"total": results.Total,
"results": results.Results,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}

View File

@@ -266,6 +266,7 @@ type TorrentSearchResponse struct {
// RedAPI специфичные структуры // RedAPI специфичные структуры
type RedAPIResponse struct { type RedAPIResponse struct {
Results []RedAPITorrent `json:"Results"` Results []RedAPITorrent `json:"Results"`
Jacred bool `json:"jacred,omitempty"` // Флаг для определения источника
} }
type RedAPITorrent struct { type RedAPITorrent struct {
@@ -277,15 +278,22 @@ type RedAPITorrent struct {
MagnetUri string `json:"MagnetUri"` MagnetUri string `json:"MagnetUri"`
PublishDate string `json:"PublishDate"` PublishDate string `json:"PublishDate"`
CategoryDesc string `json:"CategoryDesc"` CategoryDesc string `json:"CategoryDesc"`
Category []int `json:"Category,omitempty"` // Для jacred
Details string `json:"Details"` Details string `json:"Details"`
Info *RedAPITorrentInfo `json:"Info,omitempty"` Info *RedAPITorrentInfo `json:"Info,omitempty"`
Languages []string `json:"languages,omitempty"` // Для jacred
FFprobe []interface{} `json:"ffprobe,omitempty"` // Для jacred
} }
type RedAPITorrentInfo struct { type RedAPITorrentInfo struct {
Quality interface{} `json:"quality,omitempty"` // Может быть string или number Quality interface{} `json:"quality,omitempty"` // Может быть string или number
VideoType string `json:"videotype,omitempty"`
Voices []string `json:"voices,omitempty"` Voices []string `json:"voices,omitempty"`
Types []string `json:"types,omitempty"` Types []string `json:"types,omitempty"`
Seasons []int `json:"seasons,omitempty"` Seasons []int `json:"seasons,omitempty"`
Name string `json:"name,omitempty"`
OriginalName string `json:"originalname,omitempty"`
Released int `json:"relased,omitempty"` // Примечание: опечатка в API
} }
// Alloha API структуры для получения информации о фильмах // Alloha API структуры для получения информации о фильмах

View File

@@ -82,11 +82,70 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
}, nil }, nil
} }
// SearchTorrentsByTitle - поиск торрентов по названию фильма/сериала
func (s *TorrentService) SearchTorrentsByTitle(title, originalTitle string, year int, isSerial bool) (*models.TorrentSearchResponse, error) {
searchParams := url.Values{}
// Добавляем основное название
if title != "" {
searchParams.Add("title", title)
}
// Добавляем оригинальное название
if originalTitle != "" {
searchParams.Add("title_original", originalTitle)
}
// Добавляем год
if year > 0 {
searchParams.Add("year", strconv.Itoa(year))
}
// Указываем тип контента
if isSerial {
searchParams.Add("is_serial", "2")
} else {
searchParams.Add("is_serial", "1")
}
if s.apiKey != "" {
searchParams.Add("apikey", s.apiKey)
}
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?%s", s.baseURL, searchParams.Encode())
resp, err := s.client.Get(searchURL)
if err != nil {
return nil, fmt.Errorf("failed to search torrents by title: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var redAPIResponse models.RedAPIResponse
if err := json.Unmarshal(body, &redAPIResponse); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
results := s.parseRedAPIResults(redAPIResponse)
return &models.TorrentSearchResponse{
Query: title,
Results: results,
Total: len(results),
}, nil
}
// parseRedAPIResults преобразует результаты RedAPI в наш формат // parseRedAPIResults преобразует результаты RedAPI в наш формат
func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult { func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult {
var results []models.TorrentResult var results []models.TorrentResult
for _, torrent := range data.Results { for i, torrent := range data.Results {
_ = i // для избежания неиспользуемой переменной
var sizeStr string var sizeStr string
switch v := torrent.Size.(type) { switch v := torrent.Size.(type) {
case string: case string:
@@ -112,25 +171,38 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
Source: "RedAPI", Source: "RedAPI",
} }
// Обработка Info для обоих форматов (RedAPI и jacred)
if torrent.Info != nil { if torrent.Info != nil {
// Обработка качества
if torrent.Info.Quality != nil {
switch v := torrent.Info.Quality.(type) { switch v := torrent.Info.Quality.(type) {
case string: case string:
result.Quality = v result.Quality = v
case float64: case float64:
// Для jacred качество приходит как число (1080, 720 и т.д.)
if v >= 2160 {
result.Quality = "4K"
} else {
result.Quality = fmt.Sprintf("%.0fp", v) result.Quality = fmt.Sprintf("%.0fp", v)
}
case int: case int:
if v >= 2160 {
result.Quality = "4K"
} else {
result.Quality = fmt.Sprintf("%dp", v) result.Quality = fmt.Sprintf("%dp", v)
} }
}
}
result.Voice = torrent.Info.Voices result.Voice = torrent.Info.Voices
result.Types = torrent.Info.Types result.Types = torrent.Info.Types
result.Seasons = torrent.Info.Seasons result.Seasons = torrent.Info.Seasons
} }
// Если качество все еще не определено, извлекаем из названия
if result.Quality == "" { if result.Quality == "" {
result.Quality = s.ExtractQuality(result.Title) result.Quality = s.ExtractQuality(result.Title)
} }
results = append(results, result) results = append(results, result)
} }