mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-12-15 20:16:10 +05:00
Fix '/api/v1/movies/popular' route
This commit is contained in:
23
README.md
23
README.md
@@ -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
16
main.go
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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{}{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user