mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-27 17:38:51 +05:00
400 lines
12 KiB
Go
400 lines
12 KiB
Go
|
|
package tmdb
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"encoding/json"
|
|||
|
|
"fmt"
|
|||
|
|
"io"
|
|||
|
|
"log"
|
|||
|
|
"net"
|
|||
|
|
"net/http"
|
|||
|
|
"net/url"
|
|||
|
|
"path"
|
|||
|
|
"time"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
baseURL = "https://api.themoviedb.org/3"
|
|||
|
|
imageBaseURL = "https://image.tmdb.org/t/p"
|
|||
|
|
googleDNS = "8.8.8.8:53" // Google Public DNS
|
|||
|
|
cloudflareDNS = "1.1.1.1:53" // Cloudflare DNS
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Client представляет клиент для работы с TMDB API
|
|||
|
|
type Client struct {
|
|||
|
|
apiKey string
|
|||
|
|
httpClient *http.Client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewClient создает новый клиент TMDB API с кастомным DNS
|
|||
|
|
func NewClient(apiKey string) *Client {
|
|||
|
|
// Создаем кастомный DNS резолвер с двумя DNS серверами
|
|||
|
|
dialer := &net.Dialer{
|
|||
|
|
Timeout: 5 * time.Second,
|
|||
|
|
Resolver: &net.Resolver{
|
|||
|
|
PreferGo: true,
|
|||
|
|
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|||
|
|
// Пробуем сначала Google DNS
|
|||
|
|
d := net.Dialer{Timeout: 5 * time.Second}
|
|||
|
|
conn, err := d.DialContext(ctx, "udp", googleDNS)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("Failed to connect to Google DNS, trying Cloudflare: %v", err)
|
|||
|
|
// Если Google DNS не отвечает, пробуем Cloudflare
|
|||
|
|
return d.DialContext(ctx, "udp", cloudflareDNS)
|
|||
|
|
}
|
|||
|
|
return conn, nil
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Создаем транспорт с кастомным диалером
|
|||
|
|
transport := &http.Transport{
|
|||
|
|
DialContext: dialer.DialContext,
|
|||
|
|
TLSHandshakeTimeout: 5 * time.Second,
|
|||
|
|
ResponseHeaderTimeout: 10 * time.Second,
|
|||
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|||
|
|
MaxIdleConns: 100,
|
|||
|
|
IdleConnTimeout: 90 * time.Second,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
client := &Client{
|
|||
|
|
apiKey: apiKey,
|
|||
|
|
httpClient: &http.Client{
|
|||
|
|
Transport: transport,
|
|||
|
|
Timeout: 10 * time.Second,
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Проверяем работу DNS и API
|
|||
|
|
log.Println("Testing DNS resolution and TMDB API access...")
|
|||
|
|
|
|||
|
|
// Тест 1: Проверяем резолвинг через DNS
|
|||
|
|
ips, err := net.LookupIP("api.themoviedb.org")
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("Warning: DNS lookup failed: %v", err)
|
|||
|
|
} else {
|
|||
|
|
log.Printf("Successfully resolved api.themoviedb.org to: %v", ips)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Тест 2: Проверяем наш IP
|
|||
|
|
resp, err := client.httpClient.Get("https://ipinfo.io/json")
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("Warning: Failed to check our IP: %v", err)
|
|||
|
|
} else {
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
var ipInfo struct {
|
|||
|
|
IP string `json:"ip"`
|
|||
|
|
City string `json:"city"`
|
|||
|
|
Country string `json:"country"`
|
|||
|
|
Org string `json:"org"`
|
|||
|
|
}
|
|||
|
|
if err := json.NewDecoder(resp.Body).Decode(&ipInfo); err != nil {
|
|||
|
|
log.Printf("Warning: Failed to decode IP info: %v", err)
|
|||
|
|
} else {
|
|||
|
|
log.Printf("Our IP info: IP=%s, City=%s, Country=%s, Org=%s",
|
|||
|
|
ipInfo.IP, ipInfo.City, ipInfo.Country, ipInfo.Org)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Тест 3: Проверяем доступ к TMDB API
|
|||
|
|
testURL := fmt.Sprintf("%s/movie/popular?api_key=%s", baseURL, apiKey)
|
|||
|
|
resp, err = client.httpClient.Get(testURL)
|
|||
|
|
if err != nil {
|
|||
|
|
log.Printf("Warning: TMDB API test failed: %v", err)
|
|||
|
|
} else {
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
if resp.StatusCode == http.StatusOK {
|
|||
|
|
log.Println("Successfully connected to TMDB API!")
|
|||
|
|
} else {
|
|||
|
|
log.Printf("Warning: TMDB API returned status code: %d", resp.StatusCode)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return client
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SetSOCKS5Proxy устанавливает SOCKS5 прокси для клиента
|
|||
|
|
func (c *Client) SetSOCKS5Proxy(proxyAddr string) error {
|
|||
|
|
return fmt.Errorf("proxy support has been removed in favor of custom DNS resolvers")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// makeRequest выполняет HTTP запрос к TMDB API
|
|||
|
|
func (c *Client) makeRequest(method, endpoint string, params url.Values) ([]byte, error) {
|
|||
|
|
// Создаем URL
|
|||
|
|
u, err := url.Parse(baseURL)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to parse base URL: %v", err)
|
|||
|
|
}
|
|||
|
|
u.Path = path.Join(u.Path, endpoint)
|
|||
|
|
if params == nil {
|
|||
|
|
params = url.Values{}
|
|||
|
|
}
|
|||
|
|
u.RawQuery = params.Encode()
|
|||
|
|
|
|||
|
|
// Создаем запрос
|
|||
|
|
req, err := http.NewRequest(method, u.String(), nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to create request: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Добавляем заголовок авторизации
|
|||
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|||
|
|
req.Header.Set("Content-Type", "application/json;charset=utf-8")
|
|||
|
|
|
|||
|
|
log.Printf("Making request to TMDB: %s %s", method, u.String())
|
|||
|
|
|
|||
|
|
// Выполняем запрос
|
|||
|
|
resp, err := c.httpClient.Do(req)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to make request: %v", err)
|
|||
|
|
}
|
|||
|
|
defer resp.Body.Close()
|
|||
|
|
|
|||
|
|
// Проверяем статус ответа
|
|||
|
|
if resp.StatusCode != http.StatusOK {
|
|||
|
|
body, _ := io.ReadAll(resp.Body)
|
|||
|
|
return nil, fmt.Errorf("TMDB API error: status=%d body=%s", resp.StatusCode, string(body))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Читаем тело ответа
|
|||
|
|
body, err := io.ReadAll(resp.Body)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return body, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetImageURL возвращает полный URL изображения
|
|||
|
|
func (c *Client) GetImageURL(path string, size string) string {
|
|||
|
|
if path == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return fmt.Sprintf("%s/%s%s", imageBaseURL, size, path)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetPopular получает список популярных фильмов
|
|||
|
|
func (c *Client) GetPopular(page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "movie/popular", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetMovie получает информацию о конкретном фильме
|
|||
|
|
func (c *Client) GetMovie(id string) (*MovieDetails, error) {
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("movie/%s", id), nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var movie MovieDetails
|
|||
|
|
if err := json.Unmarshal(body, &movie); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &movie, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SearchMovies ищет фильмы по запросу с поддержкой русского языка
|
|||
|
|
func (c *Client) SearchMovies(query string, page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("query", query)
|
|||
|
|
params.Set("page", page)
|
|||
|
|
params.Set("language", "ru-RU") // Добавляем русский язык
|
|||
|
|
params.Set("region", "RU") // Добавляем русский регион
|
|||
|
|
params.Set("include_adult", "false") // Исключаем взрослый контент
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "search/movie", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Фильтруем результаты
|
|||
|
|
filteredResults := make([]Movie, 0)
|
|||
|
|
for _, movie := range response.Results {
|
|||
|
|
// Проверяем, что у фильма есть постер и описание
|
|||
|
|
if movie.PosterPath != "" && movie.Overview != "" {
|
|||
|
|
// Проверяем, что рейтинг больше 0
|
|||
|
|
if movie.VoteAverage > 0 {
|
|||
|
|
filteredResults = append(filteredResults, movie)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Обновляем результаты
|
|||
|
|
response.Results = filteredResults
|
|||
|
|
response.TotalResults = len(filteredResults)
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTopRated получает список лучших фильмов
|
|||
|
|
func (c *Client) GetTopRated(page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "movie/top_rated", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetUpcoming получает список предстоящих фильмов
|
|||
|
|
func (c *Client) GetUpcoming(page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "movie/upcoming", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DiscoverMovies получает список фильмов по фильтрам
|
|||
|
|
func (c *Client) DiscoverMovies(page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "discover/movie", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// DiscoverTV получает список сериалов по фильтрам
|
|||
|
|
func (c *Client) DiscoverTV(page string) (*MoviesResponse, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "discover/tv", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var response MoviesResponse
|
|||
|
|
if err := json.Unmarshal(body, &response); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &response, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ExternalIDs содержит внешние идентификаторы фильма/сериала
|
|||
|
|
type ExternalIDs struct {
|
|||
|
|
ID int `json:"id"`
|
|||
|
|
IMDbID string `json:"imdb_id"`
|
|||
|
|
FacebookID string `json:"facebook_id"`
|
|||
|
|
InstagramID string `json:"instagram_id"`
|
|||
|
|
TwitterID string `json:"twitter_id"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetMovieExternalIDs возвращает внешние идентификаторы фильма
|
|||
|
|
func (c *Client) GetMovieExternalIDs(id string) (*ExternalIDs, error) {
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("movie/%s/external_ids", id), nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var externalIDs ExternalIDs
|
|||
|
|
if err := json.Unmarshal(body, &externalIDs); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &externalIDs, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTVExternalIDs возвращает внешние идентификаторы сериала
|
|||
|
|
func (c *Client) GetTVExternalIDs(id string) (*ExternalIDs, error) {
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, fmt.Sprintf("tv/%s/external_ids", id), nil)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var externalIDs ExternalIDs
|
|||
|
|
if err := json.Unmarshal(body, &externalIDs); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &externalIDs, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TVSearchResults содержит результаты поиска сериалов
|
|||
|
|
type TVSearchResults struct {
|
|||
|
|
Page int `json:"page"`
|
|||
|
|
TotalResults int `json:"total_results"`
|
|||
|
|
TotalPages int `json:"total_pages"`
|
|||
|
|
Results []TV `json:"results"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TV содержит информацию о сериале
|
|||
|
|
type TV struct {
|
|||
|
|
ID int `json:"id"`
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
OriginalName string `json:"original_name"`
|
|||
|
|
Overview string `json:"overview"`
|
|||
|
|
FirstAirDate string `json:"first_air_date"`
|
|||
|
|
PosterPath string `json:"poster_path"`
|
|||
|
|
BackdropPath string `json:"backdrop_path"`
|
|||
|
|
VoteAverage float64 `json:"vote_average"`
|
|||
|
|
VoteCount int `json:"vote_count"`
|
|||
|
|
Popularity float64 `json:"popularity"`
|
|||
|
|
OriginalLanguage string `json:"original_language"`
|
|||
|
|
GenreIDs []int `json:"genre_ids"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SearchTV ищет сериалы в TMDB
|
|||
|
|
func (c *Client) SearchTV(query string, page string) (*TVSearchResults, error) {
|
|||
|
|
params := url.Values{}
|
|||
|
|
params.Set("query", query)
|
|||
|
|
params.Set("page", page)
|
|||
|
|
|
|||
|
|
body, err := c.makeRequest(http.MethodGet, "search/tv", params)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var results TVSearchResults
|
|||
|
|
if err := json.Unmarshal(body, &results); err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding response: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &results, nil
|
|||
|
|
}
|