mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 18:08:51 +05:00
Update 16 files
- /docs/swagger.yaml - /docs/swagger.json - /docs/docs.go - /internal/api/init.go - /internal/api/models.go - /internal/api/handlers.go - /internal/api/utils.go - /internal/tmdb/models.go - /internal/tmdb/client.go - /build.sh - /go.mod - /go.sum - /main.go - /render.yaml - /run.sh - /README.md
This commit is contained in:
505
internal/api/handlers.go
Normal file
505
internal/api/handlers.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"neomovies-api/internal/tmdb"
|
||||
)
|
||||
|
||||
// GetPopularMovies возвращает список популярных фильмов
|
||||
// @Summary Get popular movies
|
||||
// @Description Get a list of popular movies
|
||||
// @Tags movies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} MoviesResponse
|
||||
// @Router /movies/popular [get]
|
||||
func GetPopularMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetPopular(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// GetMovie возвращает информацию о фильме
|
||||
// @Summary Get movie details
|
||||
// @Description Get detailed information about a specific movie
|
||||
// @Tags movies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Movie ID"
|
||||
// @Success 200 {object} MovieDetails
|
||||
// @Router /movies/{id} [get]
|
||||
func GetMovie(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
movie, err := tmdbClient.GetMovie(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
if movie.PosterPath != "" {
|
||||
movie.PosterPath = tmdbClient.GetImageURL(movie.PosterPath, "original")
|
||||
}
|
||||
if movie.BackdropPath != "" {
|
||||
movie.BackdropPath = tmdbClient.GetImageURL(movie.BackdropPath, "original")
|
||||
}
|
||||
|
||||
// Обрабатываем изображения для коллекции
|
||||
if movie.BelongsToCollection != nil {
|
||||
if movie.BelongsToCollection.PosterPath != "" {
|
||||
movie.BelongsToCollection.PosterPath = tmdbClient.GetImageURL(movie.BelongsToCollection.PosterPath, "w500")
|
||||
}
|
||||
if movie.BelongsToCollection.BackdropPath != "" {
|
||||
movie.BelongsToCollection.BackdropPath = tmdbClient.GetImageURL(movie.BelongsToCollection.BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
// Обрабатываем логотипы компаний
|
||||
for i := range movie.ProductionCompanies {
|
||||
if movie.ProductionCompanies[i].LogoPath != "" {
|
||||
movie.ProductionCompanies[i].LogoPath = tmdbClient.GetImageURL(movie.ProductionCompanies[i].LogoPath, "w185")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movie)
|
||||
}
|
||||
|
||||
// SearchMovies ищет фильмы
|
||||
// @Summary Поиск фильмов
|
||||
// @Description Поиск фильмов по запросу
|
||||
// @Tags movies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string true "Поисковый запрос"
|
||||
// @Param page query string false "Номер страницы (по умолчанию 1)"
|
||||
// @Success 200 {object} SearchResponse
|
||||
// @Router /movies/search [get]
|
||||
func SearchMovies(c *gin.Context) {
|
||||
query := c.Query("query")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
// Получаем результаты поиска
|
||||
results, err := tmdbClient.SearchMovies(query, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Преобразуем результаты в формат ответа
|
||||
response := SearchResponse{
|
||||
Page: results.Page,
|
||||
TotalPages: results.TotalPages,
|
||||
TotalResults: results.TotalResults,
|
||||
Results: make([]MovieResponse, 0),
|
||||
}
|
||||
|
||||
// Преобразуем каждый фильм
|
||||
for _, movie := range results.Results {
|
||||
// Форматируем дату
|
||||
releaseDate := formatDate(movie.ReleaseDate)
|
||||
|
||||
// Добавляем фильм в результаты
|
||||
response.Results = append(response.Results, MovieResponse{
|
||||
ID: movie.ID,
|
||||
Title: movie.Title,
|
||||
Overview: movie.Overview,
|
||||
ReleaseDate: releaseDate,
|
||||
VoteAverage: movie.VoteAverage,
|
||||
PosterPath: tmdbClient.GetImageURL(movie.PosterPath, "w500"),
|
||||
BackdropPath: tmdbClient.GetImageURL(movie.BackdropPath, "w1280"),
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetTopRatedMovies возвращает список лучших фильмов
|
||||
// @Summary Get top rated movies
|
||||
// @Description Get a list of top rated movies
|
||||
// @Tags movies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} MoviesResponse
|
||||
// @Router /movies/top-rated [get]
|
||||
func GetTopRatedMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetTopRated(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// GetUpcomingMovies возвращает список предстоящих фильмов
|
||||
// @Summary Get upcoming movies
|
||||
// @Description Get a list of upcoming movies
|
||||
// @Tags movies
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} MoviesResponse
|
||||
// @Router /movies/upcoming [get]
|
||||
func GetUpcomingMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetUpcoming(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// GetTMDBPopularMovies возвращает список популярных фильмов из TMDB
|
||||
// @Summary Get TMDB popular movies
|
||||
// @Description Get a list of popular movies directly from TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} TMDBMoviesResponse
|
||||
// @Router /bridge/tmdb/movie/popular [get]
|
||||
func GetTMDBPopularMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetPopular(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// GetTMDBMovie возвращает информацию о фильме из TMDB
|
||||
// @Summary Get TMDB movie details
|
||||
// @Description Get detailed information about a specific movie directly from TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Movie ID"
|
||||
// @Success 200 {object} tmdb.Movie
|
||||
// @Router /bridge/tmdb/movie/{id} [get]
|
||||
func GetTMDBMovie(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
movie, err := tmdbClient.GetMovie(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movie)
|
||||
}
|
||||
|
||||
// GetTMDBTopRatedMovies возвращает список лучших фильмов из TMDB
|
||||
// @Summary Get TMDB top rated movies
|
||||
// @Description Get a list of top rated movies directly from TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} TMDBMoviesResponse
|
||||
// @Router /bridge/tmdb/movie/top_rated [get]
|
||||
func GetTMDBTopRatedMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetTopRated(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// GetTMDBUpcomingMovies возвращает список предстоящих фильмов из TMDB
|
||||
// @Summary Get TMDB upcoming movies
|
||||
// @Description Get a list of upcoming movies directly from TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} TMDBMoviesResponse
|
||||
// @Router /bridge/tmdb/movie/upcoming [get]
|
||||
func GetTMDBUpcomingMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
|
||||
movies, err := tmdbClient.GetUpcoming(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Добавляем полные URL для изображений
|
||||
for i := range movies.Results {
|
||||
if movies.Results[i].PosterPath != "" {
|
||||
movies.Results[i].PosterPath = tmdbClient.GetImageURL(movies.Results[i].PosterPath, "w500")
|
||||
}
|
||||
if movies.Results[i].BackdropPath != "" {
|
||||
movies.Results[i].BackdropPath = tmdbClient.GetImageURL(movies.Results[i].BackdropPath, "w1280")
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// SearchTMDBMovies ищет фильмы в TMDB
|
||||
// @Summary Search TMDB movies
|
||||
// @Description Search for movies directly in TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string true "Search query"
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} tmdb.MoviesResponse
|
||||
// @Router /bridge/tmdb/search/movie [get]
|
||||
func SearchTMDBMovies(c *gin.Context) {
|
||||
query := c.Query("query")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
movies, err := tmdbClient.SearchMovies(query, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// SearchTMDBTV ищет сериалы в TMDB
|
||||
// @Summary Search TMDB TV shows
|
||||
// @Description Search for TV shows directly in TMDB
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param query query string true "Search query"
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} tmdb.TVSearchResults
|
||||
// @Router /bridge/tmdb/search/tv [get]
|
||||
func SearchTMDBTV(c *gin.Context) {
|
||||
query := c.Query("query")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter is required"})
|
||||
return
|
||||
}
|
||||
|
||||
page := c.DefaultQuery("page", "1")
|
||||
tv, err := tmdbClient.SearchTV(query, page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tv)
|
||||
}
|
||||
|
||||
// DiscoverMovies возвращает список фильмов по фильтрам
|
||||
// @Summary Discover movies
|
||||
// @Description Get a list of movies based on filters
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} TMDBMoviesResponse
|
||||
// @Router /bridge/tmdb/discover/movie [get]
|
||||
func DiscoverMovies(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
movies, err := tmdbClient.DiscoverMovies(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, movies)
|
||||
}
|
||||
|
||||
// DiscoverTV возвращает список сериалов по фильтрам
|
||||
// @Summary Discover TV shows
|
||||
// @Description Get a list of TV shows based on filters
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number (default: 1)"
|
||||
// @Success 200 {object} TMDBMoviesResponse
|
||||
// @Router /bridge/tmdb/discover/tv [get]
|
||||
func DiscoverTV(c *gin.Context) {
|
||||
page := c.DefaultQuery("page", "1")
|
||||
shows, err := tmdbClient.DiscoverTV(page)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, shows)
|
||||
}
|
||||
|
||||
// GetTMDBMovieExternalIDs возвращает внешние идентификаторы фильма
|
||||
// @Summary Get TMDB movie external IDs
|
||||
// @Description Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific movie
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Movie ID"
|
||||
// @Success 200 {object} tmdb.ExternalIDs
|
||||
// @Router /bridge/tmdb/movie/{id}/external_ids [get]
|
||||
func GetTMDBMovieExternalIDs(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
externalIDs, err := tmdbClient.GetMovieExternalIDs(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, externalIDs)
|
||||
}
|
||||
|
||||
// GetTMDBTVExternalIDs возвращает внешние идентификаторы сериала
|
||||
// @Summary Get TMDB TV show external IDs
|
||||
// @Description Get external IDs (IMDb, Facebook, Instagram, Twitter) for a specific TV show
|
||||
// @Tags tmdb
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "TV Show ID"
|
||||
// @Success 200 {object} tmdb.ExternalIDs
|
||||
// @Router /bridge/tmdb/tv/{id}/external_ids [get]
|
||||
func GetTMDBTVExternalIDs(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
externalIDs, err := tmdbClient.GetTVExternalIDs(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, externalIDs)
|
||||
}
|
||||
|
||||
// HealthCheck godoc
|
||||
// @Summary Проверка работоспособности API
|
||||
// @Description Проверяет, что API работает
|
||||
// @Tags health
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Router /health [get]
|
||||
func HealthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
// InitTMDBClientWithProxy инициализирует TMDB клиент с прокси
|
||||
func InitTMDBClientWithProxy(apiKey string, proxyAddr string) error {
|
||||
tmdbClient = tmdb.NewClient(apiKey)
|
||||
return tmdbClient.SetSOCKS5Proxy(proxyAddr)
|
||||
}
|
||||
|
||||
// Admin handlers
|
||||
|
||||
// GetAdminMovies возвращает список фильмов для админа
|
||||
func GetAdminMovies(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Admin movies list"})
|
||||
}
|
||||
|
||||
// ToggleMovieVisibility переключает видимость фильма
|
||||
func ToggleMovieVisibility(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Movie visibility toggled"})
|
||||
}
|
||||
|
||||
// GetUsers возвращает список пользователей
|
||||
func GetUsers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Users list"})
|
||||
}
|
||||
|
||||
// CreateUser создает нового пользователя
|
||||
func CreateUser(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User created"})
|
||||
}
|
||||
|
||||
// ToggleAdmin переключает права администратора
|
||||
func ToggleAdmin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Admin status toggled"})
|
||||
}
|
||||
|
||||
// SendVerification отправляет код верификации
|
||||
func SendVerification(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Verification code sent"})
|
||||
}
|
||||
|
||||
// VerifyCode проверяет код верификации
|
||||
func VerifyCode(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Code verified"})
|
||||
}
|
||||
14
internal/api/init.go
Normal file
14
internal/api/init.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"neomovies-api/internal/tmdb"
|
||||
)
|
||||
|
||||
var (
|
||||
tmdbClient *tmdb.Client
|
||||
)
|
||||
|
||||
// InitTMDBClient инициализирует TMDB клиент
|
||||
func InitTMDBClient(apiKey string) {
|
||||
tmdbClient = tmdb.NewClient(apiKey)
|
||||
}
|
||||
64
internal/api/models.go
Normal file
64
internal/api/models.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package api
|
||||
|
||||
// Genre представляет жанр фильма
|
||||
type Genre struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Movie представляет базовую информацию о фильме
|
||||
type Movie struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Overview string `json:"overview"`
|
||||
PosterPath *string `json:"poster_path"`
|
||||
BackdropPath *string `json:"backdrop_path"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
Genres []Genre `json:"genres"`
|
||||
}
|
||||
|
||||
// MovieDetails представляет детальную информацию о фильме
|
||||
type MovieDetails struct {
|
||||
Movie
|
||||
Runtime int `json:"runtime"`
|
||||
Tagline string `json:"tagline"`
|
||||
Budget int `json:"budget"`
|
||||
Revenue int `json:"revenue"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// MoviesResponse представляет ответ со списком фильмов
|
||||
type MoviesResponse struct {
|
||||
Page int `json:"page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
Results []Movie `json:"results"`
|
||||
}
|
||||
|
||||
// TMDBMoviesResponse представляет ответ со списком фильмов от TMDB API
|
||||
type TMDBMoviesResponse struct {
|
||||
Page int `json:"page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
Results []Movie `json:"results"`
|
||||
}
|
||||
|
||||
// SearchResponse представляет ответ на поисковый запрос
|
||||
type SearchResponse struct {
|
||||
Page int `json:"page"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
Results []MovieResponse `json:"results"`
|
||||
}
|
||||
|
||||
// MovieResponse представляет информацию о фильме в ответе API
|
||||
type MovieResponse struct {
|
||||
ID int `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Overview string `json:"overview"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
}
|
||||
24
internal/api/utils.go
Normal file
24
internal/api/utils.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package api
|
||||
|
||||
import "time"
|
||||
|
||||
// formatDate форматирует дату в более читаемый формат
|
||||
func formatDate(date string) string {
|
||||
if date == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Парсим дату из формата YYYY-MM-DD
|
||||
t, err := time.Parse("2006-01-02", date)
|
||||
if err != nil {
|
||||
return date
|
||||
}
|
||||
|
||||
// Форматируем дату в русском стиле
|
||||
months := []string{
|
||||
"января", "февраля", "марта", "апреля", "мая", "июня",
|
||||
"июля", "августа", "сентября", "октября", "ноября", "декабря",
|
||||
}
|
||||
|
||||
return t.Format("2") + " " + months[t.Month()-1] + " " + t.Format("2006")
|
||||
}
|
||||
399
internal/tmdb/client.go
Normal file
399
internal/tmdb/client.go
Normal file
@@ -0,0 +1,399 @@
|
||||
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
|
||||
}
|
||||
76
internal/tmdb/models.go
Normal file
76
internal/tmdb/models.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package tmdb
|
||||
|
||||
// MoviesResponse представляет ответ от TMDB API со списком фильмов
|
||||
type MoviesResponse struct {
|
||||
Page int `json:"page"`
|
||||
Results []Movie `json:"results"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalResults int `json:"total_results"`
|
||||
}
|
||||
|
||||
// Movie представляет информацию о фильме
|
||||
type Movie struct {
|
||||
Adult bool `json:"adult"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
GenreIDs []int `json:"genre_ids"`
|
||||
ID int `json:"id"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
Overview string `json:"overview"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Title string `json:"title"`
|
||||
Video bool `json:"video"`
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
}
|
||||
|
||||
// Genre представляет жанр фильма
|
||||
type Genre struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// Collection представляет коллекцию фильмов
|
||||
type Collection struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
}
|
||||
|
||||
// ProductionCompany представляет компанию-производителя
|
||||
type ProductionCompany struct {
|
||||
ID int `json:"id"`
|
||||
LogoPath string `json:"logo_path"`
|
||||
Name string `json:"name"`
|
||||
Country string `json:"origin_country"`
|
||||
}
|
||||
|
||||
// MovieDetails представляет детальную информацию о фильме
|
||||
type MovieDetails struct {
|
||||
Adult bool `json:"adult"`
|
||||
BackdropPath string `json:"backdrop_path"`
|
||||
BelongsToCollection *Collection `json:"belongs_to_collection"`
|
||||
Budget int `json:"budget"`
|
||||
Genres []Genre `json:"genres"`
|
||||
Homepage string `json:"homepage"`
|
||||
ID int `json:"id"`
|
||||
IMDbID string `json:"imdb_id"`
|
||||
OriginalLanguage string `json:"original_language"`
|
||||
OriginalTitle string `json:"original_title"`
|
||||
Overview string `json:"overview"`
|
||||
Popularity float64 `json:"popularity"`
|
||||
PosterPath string `json:"poster_path"`
|
||||
ProductionCompanies []ProductionCompany `json:"production_companies"`
|
||||
ReleaseDate string `json:"release_date"`
|
||||
Revenue int `json:"revenue"`
|
||||
Runtime int `json:"runtime"`
|
||||
Status string `json:"status"`
|
||||
Tagline string `json:"tagline"`
|
||||
Title string `json:"title"`
|
||||
Video bool `json:"video"`
|
||||
VoteAverage float64 `json:"vote_average"`
|
||||
VoteCount int `json:"vote_count"`
|
||||
}
|
||||
Reference in New Issue
Block a user