Files
neomovies-api/pkg/handlers/favorites.go
2025-11-21 20:46:54 +02:00

363 lines
11 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 handlers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
"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
tmdbService *services.TMDBService
kpService *services.KinopoiskService
}
func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler {
return &FavoritesHandler{
favoritesService: favoritesService,
config: cfg,
}
}
func NewFavoritesHandlerWithServices(favoritesService *services.FavoritesService, cfg *config.Config, tmdb *services.TMDBService, kp *services.KinopoiskService) *FavoritesHandler {
return &FavoritesHandler{
favoritesService: favoritesService,
config: cfg,
tmdbService: tmdb,
kpService: kp,
}
}
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 или Kinopoisk
func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*models.MediaInfo, error) {
mediaIDInt, err := strconv.Atoi(mediaID)
if err != nil {
return nil, fmt.Errorf("invalid media ID: %s", mediaID)
}
// Пробуем Kinopoisk сначала, если доступен
if h.kpService != nil {
if kpFilm, err := h.kpService.GetFilmByKinopoiskId(mediaIDInt); err == nil {
return h.mapKPFilmToMediaInfo(kpFilm, mediaType), nil
}
}
// Fallback на TMDB
if h.tmdbService != nil {
if mediaType == "movie" {
movie, err := h.tmdbService.GetMovie(mediaIDInt, "ru-RU")
if err == nil {
return h.mapTMDBMovieToMediaInfo(movie), nil
}
} else if mediaType == "tv" {
tv, err := h.tmdbService.GetTVShow(mediaIDInt, "ru-RU")
if err == nil {
return h.mapTMDBTVToMediaInfo(tv), nil
}
}
}
// Если оба сервиса не доступны, пробуем прямой запрос к TMDB API
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)
}
client := &http.Client{Timeout: 6 * time.Second}
resp, err := client.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
}
// mapKPFilmToMediaInfo преобразует KP фильм в MediaInfo
func (h *FavoritesHandler) mapKPFilmToMediaInfo(kpFilm *services.KPFilm, mediaType string) *models.MediaInfo {
mediaInfo := &models.MediaInfo{
ID: fmt.Sprintf("%d", kpFilm.KinopoiskId),
MediaType: mediaType,
Title: kpFilm.NameRu,
OriginalTitle: kpFilm.NameEn,
Overview: kpFilm.Description,
PosterPath: kpFilm.PosterUrl,
BackdropPath: kpFilm.CoverUrl,
VoteAverage: kpFilm.RatingKinopoisk,
VoteCount: kpFilm.RatingKinopoiskVoteCount,
Popularity: kpFilm.RatingImdb,
}
if mediaType == "movie" {
mediaInfo.ReleaseDate = fmt.Sprintf("%d", kpFilm.Year)
} else {
mediaInfo.FirstAirDate = fmt.Sprintf("%d", kpFilm.Year)
}
return mediaInfo
}
// mapTMDBMovieToMediaInfo преобразует TMDB фильм в MediaInfo
func (h *FavoritesHandler) mapTMDBMovieToMediaInfo(movie *models.Movie) *models.MediaInfo {
mediaInfo := &models.MediaInfo{
ID: fmt.Sprintf("%d", movie.ID),
MediaType: "movie",
Title: movie.Title,
OriginalTitle: movie.OriginalTitle,
Overview: movie.Overview,
PosterPath: movie.PosterPath,
BackdropPath: movie.BackdropPath,
ReleaseDate: movie.ReleaseDate,
VoteAverage: movie.VoteAverage,
VoteCount: movie.VoteCount,
Popularity: movie.Popularity,
}
return mediaInfo
}
// mapTMDBTVToMediaInfo преобразует TMDB сериал в MediaInfo
func (h *FavoritesHandler) mapTMDBTVToMediaInfo(tv *models.TVShow) *models.MediaInfo {
mediaInfo := &models.MediaInfo{
ID: fmt.Sprintf("%d", tv.ID),
MediaType: "tv",
Title: tv.Name,
OriginalTitle: tv.OriginalName,
Overview: tv.Overview,
PosterPath: tv.PosterPath,
BackdropPath: tv.BackdropPath,
FirstAirDate: tv.FirstAirDate,
VoteAverage: tv.VoteAverage,
VoteCount: tv.VoteCount,
Popularity: tv.Popularity,
}
return mediaInfo
}