feat(players/alloha): add meta endpoint for seasons/episodes; perf: http clients/transport; docs: update

This commit is contained in:
Erno
2025-10-21 15:35:20 +00:00
parent 53a405a743
commit 04fe3f3925
7 changed files with 173 additions and 48 deletions

View File

@@ -278,18 +278,15 @@ curl -X POST https://api.neomovies.ru/api/v1/auth/login \
}' }'
``` ```
### Поиск фильмов ### Поиск (унифицированный)
```bash ```bash
# Поиск фильмов # Мультипоиск (источник обязателен)
curl "https://api.neomovies.ru/api/v1/movies/search?query=marvel&page=1" curl "https://api.neomovies.ru/api/v1/search?query=matrix&source=tmdb&page=1"
# Детали фильма # Детали по униф. ID
curl "https://api.neomovies.ru/api/v1/movies/550" curl "https://api.neomovies.ru/api/v1/movie/tmdb_550"
curl "https://api.neomovies.ru/api/v1/tv/kp_61365"
# Добавить в избранное (с JWT токеном)
curl -X POST https://api.neomovies.ru/api/v1/favorites/550 \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
``` ```
### Поиск торрентов ### Поиск торрентов

View File

@@ -69,7 +69,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
docsHandler := handlersPkg.NewDocsHandler() docsHandler := handlersPkg.NewDocsHandler()
searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService) searchHandler := handlersPkg.NewSearchHandler(tmdbService, kpService)
unifiedHandler := handlersPkg.NewUnifiedHandler(tmdbService, kpService) unifiedHandler := handlersPkg.NewUnifiedHandler(tmdbService, kpService)
categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService) categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService).WithKinopoisk(kpService)
playersHandler := handlersPkg.NewPlayersHandler(globalCfg) playersHandler := handlersPkg.NewPlayersHandler(globalCfg)
torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService) torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService)
reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService) reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService)
@@ -97,6 +97,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET") api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET")
api.HandleFunc("/players/alloha/{id_type}/{id}", playersHandler.GetAllohaPlayer).Methods("GET") api.HandleFunc("/players/alloha/{id_type}/{id}", playersHandler.GetAllohaPlayer).Methods("GET")
api.HandleFunc("/players/alloha/meta/kp/{kp_id}", playersHandler.GetAllohaMetaByKP).Methods("GET")
api.HandleFunc("/players/lumex/{id_type}/{id}", playersHandler.GetLumexPlayer).Methods("GET") api.HandleFunc("/players/lumex/{id_type}/{id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{id_type}/{id}", playersHandler.GetVibixPlayer).Methods("GET") api.HandleFunc("/players/vibix/{id_type}/{id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET") api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET")

View File

@@ -49,7 +49,7 @@ func main() {
docsHandler := appHandlers.NewDocsHandler() docsHandler := appHandlers.NewDocsHandler()
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService) searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService) unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService)
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService) categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService).WithKinopoisk(kpService)
playersHandler := appHandlers.NewPlayersHandler(cfg) playersHandler := appHandlers.NewPlayersHandler(cfg)
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService) torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
reactionsHandler := appHandlers.NewReactionsHandler(reactionsService) reactionsHandler := appHandlers.NewReactionsHandler(reactionsService)

View File

