Fix '/api/v1/movies/popular' route

This commit is contained in:
2025-11-21 16:29:08 +02:00
parent d9e48495f7
commit 65e7ff90bd
11 changed files with 220 additions and 239 deletions

View File

@@ -1,22 +1,23 @@
# Neo Movies API (Unified)
# NeoMovies API
REST API для поиска и получения информации о фильмах, использующий TMDB API.
Полнофункциональный REST API для поиска и получения информации о фильмах и сериалах с интеграцией Kinopoisk и TMDB.
## Особенности
- Интеграция с Kinopoisk API для русского контента
- Автоматическое переключение между TMDB и Kinopoisk
- Поиск фильмов и сериалов
- Информация о фильмах
- Популярные, топ-рейтинговые, предстоящие фильмы
- Поддержка русских плееров (Alloha, Lumex, Vibix, HDVB)
- Swagger документация
- Полная поддержка русского языка
- **Двойная интеграция**: Kinopoisk API для русского контента + TMDB для международного
- **Умное переключение**: автоматический выбор источника по языку запроса
- **Коллекции Kinopoisk**: популярные, топ-рейтинговые фильмы/сериалы из официальных коллекций
- **Унифицированный формат**: единый ответ для контента из разных источников
- **Русские плееры**: Alloha, Lumex, Vibix, HDVB, Vidsrc, Vidlink
- **Поиск торрентов**: интеграция с RedAPI для поиска торрентов
- **Система реакций**: лайки, дизлайки, избранное с сохранением в БД
- **Аутентификация**: JWT + Google OAuth
- **Интерактивная документация**: Swagger/OpenAPI
- **Высокая производительность**: Go + горутины + кэширование
## 🛠 Быстрый старт
### Локальная разработка
1. **Клонирование репозитория**
```bash
git clone https://gitlab.com/foxixus/neomovies-api.git

16
main.go
View File

@@ -47,9 +47,9 @@ func main() {
tvHandler := appHandlers.NewTVHandler(tvService)
favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg)
docsHandler := appHandlers.NewDocsHandler()
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService)
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService).WithKinopoisk(kpService)
searchHandler := appHandlers.NewSearchHandler(tmdbService, kpService)
unifiedHandler := appHandlers.NewUnifiedHandler(tmdbService, kpService)
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService).WithKinopoisk(kpService)
playersHandler := appHandlers.NewPlayersHandler(cfg)
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
reactionsHandler := appHandlers.NewReactionsHandler(reactionsService)
@@ -101,11 +101,11 @@ func main() {
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
// Unified prefixed routes
api.HandleFunc("/movie/{id}", unifiedHandler.GetMovie).Methods("GET")
api.HandleFunc("/tv/{id}", unifiedHandler.GetTV).Methods("GET")
api.HandleFunc("/search", unifiedHandler.Search).Methods("GET")
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
// Unified prefixed routes
api.HandleFunc("/movie/{id}", unifiedHandler.GetMovie).Methods("GET")
api.HandleFunc("/tv/{id}", unifiedHandler.GetTV).Methods("GET")
api.HandleFunc("/search", unifiedHandler.Search).Methods("GET")
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")

View File

@@ -32,13 +32,6 @@ type Config struct {
func New() *Config {
mongoURI := getMongoURI()
kpAPIKey := getEnv(EnvKPAPIKey, "")
if kpAPIKey == "" {
log.Printf("[Config] ⚠️ KPAPI_KEY is not set")
} else {
log.Printf("[Config] KPAPI_KEY is set (first 10 chars): %s...", kpAPIKey[:10])
}
return &Config{
MongoURI: mongoURI,
@@ -60,7 +53,7 @@ func New() *Config {
FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
KPAPIKey: kpAPIKey,
KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
}

View File

@@ -996,18 +996,26 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
"/api/v1/movies/popular": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Популярные фильмы",
"description": "Получение списка популярных фильмов",
"description": "Получение списка популярных фильмов из Kinopoisk коллекции TOP_POPULAR_ALL (для русского языка) или TMDB (для английского)",
"tags": []string{"Movies"},
"parameters": []map[string]interface{}{
{
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"description": "Номер страницы",
},
{
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"description": "Язык: ru-RU (Kinopoisk) или en-US (TMDB)",
},
{
"name": "region",
"in": "query",
"schema": map[string]string{"type": "string"},
"description": "Регион для TMDB (например, US, RU)",
},
},
"responses": map[string]interface{}{
@@ -1171,18 +1179,26 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
"/api/v1/movies/top-rated": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Топ рейтинг фильмов",
"description": "Получение списка фильмов с высоким рейтингом",
"description": "Получение списка фильмов с высоким рейтингом из Kinopoisk коллекции TOP_250_MOVIES (для русского языка) или TMDB (для английского)",
"tags": []string{"Movies"},
"parameters": []map[string]interface{}{
{
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"description": "Номер страницы",
},
{
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"description": "Язык: ru-RU (Kinopoisk TOP_250_MOVIES) или en-US (TMDB)",
},
{
"name": "region",
"in": "query",
"schema": map[string]string{"type": "string"},
"description": "Регион для TMDB (например, US, RU)",
},
},
"responses": map[string]interface{}{
@@ -1345,18 +1361,20 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
"/api/v1/tv/popular": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Популярные сериалы",
"description": "Получение списка популярных сериалов",
"description": "Получение списка популярных сериалов из Kinopoisk коллекции TOP_POPULAR_ALL (для русского языка) или TMDB (для английского)",
"tags": []string{"TV Series"},
"parameters": []map[string]interface{}{
{
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"description": "Номер страницы",
},
{
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"description": "Язык: ru-RU (Kinopoisk) или en-US (TMDB)",
},
},
"responses": map[string]interface{}{
@@ -1369,18 +1387,20 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
"/api/v1/tv/top-rated": map[string]interface{}{
"get": map[string]interface{}{
"summary": "Топ рейтинг сериалов",
"description": "Получение списка сериалов с высоким рейтингом",
"description": "Получение списка сериалов с высоким рейтингом из Kinopoisk коллекции TOP_250_TV_SHOWS (для русского языка) или TMDB (для английского)",
"tags": []string{"TV Series"},
"parameters": []map[string]interface{}{
{
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"name": "page",
"in": "query",
"schema": map[string]string{"type": "integer", "default": "1"},
"description": "Номер страницы",
},
{
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"name": "language",
"in": "query",
"schema": map[string]string{"type": "string", "default": "ru-RU"},
"description": "Язык: ru-RU (Kinopoisk TOP_250_TV_SHOWS) или en-US (TMDB)",
},
},
"responses": map[string]interface{}{

View File

@@ -1,45 +1,30 @@
package handlers
import (
"log"
"net/http"
"strings"
)
// GetLanguage extracts the lang parameter from request and returns it with default "ru"
// Supports both "lang" and "language" query parameters
// Valid values: "ru", "en"
// Default: "ru"
func GetLanguage(r *http.Request) string {
// Check "lang" parameter first (our new standard)
lang := r.URL.Query().Get("lang")
// Fall back to "language" for backward compatibility
if lang == "" {
lang = r.URL.Query().Get("language")
}
// Default to "ru" if not specified
if lang == "" {
return "ru-RU"
}
// Sanitize - remove any quotes or suspicious characters
lang = strings.TrimSpace(lang)
lang = strings.Trim(lang, "'\"")
if lang != r.URL.Query().Get("language") && lang != r.URL.Query().Get("lang") {
log.Printf("[GetLanguage] Sanitized language parameter from %s to %s", r.URL.Query().Get("language"), lang)
}
// Convert short codes to TMDB format
switch lang {
case "en":
return "en-US"
case "ru":
return "ru-RU"
default:
// Return as-is if already in correct format
return lang
}
}

View File

@@ -2,7 +2,6 @@ package handlers
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
@@ -101,17 +100,12 @@ func (h *MovieHandler) Popular(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := GetLanguage(r)
region := r.URL.Query().Get("region")
log.Printf("[Handler] Popular request: page=%d, language=%s, region=%s", page, language, region)
movies, err := h.movieService.GetPopular(page, language, region)
if err != nil {
log.Printf("[Handler] Popular error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("[Handler] Popular response: %d results", len(movies.Results))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
@@ -124,17 +118,12 @@ func (h *MovieHandler) TopRated(w http.ResponseWriter, r *http.Request) {
page := getIntQuery(r, "page", 1)
language := GetLanguage(r)
region := r.URL.Query().Get("region")
log.Printf("[Handler] TopRated request: page=%d, language=%s, region=%s", page, language, region)
movies, err := h.movieService.GetTopRated(page, language, region)
if err != nil {
log.Printf("[Handler] TopRated error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("[Handler] TopRated response: %d results", len(movies.Results))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{

View File

@@ -3,7 +3,6 @@ package services
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
@@ -126,29 +125,19 @@ func NewKinopoiskService(apiKey, baseURL string) *KinopoiskService {
func (s *KinopoiskService) makeRequest(endpoint string, target interface{}) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
log.Printf("[Kinopoisk] makeRequest error creating request: %v", err)
return err
}
// Log API key status
if s.apiKey == "" {
log.Printf("[Kinopoisk] ⚠️ API Key is EMPTY!")
} else {
log.Printf("[Kinopoisk] Using API Key (first 10 chars): %s...", s.apiKey[:10])
}
req.Header.Set("X-API-KEY", s.apiKey)
req.Header.Set("Accept", "application/json")
resp, err := s.client.Do(req)
if err != nil {
log.Printf("[Kinopoisk] makeRequest HTTP error: %v", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("[Kinopoisk] makeRequest status code: %d for endpoint: %s", resp.StatusCode, endpoint)
return fmt.Errorf("Kinopoisk API error: %d", resp.StatusCode)
}
@@ -188,14 +177,9 @@ func (s *KinopoiskService) SearchFilms(keyword string, page int) (*KPSearchRespo
return &response, err
}
// GetPopularFilms tries to get popular films using filters API
// This is an alternative to GetTopFilms which may not work anymore
func (s *KinopoiskService) GetPopularFilms(page int) (*KPSearchResponse, error) {
// Try using the filters API with popularity sort
endpoint := fmt.Sprintf("%s/v2.2/films?sortField=num_vote&sortType=DESC&page=%d", s.baseURL, page)
log.Printf("[Kinopoisk] GetPopularFilms (via filters): %s", endpoint)
func (s *KinopoiskService) GetCollection(collectionType string, page int) (*KPSearchResponse, error) {
endpoint := fmt.Sprintf("%s/v2.2/films/collections?type=%s&page=%d", s.baseURL, collectionType, page)
// Try new format first (items/total)
var responseNew struct {
Total int `json:"total"`
TotalPages int `json:"totalPages"`
@@ -204,13 +188,10 @@ func (s *KinopoiskService) GetPopularFilms(page int) (*KPSearchResponse, error)
err := s.makeRequest(endpoint, &responseNew)
if err != nil {
log.Printf("[Kinopoisk] GetPopularFilms error: %v", err)
return nil, err
}
// If we got items, use them
if len(responseNew.Items) > 0 {
log.Printf("[Kinopoisk] GetPopularFilms got %d films (new format)", len(responseNew.Items))
return &KPSearchResponse{
PagesCount: responseNew.TotalPages,
Films: responseNew.Items,
@@ -218,7 +199,6 @@ func (s *KinopoiskService) GetPopularFilms(page int) (*KPSearchResponse, error)
}, nil
}
// Fallback to old format (films/pagesCount)
var responseOld struct {
PagesCount int `json:"pagesCount"`
Films []KPFilmShort `json:"films"`
@@ -226,12 +206,9 @@ func (s *KinopoiskService) GetPopularFilms(page int) (*KPSearchResponse, error)
err = s.makeRequest(endpoint, &responseOld)
if err != nil {
log.Printf("[Kinopoisk] GetPopularFilms error (old format): %v", err)
return nil, err
}
log.Printf("[Kinopoisk] GetPopularFilms got %d films (old format)", len(responseOld.Films))
return &KPSearchResponse{
PagesCount: responseOld.PagesCount,
Films: responseOld.Films,
@@ -254,35 +231,6 @@ func (s *KinopoiskService) GetExternalSources(kinopoiskId int) ([]KPExternalSour
return response.Items, nil
}
func (s *KinopoiskService) GetTopFilms(topType string, page int) (*KPSearchResponse, error) {
endpoint := fmt.Sprintf("%s/v2.2/films/top?type=%s&page=%d", s.baseURL, topType, page)
log.Printf("[Kinopoisk] GetTopFilms: %s", endpoint)
var response struct {
PagesCount int `json:"pagesCount"`
Films []KPFilmShort `json:"films"`
}
err := s.makeRequest(endpoint, &response)
if err != nil {
log.Printf("[Kinopoisk] GetTopFilms error: %v", err)
return nil, err
}
log.Printf("[Kinopoisk] GetTopFilms got %d films (pagesCount=%d)", len(response.Films), response.PagesCount)
// If no films returned, log warning
if len(response.Films) == 0 {
log.Printf("[Kinopoisk] ⚠️ GetTopFilms returned empty results for type=%s", topType)
}
return &KPSearchResponse{
PagesCount: response.PagesCount,
Films: response.Films,
SearchFilmsCountResult: len(response.Films),
}, nil
}
func KPIdToImdbId(kpService *KinopoiskService, kpId int) (string, error) {
film, err := kpService.GetFilmByKinopoiskId(kpId)
if err != nil {

View File

@@ -281,6 +281,37 @@ func MapKPSearchToTMDBResponse(kpSearch *KPSearchResponse) *models.TMDBResponse
}
}
func MapKPSearchToTMDBTVResponse(kpSearch *KPSearchResponse) *models.TMDBTVResponse {
if kpSearch == nil {
return &models.TMDBTVResponse{
Page: 1,
Results: []models.TVShow{},
TotalPages: 0,
TotalResults: 0,
}
}
results := make([]models.TVShow, 0)
for _, film := range kpSearch.Films {
tvShow := mapKPFilmShortToTVShow(film)
if tvShow != nil {
results = append(results, *tvShow)
}
}
totalPages := kpSearch.PagesCount
if totalPages == 0 && len(results) > 0 {
totalPages = 1
}
return &models.TMDBTVResponse{
Page: 1,
Results: results,
TotalPages: totalPages,
TotalResults: kpSearch.SearchFilmsCountResult,
}
}
func mapKPFilmShortToMovie(film KPFilmShort) *models.Movie {
genres := make([]models.Genre, 0)
for _, g := range film.Genres {
@@ -352,6 +383,68 @@ func mapKPFilmShortToMovie(film KPFilmShort) *models.Movie {
}
}
func mapKPFilmShortToTVShow(film KPFilmShort) *models.TVShow {
genres := make([]models.Genre, 0)
for _, g := range film.Genres {
genres = append(genres, models.Genre{
ID: 0,
Name: g.Genre,
})
}
year := film.Year
releaseDate := ""
if year > 0 {
releaseDate = fmt.Sprintf("%d-01-01", year)
}
posterPath := film.PosterUrlPreview
if posterPath == "" {
posterPath = film.PosterUrl
}
title := film.NameRu
if title == "" {
title = film.NameEn
}
if title == "" {
title = film.NameOriginal
}
originalTitle := film.NameOriginal
if originalTitle == "" {
originalTitle = film.NameEn
}
if originalTitle == "" {
originalTitle = film.NameRu
}
rating := film.RatingKinopoisk
if rating == 0 && film.Rating != "" {
rating, _ = strconv.ParseFloat(film.Rating, 64)
}
id := film.KinopoiskId
if id == 0 {
id = film.FilmId
}
return &models.TVShow{
ID: id,
Name: title,
OriginalName: originalTitle,
Overview: film.Description,
PosterPath: posterPath,
FirstAirDate: releaseDate,
VoteAverage: rating,
VoteCount: film.RatingVoteCount,
Popularity: rating * 100,
Genres: genres,
KinopoiskID: id,
IMDbID: film.ImdbId,
}
}
func detectLanguage(film *KPFilm) string {
if film.NameRu != "" {
return "ru"

View File

@@ -2,7 +2,7 @@ package services
import (
"fmt"
"log"
"go.mongodb.org/mongo-driver/mongo"
"neomovies-api/pkg/models"
@@ -31,105 +31,60 @@ func (s *MovieService) Search(query string, page int, language, region string, y
}
func (s *MovieService) GetByID(id int, language string, idType string) (*models.Movie, error) {
// Строго уважаем явный id_type, без скрытого fallback на TMDB
switch idType {
case "kp":
if s.kpService == nil {
return nil, fmt.Errorf("kinopoisk service not configured")
}
// Строго уважаем явный id_type, без скрытого fallback на TMDB
switch idType {
case "kp":
if s.kpService == nil {
return nil, fmt.Errorf("kinopoisk service not configured")
}
// Сначала пробуем как Kinopoisk ID
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(id); err == nil {
// Возвращаем KP-модель в TMDB-формате без подмены на TMDB объект
return MapKPFilmToTMDBMovie(kpFilm), nil
}
// Сначала пробуем как Kinopoisk ID
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(id); err == nil {
// Возвращаем KP-модель в TMDB-формате без подмены на TMDB объект
return MapKPFilmToTMDBMovie(kpFilm), nil
}
// Возможно пришел TMDB ID — пробуем конвертировать TMDB -> KP
if kpId, convErr := TmdbIdToKPId(s.tmdb, s.kpService, id); convErr == nil {
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(kpId); err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
}
// Явно указан KP, но ничего не нашли — возвращаем ошибку
return nil, fmt.Errorf("film not found in Kinopoisk with id %d", id)
// Возможно пришел TMDB ID — пробуем конвертировать TMDB -> KP
if kpId, convErr := TmdbIdToKPId(s.tmdb, s.kpService, id); convErr == nil {
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(kpId); err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
}
// Явно указан KP, но ничего не нашли — возвращаем ошибку
return nil, fmt.Errorf("film not found in Kinopoisk with id %d", id)
case "tmdb":
return s.tmdb.GetMovie(id, language)
}
case "tmdb":
return s.tmdb.GetMovie(id, language)
}
// Если id_type не указан — старая логика по языку
if ShouldUseKinopoisk(language) && s.kpService != nil {
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(id); err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
}
return s.tmdb.GetMovie(id, language)
// Если id_type не указан — старая логика по языку
if ShouldUseKinopoisk(language) && s.kpService != nil {
if kpFilm, err := s.kpService.GetFilmByKinopoiskId(id); err == nil {
return MapKPFilmToTMDBMovie(kpFilm), nil
}
}
return s.tmdb.GetMovie(id, language)
}
func (s *MovieService) GetPopular(page int, language, region string) (*models.TMDBResponse, error) {
log.Printf("[GetPopular] language=%s, region=%s, page=%d", language, region, page)
if ShouldUseKinopoisk(language) && s.kpService != nil {
log.Printf("[GetPopular] Using Kinopoisk for language: %s", language)
// Try GetTopFilms first
kpTop, err := s.kpService.GetTopFilms("TOP_100_POPULAR_FILMS", page)
if err != nil {
log.Printf("[GetPopular] GetTopFilms error: %v, trying GetPopularFilms", err)
} else if kpTop != nil && len(kpTop.Films) > 0 {
log.Printf("[GetPopular] Got %d films from GetTopFilms", len(kpTop.Films))
return MapKPSearchToTMDBResponse(kpTop), nil
} else {
log.Printf("[GetPopular] GetTopFilms returned empty results, trying GetPopularFilms")
}
// Try GetPopularFilms as fallback
kpPopular, err := s.kpService.GetPopularFilms(page)
if err != nil {
log.Printf("[GetPopular] GetPopularFilms error: %v, falling back to TMDB", err)
} else if kpPopular != nil && len(kpPopular.Films) > 0 {
log.Printf("[GetPopular] Got %d films from GetPopularFilms", len(kpPopular.Films))
return MapKPSearchToTMDBResponse(kpPopular), nil
} else {
log.Printf("[GetPopular] GetPopularFilms returned empty results, falling back to TMDB")
kpResult, err := s.kpService.GetCollection("TOP_POPULAR_ALL", page)
if err == nil && kpResult != nil && len(kpResult.Films) > 0 {
return MapKPSearchToTMDBResponse(kpResult), nil
}
}
log.Printf("[GetPopular] Using TMDB for language: %s", language)
return s.tmdb.GetPopularMovies(page, language, region)
}
func (s *MovieService) GetTopRated(page int, language, region string) (*models.TMDBResponse, error) {
log.Printf("[GetTopRated] language=%s, region=%s, page=%d", language, region, page)
if ShouldUseKinopoisk(language) && s.kpService != nil {
log.Printf("[GetTopRated] Using Kinopoisk for language: %s", language)
// Try GetTopFilms first
kpTop, err := s.kpService.GetTopFilms("TOP_250_BEST_FILMS", page)
if err != nil {
log.Printf("[GetTopRated] GetTopFilms error: %v, trying GetPopularFilms", err)
} else if kpTop != nil && len(kpTop.Films) > 0 {
log.Printf("[GetTopRated] Got %d films from GetTopFilms", len(kpTop.Films))
return MapKPSearchToTMDBResponse(kpTop), nil
} else {
log.Printf("[GetTopRated] GetTopFilms returned empty results, trying GetPopularFilms")
}
// Try GetPopularFilms as fallback (sorted by rating)
kpPopular, err := s.kpService.GetPopularFilms(page)
if err != nil {
log.Printf("[GetTopRated] GetPopularFilms error: %v, falling back to TMDB", err)
} else if kpPopular != nil && len(kpPopular.Films) > 0 {
log.Printf("[GetTopRated] Got %d films from GetPopularFilms", len(kpPopular.Films))
return MapKPSearchToTMDBResponse(kpPopular), nil
} else {
log.Printf("[GetTopRated] GetPopularFilms returned empty results, falling back to TMDB")
kpResult, err := s.kpService.GetCollection("TOP_250_MOVIES", page)
if err == nil && kpResult != nil && len(kpResult.Films) > 0 {
return MapKPSearchToTMDBResponse(kpResult), nil
}
}
log.Printf("[GetTopRated] Using TMDB for language: %s", language)
return s.tmdb.GetTopRatedMovies(page, language, region)
}
@@ -155,29 +110,29 @@ func (s *MovieService) GetExternalIDs(id int) (*models.ExternalIDs, error) {
if err == nil && kpFilm != nil {
externalIDs := MapKPExternalIDsToTMDB(kpFilm)
externalIDs.ID = id
// Пытаемся получить TMDB ID через IMDB ID
if kpFilm.ImdbId != "" && s.tmdb != nil {
if tmdbID, tmdbErr := s.tmdb.FindTMDBIdByIMDB(kpFilm.ImdbId, "movie", "ru-RU"); tmdbErr == nil {
externalIDs.TMDBID = tmdbID
}
}
return externalIDs, nil
}
}
tmdbIDs, err := s.tmdb.GetMovieExternalIDs(id)
if err != nil {
return nil, err
}
if s.kpService != nil && tmdbIDs.IMDbID != "" {
kpFilm, err := s.kpService.GetFilmByImdbId(tmdbIDs.IMDbID)
if err == nil && kpFilm != nil {
tmdbIDs.KinopoiskID = kpFilm.KinopoiskId
}
}
return tmdbIDs, nil
}

View File

@@ -3,7 +3,6 @@ package services
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"strconv"
@@ -34,23 +33,19 @@ func NewTMDBService(accessToken string) *TMDBService {
func (s *TMDBService) makeRequest(endpoint string, target interface{}) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
log.Printf("[TMDB] makeRequest error creating request: %v", err)
return err
}
// Используем Bearer токен вместо API key в query параметрах
req.Header.Set("Authorization", "Bearer "+s.accessToken)
req.Header.Set("Accept", "application/json")
resp, err := s.client.Do(req)
if err != nil {
log.Printf("[TMDB] makeRequest HTTP error: %v", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("[TMDB] makeRequest status code: %d", resp.StatusCode)
return fmt.Errorf("TMDB API error: %d", resp.StatusCode)
}
@@ -303,15 +298,9 @@ func (s *TMDBService) GetPopularMovies(page int, language, region string) (*mode
}
endpoint := fmt.Sprintf("%s/movie/popular?%s", s.baseURL, params.Encode())
log.Printf("[TMDB] GetPopularMovies: %s", endpoint)
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
if err != nil {
log.Printf("[TMDB] GetPopularMovies error: %v", err)
} else {
log.Printf("[TMDB] GetPopularMovies got %d results", len(response.Results))
}
return &response, err
}
@@ -330,15 +319,9 @@ func (s *TMDBService) GetTopRatedMovies(page int, language, region string) (*mod
}
endpoint := fmt.Sprintf("%s/movie/top_rated?%s", s.baseURL, params.Encode())
log.Printf("[TMDB] GetTopRatedMovies: %s", endpoint)
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
if err != nil {
log.Printf("[TMDB] GetTopRatedMovies error: %v", err)
} else {
log.Printf("[TMDB] GetTopRatedMovies got %d results", len(response.Results))
}
return &response, err
}

View File

@@ -77,10 +77,24 @@ func (s *TVService) GetByID(id int, language string, idType string) (*models.TVS
}
func (s *TVService) GetPopular(page int, language string) (*models.TMDBTVResponse, error) {
if ShouldUseKinopoisk(language) && s.kpService != nil {
kpResult, err := s.kpService.GetCollection("TOP_POPULAR_ALL", page)
if err == nil && kpResult != nil && len(kpResult.Films) > 0 {
return MapKPSearchToTMDBTVResponse(kpResult), nil
}
}
return s.tmdb.GetPopularTVShows(page, language)
}
func (s *TVService) GetTopRated(page int, language string) (*models.TMDBTVResponse, error) {
if ShouldUseKinopoisk(language) && s.kpService != nil {
kpResult, err := s.kpService.GetCollection("TOP_250_TV_SHOWS", page)
if err == nil && kpResult != nil && len(kpResult.Films) > 0 {
return MapKPSearchToTMDBTVResponse(kpResult), nil
}
}
return s.tmdb.GetTopRatedTVShows(page, language)
}