Files
neomovies-api/pkg/services/tmdb.go
Erno c7aa844f49 feat(api): support prefixed IDs in routes and add unified mappers scaffolding
- Handlers parse IDs like kp_123 / tmdb_456 and set id_type accordingly
- Add KP->Unified and TMDB->Unified movie mappers (basic fields)
- Keep backward compatibility for numeric IDs
2025-10-19 08:30:16 +00:00

591 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package services
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"neomovies-api/pkg/models"
)
type TMDBService struct {
accessToken string
baseURL string
client *http.Client
}
func NewTMDBService(accessToken string) *TMDBService {
return &TMDBService{
accessToken: accessToken,
baseURL: "https://api.themoviedb.org/3",
client: &http.Client{},
}
}
func (s *TMDBService) makeRequest(endpoint string, target interface{}) error {
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
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 {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("TMDB API error: %d", resp.StatusCode)
}
return json.NewDecoder(resp.Body).Decode(target)
}
func (s *TMDBService) SearchMovies(query string, page int, language, region string, year int) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("query", query)
params.Set("page", strconv.Itoa(page))
params.Set("include_adult", "false")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
if year > 0 {
params.Set("year", strconv.Itoa(year))
}
endpoint := fmt.Sprintf("%s/search/movie?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) SearchMulti(query string, page int, language string) (*models.MultiSearchResponse, error) {
params := url.Values{}
params.Set("query", query)
params.Set("page", strconv.Itoa(page))
params.Set("include_adult", "false")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/search/multi?%s", s.baseURL, params.Encode())
var response models.MultiSearchResponse
err := s.makeRequest(endpoint, &response)
if err != nil {
return nil, err
}
// Фильтруем результаты: убираем "person", и без названия
filteredResults := make([]models.MultiSearchResult, 0)
for _, result := range response.Results {
if result.MediaType == "person" {
continue
}
hasTitle := false
if result.MediaType == "movie" && result.Title != "" {
hasTitle = true
} else if result.MediaType == "tv" && result.Name != "" {
hasTitle = true
}
if hasTitle {
filteredResults = append(filteredResults, result)
}
}
response.Results = filteredResults
response.TotalResults = len(filteredResults)
return &response, nil
}
// Алиас для совместимости с новым WebTorrent handler
func (s *TMDBService) SearchTV(query string, page int, language string, firstAirDateYear int) (*models.TMDBTVResponse, error) {
return s.SearchTVShows(query, page, language, firstAirDateYear)
}
func (s *TMDBService) SearchTVShows(query string, page int, language string, firstAirDateYear int) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("query", query)
params.Set("page", strconv.Itoa(page))
params.Set("include_adult", "false")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if firstAirDateYear > 0 {
params.Set("first_air_date_year", strconv.Itoa(firstAirDateYear))
}
endpoint := fmt.Sprintf("%s/search/tv?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetMovie(id int, language string) (*models.Movie, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/movie/%d?%s", s.baseURL, id, params.Encode())
var movie models.Movie
err := s.makeRequest(endpoint, &movie)
return &movie, err
}
func (s *TMDBService) GetTVShow(id int, language string) (*models.TVShow, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/%d?%s", s.baseURL, id, params.Encode())
var tvShow models.TVShow
err := s.makeRequest(endpoint, &tvShow)
return &tvShow, err
}
// Map TMDB movie to unified content with prefixed IDs. Requires optional external IDs for imdbId.
func MapTMDBToUnifiedMovie(movie *models.Movie, external *models.ExternalIDs) *models.UnifiedContent {
if movie == nil {
return nil
}
genres := make([]models.UnifiedGenre, 0, len(movie.Genres))
for _, g := range movie.Genres {
name := strings.TrimSpace(g.Name)
id := strings.ToLower(strings.ReplaceAll(name, " ", "-"))
if id == "" {
id = strconv.Itoa(g.ID)
}
genres = append(genres, models.UnifiedGenre{ID: id, Name: name})
}
var imdb string
if external != nil {
imdb = external.IMDbID
}
var budgetPtr *int64
if movie.Budget > 0 {
v := movie.Budget
budgetPtr = &v
}
var revenuePtr *int64
if movie.Revenue > 0 {
v := movie.Revenue
revenuePtr = &v
}
ext := models.UnifiedExternalIDs{
KP: nil,
TMDB: &movie.ID,
IMDb: imdb,
}
return &models.UnifiedContent{
ID: strconv.Itoa(movie.ID),
SourceID: "tmdb_" + strconv.Itoa(movie.ID),
Title: movie.Title,
OriginalTitle: movie.OriginalTitle,
Description: movie.Overview,
ReleaseDate: movie.ReleaseDate,
EndDate: nil,
Type: "movie",
Genres: genres,
Rating: movie.VoteAverage,
PosterURL: movie.PosterPath,
BackdropURL: movie.BackdropPath,
Director: "",
Cast: []models.UnifiedCastMember{},
Duration: movie.Runtime,
Country: firstCountry(movie.ProductionCountries),
Language: movie.OriginalLanguage,
Budget: budgetPtr,
Revenue: revenuePtr,
IMDbID: imdb,
ExternalIDs: ext,
}
}
func firstCountry(countries []models.ProductionCountry) string {
if len(countries) == 0 {
return ""
}
if strings.TrimSpace(countries[0].Name) != "" {
return countries[0].Name
}
return countries[0].ISO31661
}
func (s *TMDBService) GetGenres(mediaType string, language string) (*models.GenresResponse, error) {
params := url.Values{}
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/genre/%s/list?%s", s.baseURL, mediaType, params.Encode())
var response models.GenresResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetAllGenres() (*models.GenresResponse, error) {
// Получаем жанры фильмов
movieGenres, err := s.GetGenres("movie", "ru-RU")
if err != nil {
return nil, err
}
// Получаем жанры сериалов
tvGenres, err := s.GetGenres("tv", "ru-RU")
if err != nil {
return nil, err
}
// Объединяем жанры, убирая дубликаты
allGenres := make(map[int]models.Genre)
for _, genre := range movieGenres.Genres {
allGenres[genre.ID] = genre
}
for _, genre := range tvGenres.Genres {
allGenres[genre.ID] = genre
}
// Преобразуем обратно в слайс
var genres []models.Genre
for _, genre := range allGenres {
genres = append(genres, genre)
}
return &models.GenresResponse{Genres: genres}, nil
}
func (s *TMDBService) GetPopularMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/popular?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetTopRatedMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/top_rated?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetUpcomingMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/upcoming?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetNowPlayingMovies(page int, language, region string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
if region != "" {
params.Set("region", region)
}
endpoint := fmt.Sprintf("%s/movie/now_playing?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetMovieRecommendations(id, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/movie/%d/recommendations?%s", s.baseURL, id, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetSimilarMovies(id, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/movie/%d/similar?%s", s.baseURL, id, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetPopularTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/popular?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetTopRatedTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/top_rated?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetOnTheAirTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/on_the_air?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetAiringTodayTVShows(page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/airing_today?%s", s.baseURL, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetTVRecommendations(id, page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/%d/recommendations?%s", s.baseURL, id, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetSimilarTVShows(id, page int, language string) (*models.TMDBTVResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/tv/%d/similar?%s", s.baseURL, id, params.Encode())
var response models.TMDBTVResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetMovieExternalIDs(id int) (*models.ExternalIDs, error) {
endpoint := fmt.Sprintf("%s/movie/%d/external_ids", s.baseURL, id)
var ids models.ExternalIDs
err := s.makeRequest(endpoint, &ids)
return &ids, err
}
func (s *TMDBService) GetTVExternalIDs(id int) (*models.ExternalIDs, error) {
endpoint := fmt.Sprintf("%s/tv/%d/external_ids", s.baseURL, id)
var ids models.ExternalIDs
err := s.makeRequest(endpoint, &ids)
return &ids, err
}
func (s *TMDBService) DiscoverMoviesByGenre(genreID, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
params.Set("with_genres", strconv.Itoa(genreID))
params.Set("sort_by", "popularity.desc")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/discover/movie?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) DiscoverTVByGenre(genreID, page int, language string) (*models.TMDBResponse, error) {
params := url.Values{}
params.Set("page", strconv.Itoa(page))
params.Set("with_genres", strconv.Itoa(genreID))
params.Set("sort_by", "popularity.desc")
if language != "" {
params.Set("language", language)
} else {
params.Set("language", "ru-RU")
}
endpoint := fmt.Sprintf("%s/discover/tv?%s", s.baseURL, params.Encode())
var response models.TMDBResponse
err := s.makeRequest(endpoint, &response)
return &response, err
}
func (s *TMDBService) GetTVSeason(tvID, seasonNumber int, language string) (*models.SeasonDetails, error) {
if language == "" {
language = "ru-RU"
}
endpoint := fmt.Sprintf("%s/tv/%d/season/%d?language=%s", s.baseURL, tvID, seasonNumber, language)
var season models.SeasonDetails
err := s.makeRequest(endpoint, &season)
return &season, err
}