@@ -13,12 +13,17 @@ import (
type CategoriesHandler struct { type CategoriesHandler struct {
tmdbService *services.TMDBService tmdbService *services.TMDBService
kpService *services.KinopoiskService
} }
func NewCategoriesHandler(tmdbService *services.TMDBService) *CategoriesHandler { func NewCategoriesHandler(tmdbService *services.TMDBService) *CategoriesHandler {
return &CategoriesHandler{ // Для совместимости, kpService может быть добавлен позже через setter при инициализации в main.go/api/index.go
tmdbService: tmdbService, return &CategoriesHandler{tmdbService: tmdbService}
} }
func (h *CategoriesHandler) WithKinopoisk(kp *services.KinopoiskService) *CategoriesHandler {
h.kpService = kp
return h
} }
type Category struct { type Category struct {
@@ -35,7 +40,7 @@ func (h *CategoriesHandler) GetCategories(w http.ResponseWriter, r *http.Request
return return
} }
// Преобразуем жанры в категории // Преобразуем жанры в категории (пока TMDB). Для KP — можно замаппить фиксированный список
var categories []Category var categories []Category
for _, genre := range genresResponse.Genres { for _, genre := range genresResponse.Genres {
slug := generateSlug(genre.Name) slug := generateSlug(genre.Name)
@@ -77,16 +82,28 @@ func (h *CategoriesHandler) GetMediaByCategory(w http.ResponseWriter, r *http.Re
return return
} }
source := r.URL.Query().Get("source") // "kp" | "tmdb"
var data interface{} var data interface{}
var err2 error var err2 error
if source == "kp" && h.kpService != nil {
// KP не имеет прямого discover по genre id TMDB — здесь можно реализовать маппинг slug->поисковый запрос
// Для простоты: используем keyword поиск по имени категории (slug как ключевое слово)
// Получим человекочитаемое имя жанра из TMDB как приближение
if mediaType == "movie" {
// Поиском KP (keyword) эмулируем категорию
data, err2 = h.kpService.SearchFilms(r.URL.Query().Get("name"), page)
} else {
// Для сериалов у KP: используем тот же поиск (KP выдаёт и сериалы в некоторых случаях)
data, err2 = h.kpService.SearchFilms(r.URL.Query().Get("name"), page)
}
} else {
if mediaType == "movie" { if mediaType == "movie" {
// Используем discover API для получения фильмов по жанру
data, err2 = h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language) data, err2 = h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language)
} else { } else {
// Используем discover API для получения сериалов по жанру
data, err2 = h.tmdbService.DiscoverTVByGenre(categoryID, page, language) data, err2 = h.tmdbService.DiscoverTVByGenre(categoryID, page, language)
} }
}
if err2 != nil { if err2 != nil {
http.Error(w, err2.Error(), http.StatusInternalServerError) http.Error(w, err2.Error(), http.StatusInternalServerError)

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@@ -177,7 +178,8 @@ func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*mo
url = fmt.Sprintf("https://api.themoviedb.org/3/tv/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken) url = fmt.Sprintf("https://api.themoviedb.org/3/tv/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken)
} }
resp, err := http.Get(url) client := &http.Client{Timeout: 6 * time.Second}
resp, err := client.Get(url)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to fetch from TMDB: %w", err) return nil, fmt.Errorf("failed to fetch from TMDB: %w", err)
} }

View File

