mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 09:58:50 +05:00
Merge branch 'main' into 'feature/add-streaming-players'
# Conflicts: # api/index.go # pkg/handlers/players.go
This commit is contained in:
@@ -3,6 +3,8 @@ package handlers
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
|
||||
@@ -16,9 +18,7 @@ type AuthHandler struct {
|
||||
}
|
||||
|
||||
func NewAuthHandler(authService *services.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
return &AuthHandler{authService: authService}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -36,11 +36,7 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: response,
|
||||
Message: "User registered successfully",
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: response, Message: "User registered successfully"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -50,23 +46,91 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.Login(req)
|
||||
// Получаем информацию о клиенте для refresh токена
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
ipAddress := r.RemoteAddr
|
||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||||
ipAddress = forwarded
|
||||
}
|
||||
|
||||
response, err := h.authService.LoginWithTokens(req, userAgent, ipAddress)
|
||||
if err != nil {
|
||||
// Определяем правильный статус код в зависимости от ошибки
|
||||
statusCode := http.StatusBadRequest
|
||||
if err.Error() == "Account not activated. Please verify your email." {
|
||||
statusCode = http.StatusForbidden // 403 для неверифицированного email
|
||||
statusCode = http.StatusForbidden
|
||||
}
|
||||
http.Error(w, err.Error(), statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: response,
|
||||
Message: "Login successful",
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: response, Message: "Login successful"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GoogleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
state := generateState()
|
||||
http.SetCookie(w, &http.Cookie{Name: "oauth_state", Value: state, HttpOnly: true, Path: "/", Expires: time.Now().Add(10 * time.Minute)})
|
||||
url, err := h.authService.GetGoogleLoginURL(state)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, url, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GoogleCallback(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
state := q.Get("state")
|
||||
code := q.Get("code")
|
||||
preferJSON := q.Get("response") == "json" || strings.Contains(r.Header.Get("Accept"), "application/json")
|
||||
cookie, _ := r.Cookie("oauth_state")
|
||||
if cookie == nil || cookie.Value != state || code == "" {
|
||||
if preferJSON {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: false, Message: "invalid oauth state"})
|
||||
return
|
||||
}
|
||||
redirectURL, ok := h.authService.BuildFrontendRedirect("", "invalid_state")
|
||||
if ok {
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, "invalid oauth state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := h.authService.HandleGoogleCallback(r.Context(), code)
|
||||
if err != nil {
|
||||
if preferJSON {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: false, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
redirectURL, ok := h.authService.BuildFrontendRedirect("", "auth_failed")
|
||||
if ok {
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if preferJSON {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: resp, Message: "Login successful"})
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, ok := h.authService.BuildFrontendRedirect(resp.Token, "")
|
||||
if ok {
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: resp, Message: "Login successful"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -83,10 +147,7 @@ func (h *AuthHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: user,
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: user})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -102,7 +163,6 @@ func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Удаляем поля, которые нельзя обновлять через этот эндпоинт
|
||||
delete(updates, "password")
|
||||
delete(updates, "email")
|
||||
delete(updates, "_id")
|
||||
@@ -115,14 +175,25 @@ func (h *AuthHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: user,
|
||||
Message: "Profile updated successfully",
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: user, Message: "Profile updated successfully"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) DeleteAccount(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.authService.DeleteAccount(r.Context(), userID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Message: "Account deleted successfully"})
|
||||
}
|
||||
|
||||
// Верификация email
|
||||
func (h *AuthHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.VerifyEmailRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -140,7 +211,6 @@ func (h *AuthHandler) VerifyEmail(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// Повторная отправка кода верификации
|
||||
func (h *AuthHandler) ResendVerificationCode(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.ResendCodeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -156,4 +226,84 @@ func (h *AuthHandler) ResendVerificationCode(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshToken refreshes an access token using a refresh token
|
||||
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
var req models.RefreshTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем информацию о клиенте
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
ipAddress := r.RemoteAddr
|
||||
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
|
||||
ipAddress = forwarded
|
||||
}
|
||||
|
||||
tokenPair, err := h.authService.RefreshAccessToken(req.RefreshToken, userAgent, ipAddress)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: tokenPair,
|
||||
Message: "Token refreshed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeRefreshToken revokes a specific refresh token
|
||||
func (h *AuthHandler) RevokeRefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req models.RefreshTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.authService.RevokeRefreshToken(userID, req.RefreshToken)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Refresh token revoked successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// RevokeAllRefreshTokens revokes all refresh tokens for the current user
|
||||
func (h *AuthHandler) RevokeAllRefreshTokens(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.authService.RevokeAllRefreshTokens(userID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "All refresh tokens revoked successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// helpers
|
||||
func generateState() string { return uuidNew() }
|
||||
|
||||
7
pkg/handlers/auth_helpers.go
Normal file
7
pkg/handlers/auth_helpers.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func uuidNew() string { return uuid.New().String() }
|
||||
@@ -53,7 +53,7 @@ func (h *CategoriesHandler) GetCategories(w http.ResponseWriter, r *http.Request
|
||||
})
|
||||
}
|
||||
|
||||
func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *CategoriesHandler) GetMediaByCategory(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
categoryID, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
@@ -67,20 +67,46 @@ func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.R
|
||||
language = "ru-RU"
|
||||
}
|
||||
|
||||
// Используем discover API для получения фильмов по жанру
|
||||
movies, err := h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
mediaType := r.URL.Query().Get("type")
|
||||
if mediaType == "" {
|
||||
mediaType = "movie" // По умолчанию фильмы для обратной совместимости
|
||||
}
|
||||
|
||||
if mediaType != "movie" && mediaType != "tv" {
|
||||
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var data interface{}
|
||||
var err2 error
|
||||
|
||||
if mediaType == "movie" {
|
||||
// Используем discover API для получения фильмов по жанру
|
||||
data, err2 = h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language)
|
||||
} else {
|
||||
// Используем discover API для получения сериалов по жанру
|
||||
data, err2 = h.tmdbService.DiscoverTVByGenre(categoryID, page, language)
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
http.Error(w, err2.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: movies,
|
||||
Data: data,
|
||||
Message: "Media retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// Старый метод для обратной совместимости
|
||||
func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.Request) {
|
||||
// Просто перенаправляем на новый метод
|
||||
h.GetMediaByCategory(w, r)
|
||||
}
|
||||
|
||||
func generateSlug(name string) string {
|
||||
// Простая функция для создания slug из названия
|
||||
// В реальном проекте стоит использовать более сложную логику
|
||||
@@ -93,4 +119,4 @@ func generateSlug(name string) string {
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
1060
pkg/handlers/docs.go
1060
pkg/handlers/docs.go
File diff suppressed because it is too large
Load Diff
260
pkg/handlers/favorites.go
Normal file
260
pkg/handlers/favorites.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"neomovies-api/pkg/config"
|
||||
"neomovies-api/pkg/middleware"
|
||||
"neomovies-api/pkg/models"
|
||||
"neomovies-api/pkg/services"
|
||||
)
|
||||
|
||||
type FavoritesHandler struct {
|
||||
favoritesService *services.FavoritesService
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler {
|
||||
return &FavoritesHandler{
|
||||
favoritesService: favoritesService,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *FavoritesHandler) GetFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
favorites, err := h.favoritesService.GetFavorites(userID)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to get favorites: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: favorites,
|
||||
Message: "Favorites retrieved successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FavoritesHandler) AddToFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
mediaID := vars["id"]
|
||||
mediaType := r.URL.Query().Get("type")
|
||||
|
||||
if mediaID == "" {
|
||||
http.Error(w, "Media ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if mediaType == "" {
|
||||
mediaType = "movie" // По умолчанию фильм для обратной совместимости
|
||||
}
|
||||
|
||||
if mediaType != "movie" && mediaType != "tv" {
|
||||
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Получаем информацию о медиа на русском языке
|
||||
mediaInfo, err := h.fetchMediaInfoRussian(mediaID, mediaType)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to fetch media information: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.favoritesService.AddToFavoritesWithInfo(userID, mediaID, mediaType, mediaInfo)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to add to favorites: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Added to favorites successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FavoritesHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
mediaID := vars["id"]
|
||||
mediaType := r.URL.Query().Get("type")
|
||||
|
||||
if mediaID == "" {
|
||||
http.Error(w, "Media ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if mediaType == "" {
|
||||
mediaType = "movie" // По умолчанию фильм для обратной совместимости
|
||||
}
|
||||
|
||||
if mediaType != "movie" && mediaType != "tv" {
|
||||
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.favoritesService.RemoveFromFavorites(userID, mediaID, mediaType)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to remove from favorites: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Removed from favorites successfully",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
mediaID := vars["id"]
|
||||
mediaType := r.URL.Query().Get("type")
|
||||
|
||||
if mediaID == "" {
|
||||
http.Error(w, "Media ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if mediaType == "" {
|
||||
mediaType = "movie" // По умолчанию фильм для обратной совместимости
|
||||
}
|
||||
|
||||
if mediaType != "movie" && mediaType != "tv" {
|
||||
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
isFavorite, err := h.favoritesService.IsFavorite(userID, mediaID, mediaType)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to check favorite status: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: map[string]bool{"isFavorite": isFavorite},
|
||||
})
|
||||
}
|
||||
|
||||
// fetchMediaInfoRussian получает информацию о медиа на русском языке из TMDB
|
||||
func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*models.MediaInfo, error) {
|
||||
var url string
|
||||
if mediaType == "movie" {
|
||||
url = fmt.Sprintf("https://api.themoviedb.org/3/movie/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken)
|
||||
} else {
|
||||
url = fmt.Sprintf("https://api.themoviedb.org/3/tv/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken)
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch from TMDB: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("TMDB API error: status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
}
|
||||
|
||||
var tmdbResponse map[string]interface{}
|
||||
if err := json.Unmarshal(body, &tmdbResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse TMDB response: %w", err)
|
||||
}
|
||||
|
||||
mediaInfo := &models.MediaInfo{
|
||||
ID: mediaID,
|
||||
MediaType: mediaType,
|
||||
}
|
||||
|
||||
// Заполняем информацию в зависимости от типа медиа
|
||||
if mediaType == "movie" {
|
||||
if title, ok := tmdbResponse["title"].(string); ok {
|
||||
mediaInfo.Title = title
|
||||
}
|
||||
if originalTitle, ok := tmdbResponse["original_title"].(string); ok {
|
||||
mediaInfo.OriginalTitle = originalTitle
|
||||
}
|
||||
if releaseDate, ok := tmdbResponse["release_date"].(string); ok {
|
||||
mediaInfo.ReleaseDate = releaseDate
|
||||
}
|
||||
} else {
|
||||
if name, ok := tmdbResponse["name"].(string); ok {
|
||||
mediaInfo.Title = name
|
||||
}
|
||||
if originalName, ok := tmdbResponse["original_name"].(string); ok {
|
||||
mediaInfo.OriginalTitle = originalName
|
||||
}
|
||||
if firstAirDate, ok := tmdbResponse["first_air_date"].(string); ok {
|
||||
mediaInfo.FirstAirDate = firstAirDate
|
||||
}
|
||||
}
|
||||
|
||||
// Общие поля
|
||||
if overview, ok := tmdbResponse["overview"].(string); ok {
|
||||
mediaInfo.Overview = overview
|
||||
}
|
||||
if posterPath, ok := tmdbResponse["poster_path"].(string); ok {
|
||||
mediaInfo.PosterPath = posterPath
|
||||
}
|
||||
if backdropPath, ok := tmdbResponse["backdrop_path"].(string); ok {
|
||||
mediaInfo.BackdropPath = backdropPath
|
||||
}
|
||||
if voteAverage, ok := tmdbResponse["vote_average"].(float64); ok {
|
||||
mediaInfo.VoteAverage = voteAverage
|
||||
}
|
||||
if voteCount, ok := tmdbResponse["vote_count"].(float64); ok {
|
||||
mediaInfo.VoteCount = int(voteCount)
|
||||
}
|
||||
if popularity, ok := tmdbResponse["popularity"].(float64); ok {
|
||||
mediaInfo.Popularity = popularity
|
||||
}
|
||||
|
||||
// Жанры
|
||||
if genres, ok := tmdbResponse["genres"].([]interface{}); ok {
|
||||
for _, genre := range genres {
|
||||
if genreMap, ok := genre.(map[string]interface{}); ok {
|
||||
if genreID, ok := genreMap["id"].(float64); ok {
|
||||
mediaInfo.GenreIDs = append(mediaInfo.GenreIDs, int(genreID))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mediaInfo, nil
|
||||
}
|
||||
@@ -26,4 +26,4 @@ func HealthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
var startTime = time.Now()
|
||||
|
||||
@@ -9,15 +9,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"neomovies-api/pkg/config"
|
||||
)
|
||||
|
||||
type ImagesHandler struct{}
|
||||
|
||||
func NewImagesHandler() *ImagesHandler {
|
||||
return &ImagesHandler{}
|
||||
}
|
||||
|
||||
const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p"
|
||||
func NewImagesHandler() *ImagesHandler { return &ImagesHandler{} }
|
||||
|
||||
func (h *ImagesHandler) GetImage(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
@@ -29,22 +26,18 @@ func (h *ImagesHandler) GetImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Если запрашивается placeholder, возвращаем локальный файл
|
||||
if imagePath == "placeholder.jpg" {
|
||||
h.servePlaceholder(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Проверяем размер изображения
|
||||
validSizes := []string{"w92", "w154", "w185", "w342", "w500", "w780", "original"}
|
||||
if !h.isValidSize(size, validSizes) {
|
||||
size = "original"
|
||||
}
|
||||
|
||||
// Формируем URL изображения
|
||||
imageURL := fmt.Sprintf("%s/%s/%s", TMDB_IMAGE_BASE_URL, size, imagePath)
|
||||
imageURL := fmt.Sprintf("%s/%s/%s", config.TMDBImageBaseURL, size, imagePath)
|
||||
|
||||
// Получаем изображение
|
||||
resp, err := http.Get(imageURL)
|
||||
if err != nil {
|
||||
h.servePlaceholder(w, r)
|
||||
@@ -57,23 +50,19 @@ func (h *ImagesHandler) GetImage(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем заголовки
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000") // кэшируем на 1 год
|
||||
w.Header().Set("Cache-Control", "public, max-age=31536000")
|
||||
|
||||
// Передаем изображение клиенту
|
||||
_, err = io.Copy(w, resp.Body)
|
||||
if err != nil {
|
||||
// Если ошибка при копировании, отдаем placeholder
|
||||
h.servePlaceholder(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ImagesHandler) servePlaceholder(w http.ResponseWriter, r *http.Request) {
|
||||
// Попробуем найти placeholder изображение
|
||||
placeholderPaths := []string{
|
||||
"./assets/placeholder.jpg",
|
||||
"./public/images/placeholder.jpg",
|
||||
@@ -89,7 +78,6 @@ func (h *ImagesHandler) servePlaceholder(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
if placeholderPath == "" {
|
||||
// Если placeholder не найден, создаем простую SVG заглушку
|
||||
h.serveSVGPlaceholder(w, r)
|
||||
return
|
||||
}
|
||||
@@ -101,7 +89,6 @@ func (h *ImagesHandler) servePlaceholder(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Определяем content-type по расширению
|
||||
ext := strings.ToLower(filepath.Ext(placeholderPath))
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
@@ -116,7 +103,7 @@ func (h *ImagesHandler) servePlaceholder(w http.ResponseWriter, r *http.Request)
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600") // кэшируем placeholder на 1 час
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
|
||||
_, err = io.Copy(w, file)
|
||||
if err != nil {
|
||||
@@ -144,4 +131,4 @@ func (h *ImagesHandler) isValidSize(size string, validSizes []string) bool {
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"neomovies-api/pkg/middleware"
|
||||
"neomovies-api/pkg/models"
|
||||
"neomovies-api/pkg/services"
|
||||
)
|
||||
@@ -190,74 +189,6 @@ func (h *MovieHandler) GetSimilar(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MovieHandler) GetFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
language := r.URL.Query().Get("language")
|
||||
|
||||
movies, err := h.movieService.GetFavorites(userID, language)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: movies,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MovieHandler) AddToFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
movieID := vars["id"]
|
||||
|
||||
err := h.movieService.AddToFavorites(userID, movieID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Movie added to favorites",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MovieHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, "User ID not found in context", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
movieID := vars["id"]
|
||||
|
||||
err := h.movieService.RemoveFromFavorites(userID, movieID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Movie removed from favorites",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *MovieHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["id"])
|
||||
@@ -284,11 +215,11 @@ func getIntQuery(r *http.Request, key string, defaultValue int) int {
|
||||
if str == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
|
||||
value, err := strconv.Atoi(str)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"neomovies-api/pkg/config"
|
||||
|
||||
@@ -16,12 +16,9 @@ type ReactionsHandler struct {
|
||||
}
|
||||
|
||||
func NewReactionsHandler(reactionsService *services.ReactionsService) *ReactionsHandler {
|
||||
return &ReactionsHandler{
|
||||
reactionsService: reactionsService,
|
||||
}
|
||||
return &ReactionsHandler{reactionsService: reactionsService}
|
||||
}
|
||||
|
||||
// Получить счетчики реакций для медиа (публичный эндпоинт)
|
||||
func (h *ReactionsHandler) GetReactionCounts(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
mediaType := vars["mediaType"]
|
||||
@@ -42,7 +39,6 @@ func (h *ReactionsHandler) GetReactionCounts(w http.ResponseWriter, r *http.Requ
|
||||
json.NewEncoder(w).Encode(counts)
|
||||
}
|
||||
|
||||
// Получить реакцию текущего пользователя (требует авторизации)
|
||||
func (h *ReactionsHandler) GetMyReaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
@@ -59,21 +55,20 @@ func (h *ReactionsHandler) GetMyReaction(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
reaction, err := h.reactionsService.GetUserReaction(userID, mediaType, mediaID)
|
||||
reactionType, err := h.reactionsService.GetMyReaction(userID, mediaType, mediaID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if reaction == nil {
|
||||
if reactionType == "" {
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{})
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(reaction)
|
||||
json.NewEncoder(w).Encode(map[string]string{"type": reactionType})
|
||||
}
|
||||
}
|
||||
|
||||
// Установить реакцию пользователя (требует авторизации)
|
||||
func (h *ReactionsHandler) SetReaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
@@ -93,31 +88,24 @@ func (h *ReactionsHandler) SetReaction(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if request.Type == "" {
|
||||
http.Error(w, "Reaction type is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := h.reactionsService.SetUserReaction(userID, mediaType, mediaID, request.Type)
|
||||
if err != nil {
|
||||
if err := h.reactionsService.SetReaction(userID, mediaType, mediaID, request.Type); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Reaction set successfully",
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Message: "Reaction set successfully"})
|
||||
}
|
||||
|
||||
// Удалить реакцию пользователя (требует авторизации)
|
||||
func (h *ReactionsHandler) RemoveReaction(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
@@ -134,20 +122,15 @@ func (h *ReactionsHandler) RemoveReaction(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
err := h.reactionsService.RemoveUserReaction(userID, mediaType, mediaID)
|
||||
if err != nil {
|
||||
if err := h.reactionsService.RemoveReaction(userID, mediaType, mediaID); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Message: "Reaction removed successfully",
|
||||
})
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Message: "Reaction removed successfully"})
|
||||
}
|
||||
|
||||
// Получить все реакции пользователя (требует авторизации)
|
||||
func (h *ReactionsHandler) GetMyReactions(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.GetUserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
@@ -164,8 +147,5 @@ func (h *ReactionsHandler) GetMyReactions(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(models.APIResponse{
|
||||
Success: true,
|
||||
Data: reactions,
|
||||
})
|
||||
}
|
||||
json.NewEncoder(w).Encode(models.APIResponse{Success: true, Data: reactions})
|
||||
}
|
||||
|
||||
@@ -42,4 +42,4 @@ func (h *SearchHandler) MultiSearch(w http.ResponseWriter, r *http.Request) {
|
||||
Success: true,
|
||||
Data: results,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,12 +123,12 @@ func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request)
|
||||
// Группируем сначала по сезонам, затем по качеству внутри каждого сезона
|
||||
seasonGroups := h.torrentService.GroupBySeason(results.Results)
|
||||
finalGroups := make(map[string]map[string][]models.TorrentResult)
|
||||
|
||||
|
||||
for season, torrents := range seasonGroups {
|
||||
qualityGroups := h.torrentService.GroupByQuality(torrents)
|
||||
finalGroups[season] = qualityGroups
|
||||
}
|
||||
|
||||
|
||||
response["grouped"] = true
|
||||
response["groups"] = finalGroups
|
||||
} else if options.GroupByQuality {
|
||||
@@ -364,4 +364,4 @@ func (h *TorrentsHandler) SearchByQuery(w http.ResponseWriter, r *http.Request)
|
||||
Success: true,
|
||||
Data: response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,4 +203,4 @@ func (h *TVHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) {
|
||||
Success: true,
|
||||
Data: externalIDs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user