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

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

View File

@@ -1,17 +1,18 @@
package handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/mux"
"neomovies-api/pkg/config"
"neomovies-api/pkg/middleware"
"neomovies-api/pkg/models"
"neomovies-api/pkg/services"
"neomovies-api/pkg/config"
"neomovies-api/pkg/middleware"
"neomovies-api/pkg/models"
"neomovies-api/pkg/services"
)
type FavoritesHandler struct {
@@ -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)
}
resp, err := http.Get(url)
client := &http.Client{Timeout: 6 * time.Second}
resp, err := client.Get(url)
if err != nil {
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)
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 {
log.Printf("Error calling Alloha API: %v", err)
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)
}
// 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) {
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)
resp, err := http.Get(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)

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"net/url"
"strconv"
"time"
"neomovies-api/pkg/models"
)
@@ -17,11 +18,16 @@ type TMDBService struct {
}
func NewTMDBService(accessToken string) *TMDBService {
return &TMDBService{
accessToken: accessToken,
baseURL: "https://api.themoviedb.org/3",
client: &http.Client{},
}
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 60 * time.Second,
}
return &TMDBService{
accessToken: accessToken,
baseURL: "https://api.themoviedb.org/3",
client: &http.Client{Timeout: 10 * time.Second, Transport: transport},
}
}
func (s *TMDBService) makeRequest(endpoint string, target interface{}) error {
@@ -194,8 +200,10 @@ func (s *TMDBService) FindTMDBIdByIMDB(imdbID string, media string, language str
endpoint := fmt.Sprintf("%s/find/%s?%s", s.baseURL, url.PathEscape(imdbID), params.Encode())
var resp struct {
MovieResults []struct{ ID int `json:"id"` } `json:"movie_results"`
TVResults []struct{ ID int `json:"id"` } `json:"tv_results"`
MovieResults []struct{ ID int `json:"id"` } `json:"movie_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 {
return 0, err
@@ -217,6 +225,12 @@ func (s *TMDBService) FindTMDBIdByIMDB(imdbID string, media string, language str
if len(resp.TVResults) > 0 {
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)
}