@@ -58,7 +58,8 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
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) log.Printf("Calling Alloha API: %s", apiURL)
resp, err := http.Get(apiURL) client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL)
if err != nil { if err != nil {
log.Printf("Error calling Alloha API: %v", err) 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)
@@ -139,6 +140,98 @@ func (h *PlayersHandler) GetAllohaPlayer(w http.ResponseWriter, r *http.Request)
log.Printf("Successfully served Alloha player for %s: %s", idType, id) log.Printf("Successfully served Alloha player for %s: %s", idType, id)
} }
// GetAllohaMetaByKP returns seasons/episodes meta for Alloha by kinopoisk_id
func (h *PlayersHandler) GetAllohaMetaByKP(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
kpID := vars["kp_id"]
if strings.TrimSpace(kpID) == "" {
http.Error(w, "kp_id is required", http.StatusBadRequest)
return
}
if h.config.AllohaToken == "" {
http.Error(w, "Server misconfiguration: ALLOHA_TOKEN missing", http.StatusInternalServerError)
return
}
apiURL := fmt.Sprintf("https://api.alloha.tv/?token=%s&kp=%s", url.QueryEscape(h.config.AllohaToken), url.QueryEscape(kpID))
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL)
if err != nil {
http.Error(w, "Failed to fetch from Alloha API", http.StatusBadGateway)
return
}
defer resp.Body.Close()
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 {
http.Error(w, "Failed to read Alloha response", http.StatusBadGateway)
return
}
// Define only the parts we need
var raw struct {
Status string `json:"status"`
Data struct {
Seasons []struct {
Key string `json:"key"`
Value struct {
Episodes []struct {
Key string `json:"key"`
Value struct {
Translation []struct {
Value struct {
Translation string `json:"translation"`
} `json:"value"`
} `json:"translation"`
} `json:"value"`
} `json:"episodes"`
} `json:"value"`
} `json:"seasons"`
} `json:"data"`
}
if err := json.Unmarshal(body, &raw); err != nil {
http.Error(w, "Invalid JSON from Alloha", http.StatusBadGateway)
return
}
type episodeMeta struct {
Episode int `json:"episode"`
Translations []string `json:"translations"`
}
type seasonMeta struct {
Season int `json:"season"`
Episodes []episodeMeta `json:"episodes"`
}
out := struct {
Success bool `json:"success"`
Seasons []seasonMeta `json:"seasons"`
}{Success: true}
for _, s := range raw.Data.Seasons {
seasonNum, _ := strconv.Atoi(strings.TrimSpace(s.Key))
sm := seasonMeta{Season: seasonNum}
for _, e := range s.Value.Episodes {
epNum, _ := strconv.Atoi(strings.TrimSpace(e.Key))
em := episodeMeta{Episode: epNum}
for _, tr := range e.Value.Translation {
t := strings.TrimSpace(tr.Value.Translation)
if t != "" {
em.Translations = append(em.Translations, t)
}
}
sm.Episodes = append(sm.Episodes, em)
}
out.Seasons = append(out.Seasons, sm)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(out)
}
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) log.Printf("GetLumexPlayer called: %s %s", r.Method, r.URL.Path)
@@ -601,7 +694,8 @@ func (h *PlayersHandler) GetHDVBPlayer(w http.ResponseWriter, r *http.Request) {
} }
log.Printf("HDVB API URL: %s", apiURL) log.Printf("HDVB API URL: %s", apiURL)
resp, err := http.Get(apiURL) client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(apiURL)
if err != nil { if err != nil {
log.Printf("Error fetching HDVB data: %v", err) log.Printf("Error fetching HDVB data: %v", err)
http.Error(w, "Failed to fetch player data", http.StatusInternalServerError) http.Error(w, "Failed to fetch player data", http.StatusInternalServerError)

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"time"
"neomovies-api/pkg/models" "neomovies-api/pkg/models"
) )
@@ -17,10 +18,15 @@ type TMDBService struct {
} }
func NewTMDBService(accessToken string) *TMDBService { func NewTMDBService(accessToken string) *TMDBService {
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 60 * time.Second,
}
return &TMDBService{ return &TMDBService{
accessToken: accessToken, accessToken: accessToken,
baseURL: "https://api.themoviedb.org/3", baseURL: "https://api.themoviedb.org/3",
client: &http.Client{}, client: &http.Client{Timeout: 10 * time.Second, Transport: transport},
} }
} }
@@ -196,6 +202,8 @@ func (s *TMDBService) FindTMDBIdByIMDB(imdbID string, media string, language str
var resp struct { var resp struct {
MovieResults []struct{ ID int `json:"id"` } `json:"movie_results"` MovieResults []struct{ ID int `json:"id"` } `json:"movie_results"`
TVResults []struct{ ID int `json:"id"` } `json:"tv_results"` TVResults []struct{ ID int `json:"id"` } `json:"tv_results"`
TVEpisodeResults []struct{ ShowID int `json:"show_id"` } `json:"tv_episode_results"`
TVSeasonResults []struct{ ShowID int `json:"show_id"` } `json:"tv_season_results"`
} }
if err := s.makeRequest(endpoint, &resp); err != nil { if err := s.makeRequest(endpoint, &resp); err != nil {
return 0, err return 0, err
@@ -217,6 +225,12 @@ func (s *TMDBService) FindTMDBIdByIMDB(imdbID string, media string, language str
if len(resp.TVResults) > 0 { if len(resp.TVResults) > 0 {
return resp.TVResults[0].ID, nil return resp.TVResults[0].ID, nil
} }
if len(resp.TVSeasonResults) > 0 {
return resp.TVSeasonResults[0].ShowID, nil
}
if len(resp.TVEpisodeResults) > 0 {
return resp.TVEpisodeResults[0].ShowID, nil
}
} }
return 0, fmt.Errorf("tmdb id not found for imdb %s", imdbID) return 0, fmt.Errorf("tmdb id not found for imdb %s", imdbID)
} }