From a68dbcdad49e26b90cba0ae406719b99e62bfb87 Mon Sep 17 00:00:00 2001 From: Ernous Date: Thu, 28 Aug 2025 21:25:21 +0300 Subject: [PATCH] bug fixes --- api/index.go | 242 ++++++++-------- main.go | 18 +- pkg/config/config.go | 62 ++-- pkg/handlers/favorites.go | 48 +-- pkg/handlers/webtorrent.go | 578 ------------------------------------- pkg/models/movie.go | 243 ++++++++-------- pkg/services/favorites.go | 75 +++-- 7 files changed, 364 insertions(+), 902 deletions(-) delete mode 100644 pkg/handlers/webtorrent.go diff --git a/api/index.go b/api/index.go index a16dd2f..a23a4bd 100644 --- a/api/index.go +++ b/api/index.go @@ -1,160 +1,158 @@ package handler import ( - "log" - "net/http" - "sync" + "log" + "net/http" + "sync" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/joho/godotenv" - "go.mongodb.org/mongo-driver/mongo" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "go.mongodb.org/mongo-driver/mongo" - "neomovies-api/pkg/config" - "neomovies-api/pkg/database" - handlersPkg "neomovies-api/pkg/handlers" - "neomovies-api/pkg/middleware" - "neomovies-api/pkg/services" + "neomovies-api/pkg/config" + "neomovies-api/pkg/database" + handlersPkg "neomovies-api/pkg/handlers" + "neomovies-api/pkg/middleware" + "neomovies-api/pkg/services" ) var ( - globalDB *mongo.Database - globalCfg *config.Config - initOnce sync.Once - initError error + globalDB *mongo.Database + globalCfg *config.Config + initOnce sync.Once + initError error ) func initializeApp() { - if err := godotenv.Load(); err != nil { _ = err } + if err := godotenv.Load(); err != nil { + _ = err + } - globalCfg = config.New() + globalCfg = config.New() - var err error - globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName) - if err != nil { - log.Printf("Failed to connect to database: %v", err) - initError = err - return - } + var err error + globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName) + if err != nil { + log.Printf("Failed to connect to database: %v", err) + initError = err + return + } - log.Println("Successfully connected to database") + log.Println("Successfully connected to database") } func Handler(w http.ResponseWriter, r *http.Request) { - initOnce.Do(initializeApp) + initOnce.Do(initializeApp) - if initError != nil { - log.Printf("Initialization error: %v", initError) - http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError) - return - } + if initError != nil { + log.Printf("Initialization error: %v", initError) + http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError) + return + } - tmdbService := services.NewTMDBService(globalCfg.TMDBAccessToken) - emailService := services.NewEmailService(globalCfg) - authService := services.NewAuthService(globalDB, globalCfg.JWTSecret, emailService, globalCfg.BaseURL, globalCfg.GoogleClientID, globalCfg.GoogleClientSecret, globalCfg.GoogleRedirectURL, globalCfg.FrontendURL) + tmdbService := services.NewTMDBService(globalCfg.TMDBAccessToken) + emailService := services.NewEmailService(globalCfg) + authService := services.NewAuthService(globalDB, globalCfg.JWTSecret, emailService, globalCfg.BaseURL, globalCfg.GoogleClientID, globalCfg.GoogleClientSecret, globalCfg.GoogleRedirectURL, globalCfg.FrontendURL) - movieService := services.NewMovieService(globalDB, tmdbService) - tvService := services.NewTVService(globalDB, tmdbService) - favoritesService := services.NewFavoritesService(globalDB, tmdbService) - torrentService := services.NewTorrentServiceWithConfig(globalCfg.RedAPIBaseURL, globalCfg.RedAPIKey) - reactionsService := services.NewReactionsService(globalDB) + movieService := services.NewMovieService(globalDB, tmdbService) + tvService := services.NewTVService(globalDB, tmdbService) + favoritesService := services.NewFavoritesService(globalDB, tmdbService) + torrentService := services.NewTorrentServiceWithConfig(globalCfg.RedAPIBaseURL, globalCfg.RedAPIKey) + reactionsService := services.NewReactionsService(globalDB) - authHandler := handlersPkg.NewAuthHandler(authService) - movieHandler := handlersPkg.NewMovieHandler(movieService) - tvHandler := handlersPkg.NewTVHandler(tvService) - favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService) - docsHandler := handlersPkg.NewDocsHandler() - searchHandler := handlersPkg.NewSearchHandler(tmdbService) - categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService) - playersHandler := handlersPkg.NewPlayersHandler(globalCfg) - webtorrentHandler := handlersPkg.NewWebTorrentHandler(tmdbService) - torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService) - reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService) - imagesHandler := handlersPkg.NewImagesHandler() + authHandler := handlersPkg.NewAuthHandler(authService) + movieHandler := handlersPkg.NewMovieHandler(movieService) + tvHandler := handlersPkg.NewTVHandler(tvService) + favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg) + docsHandler := handlersPkg.NewDocsHandler() + searchHandler := handlersPkg.NewSearchHandler(tmdbService) + categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService) + playersHandler := handlersPkg.NewPlayersHandler(globalCfg) + torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService) + reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService) + imagesHandler := handlersPkg.NewImagesHandler() - router := mux.NewRouter() + router := mux.NewRouter() - router.HandleFunc("/", docsHandler.ServeDocs).Methods("GET") - router.HandleFunc("/openapi.json", docsHandler.GetOpenAPISpec).Methods("GET") + router.HandleFunc("/", docsHandler.ServeDocs).Methods("GET") + router.HandleFunc("/openapi.json", docsHandler.GetOpenAPISpec).Methods("GET") - api := router.PathPrefix("/api/v1").Subrouter() + api := router.PathPrefix("/api/v1").Subrouter() - api.HandleFunc("/health", handlersPkg.HealthCheck).Methods("GET") - api.HandleFunc("/auth/register", authHandler.Register).Methods("POST") - api.HandleFunc("/auth/login", authHandler.Login).Methods("POST") - api.HandleFunc("/auth/verify", authHandler.VerifyEmail).Methods("POST") - api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST") - api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") - api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET") + api.HandleFunc("/health", handlersPkg.HealthCheck).Methods("GET") + api.HandleFunc("/auth/register", authHandler.Register).Methods("POST") + api.HandleFunc("/auth/login", authHandler.Login).Methods("POST") + api.HandleFunc("/auth/verify", authHandler.VerifyEmail).Methods("POST") + api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST") + api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") + api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET") - api.HandleFunc("/search/multi", searchHandler.MultiSearch).Methods("GET") + api.HandleFunc("/search/multi", searchHandler.MultiSearch).Methods("GET") - api.HandleFunc("/categories", categoriesHandler.GetCategories).Methods("GET") - api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET") - api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET") + api.HandleFunc("/categories", categoriesHandler.GetCategories).Methods("GET") + api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET") + api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET") - api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET") - api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET") - api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET") + api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET") + api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET") + api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET") - api.HandleFunc("/webtorrent/player", webtorrentHandler.OpenPlayer).Methods("GET") - api.HandleFunc("/webtorrent/metadata", webtorrentHandler.GetMetadata).Methods("GET") + api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET") + api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") + api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET") + api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET") + api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET") + api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET") - api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET") - api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") - api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET") - api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET") - api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET") - api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET") - - api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET") + api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET") - api.HandleFunc("/images/{size}/{path:.*}", imagesHandler.GetImage).Methods("GET") + api.HandleFunc("/images/{size}/{path:.*}", imagesHandler.GetImage).Methods("GET") - api.HandleFunc("/movies/search", movieHandler.Search).Methods("GET") - api.HandleFunc("/movies/popular", movieHandler.Popular).Methods("GET") - api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET") - api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET") - api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET") - api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET") - api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET") - api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET") - api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET") + api.HandleFunc("/movies/search", movieHandler.Search).Methods("GET") + api.HandleFunc("/movies/popular", movieHandler.Popular).Methods("GET") + api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET") + api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET") + api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET") + api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET") + api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET") + api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET") + api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET") - api.HandleFunc("/tv/search", tvHandler.Search).Methods("GET") - api.HandleFunc("/tv/popular", tvHandler.Popular).Methods("GET") - api.HandleFunc("/tv/top-rated", tvHandler.TopRated).Methods("GET") - api.HandleFunc("/tv/on-the-air", tvHandler.OnTheAir).Methods("GET") - api.HandleFunc("/tv/airing-today", tvHandler.AiringToday).Methods("GET") - api.HandleFunc("/tv/{id}", tvHandler.GetByID).Methods("GET") - api.HandleFunc("/tv/{id}/recommendations", tvHandler.GetRecommendations).Methods("GET") - api.HandleFunc("/tv/{id}/similar", tvHandler.GetSimilar).Methods("GET") - api.HandleFunc("/tv/{id}/external-ids", tvHandler.GetExternalIDs).Methods("GET") + api.HandleFunc("/tv/search", tvHandler.Search).Methods("GET") + api.HandleFunc("/tv/popular", tvHandler.Popular).Methods("GET") + api.HandleFunc("/tv/top-rated", tvHandler.TopRated).Methods("GET") + api.HandleFunc("/tv/on-the-air", tvHandler.OnTheAir).Methods("GET") + api.HandleFunc("/tv/airing-today", tvHandler.AiringToday).Methods("GET") + api.HandleFunc("/tv/{id}", tvHandler.GetByID).Methods("GET") + api.HandleFunc("/tv/{id}/recommendations", tvHandler.GetRecommendations).Methods("GET") + api.HandleFunc("/tv/{id}/similar", tvHandler.GetSimilar).Methods("GET") + api.HandleFunc("/tv/{id}/external-ids", tvHandler.GetExternalIDs).Methods("GET") - protected := api.PathPrefix("").Subrouter() - protected.Use(middleware.JWTAuth(globalCfg.JWTSecret)) + protected := api.PathPrefix("").Subrouter() + protected.Use(middleware.JWTAuth(globalCfg.JWTSecret)) - protected.HandleFunc("/favorites", favoritesHandler.GetFavorites).Methods("GET") - protected.HandleFunc("/favorites/{id}", favoritesHandler.AddToFavorites).Methods("POST") - protected.HandleFunc("/favorites/{id}", favoritesHandler.RemoveFromFavorites).Methods("DELETE") - protected.HandleFunc("/favorites/{id}/check", favoritesHandler.CheckIsFavorite).Methods("GET") + protected.HandleFunc("/favorites", favoritesHandler.GetFavorites).Methods("GET") + protected.HandleFunc("/favorites/{id}", favoritesHandler.AddToFavorites).Methods("POST") + protected.HandleFunc("/favorites/{id}", favoritesHandler.RemoveFromFavorites).Methods("DELETE") + protected.HandleFunc("/favorites/{id}/check", favoritesHandler.CheckIsFavorite).Methods("GET") - protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET") - protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT") - protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE") - - protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET") - protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST") - protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE") - protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET") + protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET") + protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT") + protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE") - corsHandler := handlers.CORS( - handlers.AllowedOrigins([]string{"*"}), - handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), - handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}), - handlers.AllowCredentials(), - handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}), - ) + protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET") + protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST") + protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE") + protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET") - corsHandler(router).ServeHTTP(w, r) -} \ No newline at end of file + corsHandler := handlers.CORS( + handlers.AllowedOrigins([]string{"*"}), + handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), + handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}), + handlers.AllowCredentials(), + handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}), + ) + + corsHandler(router).ServeHTTP(w, r) +} diff --git a/main.go b/main.go index 9e11250..28208af 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,9 @@ import ( ) func main() { - if err := godotenv.Load(); err != nil { _ = err } + if err := godotenv.Load(); err != nil { + _ = err + } cfg := config.New() @@ -42,12 +44,11 @@ func main() { authHandler := appHandlers.NewAuthHandler(authService) movieHandler := appHandlers.NewMovieHandler(movieService) tvHandler := appHandlers.NewTVHandler(tvService) - favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService) + favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg) docsHandler := appHandlers.NewDocsHandler() searchHandler := appHandlers.NewSearchHandler(tmdbService) categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService) playersHandler := appHandlers.NewPlayersHandler(cfg) - webtorrentHandler := appHandlers.NewWebTorrentHandler(tmdbService) torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService) reactionsHandler := appHandlers.NewReactionsHandler(reactionsService) imagesHandler := appHandlers.NewImagesHandler() @@ -75,10 +76,7 @@ func main() { api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).Methods("GET") api.HandleFunc("/players/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET") - api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET") - - api.HandleFunc("/webtorrent/player", webtorrentHandler.OpenPlayer).Methods("GET") - api.HandleFunc("/webtorrent/metadata", webtorrentHandler.GetMetadata).Methods("GET") + api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET") api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET") api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") @@ -154,10 +152,12 @@ func main() { } port := cfg.Port - if port == "" { port = "3000" } + if port == "" { + port = "3000" + } if err := http.ListenAndServe(":"+port, finalHandler); err != nil { fmt.Printf("❌ Server failed to start: %v\n", err) os.Exit(1) } -} \ No newline at end of file +} diff --git a/pkg/config/config.go b/pkg/config/config.go index eda0414..b05d585 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -6,46 +6,50 @@ import ( ) type Config struct { - MongoURI string - MongoDBName string - TMDBAccessToken string - JWTSecret string - Port string - BaseURL string - NodeEnv string - GmailUser string - GmailPassword string - LumexURL string - AllohaToken string - RedAPIBaseURL string - RedAPIKey string - GoogleClientID string + MongoURI string + MongoDBName string + TMDBAccessToken string + JWTSecret string + Port string + BaseURL string + NodeEnv string + GmailUser string + GmailPassword string + LumexURL string + AllohaToken string + RedAPIBaseURL string + RedAPIKey string + GoogleClientID string GoogleClientSecret string GoogleRedirectURL string FrontendURL string + VibixHost string + VibixToken string } func New() *Config { mongoURI := getMongoURI() return &Config{ - MongoURI: mongoURI, - MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName), - TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""), - JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret), - Port: getEnv(EnvPort, DefaultPort), - BaseURL: getEnv(EnvBaseURL, DefaultBaseURL), - NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv), - GmailUser: getEnv(EnvGmailUser, ""), - GmailPassword: getEnv(EnvGmailPassword, ""), - LumexURL: getEnv(EnvLumexURL, ""), - AllohaToken: getEnv(EnvAllohaToken, ""), - RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase), - RedAPIKey: getEnv(EnvRedAPIKey, ""), - GoogleClientID: getEnv(EnvGoogleClientID, ""), + MongoURI: mongoURI, + MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName), + TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""), + JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret), + Port: getEnv(EnvPort, DefaultPort), + BaseURL: getEnv(EnvBaseURL, DefaultBaseURL), + NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv), + GmailUser: getEnv(EnvGmailUser, ""), + GmailPassword: getEnv(EnvGmailPassword, ""), + LumexURL: getEnv(EnvLumexURL, ""), + AllohaToken: getEnv(EnvAllohaToken, ""), + RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase), + RedAPIKey: getEnv(EnvRedAPIKey, ""), + GoogleClientID: getEnv(EnvGoogleClientID, ""), GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""), GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""), FrontendURL: getEnv(EnvFrontendURL, ""), + VibixHost: getEnv(EnvVibixHost, DefaultVibixHost), + VibixToken: getEnv(EnvVibixToken, ""), } } @@ -65,4 +69,4 @@ func getEnv(key, defaultValue string) string { return value } return defaultValue -} \ No newline at end of file +} diff --git a/pkg/handlers/favorites.go b/pkg/handlers/favorites.go index 858dd62..7fc42f7 100644 --- a/pkg/handlers/favorites.go +++ b/pkg/handlers/favorites.go @@ -8,10 +8,10 @@ import ( "github.com/gorilla/mux" + "neomovies-api/pkg/config" "neomovies-api/pkg/middleware" "neomovies-api/pkg/models" "neomovies-api/pkg/services" - "neomovies-api/pkg/config" ) type FavoritesHandler struct { @@ -22,26 +22,10 @@ type FavoritesHandler struct { func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler { return &FavoritesHandler{ favoritesService: favoritesService, - config: cfg, + config: cfg, } } -type MediaInfo struct { - ID string `json:"id"` - Title string `json:"title"` - OriginalTitle string `json:"original_title,omitempty"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - ReleaseDate string `json:"release_date,omitempty"` - FirstAirDate string `json:"first_air_date,omitempty"` - VoteAverage float64 `json:"vote_average"` - VoteCount int `json:"vote_count"` - MediaType string `json:"media_type"` - Popularity float64 `json:"popularity"` - GenreIDs []int `json:"genre_ids"` -} - func (h *FavoritesHandler) GetFavorites(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.GetUserIDFromContext(r.Context()) if !ok { @@ -73,16 +57,16 @@ func (h *FavoritesHandler) AddToFavorites(w http.ResponseWriter, r *http.Request 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 @@ -118,16 +102,16 @@ func (h *FavoritesHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Re 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 @@ -156,16 +140,16 @@ func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Reques 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 @@ -185,12 +169,12 @@ func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Reques } // fetchMediaInfoRussian получает информацию о медиа на русском языке из TMDB -func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*MediaInfo, error) { +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.TMDBAPIKey) + 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.TMDBAPIKey) + 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) @@ -213,7 +197,7 @@ func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*Me return nil, fmt.Errorf("failed to parse TMDB response: %w", err) } - mediaInfo := &MediaInfo{ + mediaInfo := &models.MediaInfo{ ID: mediaID, MediaType: mediaType, } @@ -273,4 +257,4 @@ func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*Me } return mediaInfo, nil -} \ No newline at end of file +} diff --git a/pkg/handlers/webtorrent.go b/pkg/handlers/webtorrent.go deleted file mode 100644 index 117e28f..0000000 --- a/pkg/handlers/webtorrent.go +++ /dev/null @@ -1,578 +0,0 @@ -package handlers - -import ( - "encoding/json" - "html/template" - "net/http" - "net/url" - "strconv" - - "neomovies-api/pkg/models" - "neomovies-api/pkg/services" -) - -type WebTorrentHandler struct { - tmdbService *services.TMDBService -} - -func NewWebTorrentHandler(tmdbService *services.TMDBService) *WebTorrentHandler { - return &WebTorrentHandler{ - tmdbService: tmdbService, - } -} - -// Структура для ответа с метаданными -type MediaMetadata struct { - ID int `json:"id"` - Title string `json:"title"` - Type string `json:"type"` // "movie" or "tv" - Year int `json:"year,omitempty"` - PosterPath string `json:"posterPath,omitempty"` - BackdropPath string `json:"backdropPath,omitempty"` - Overview string `json:"overview,omitempty"` - Seasons []SeasonMetadata `json:"seasons,omitempty"` - Episodes []EpisodeMetadata `json:"episodes,omitempty"` - Runtime int `json:"runtime,omitempty"` - Genres []models.Genre `json:"genres,omitempty"` -} - -type SeasonMetadata struct { - SeasonNumber int `json:"seasonNumber"` - Name string `json:"name"` - Episodes []EpisodeMetadata `json:"episodes"` -} - -type EpisodeMetadata struct { - EpisodeNumber int `json:"episodeNumber"` - SeasonNumber int `json:"seasonNumber"` - Name string `json:"name"` - Overview string `json:"overview,omitempty"` - Runtime int `json:"runtime,omitempty"` - StillPath string `json:"stillPath,omitempty"` -} - -// Открытие плеера с магнет ссылкой -func (h *WebTorrentHandler) OpenPlayer(w http.ResponseWriter, r *http.Request) { - magnetLink := r.Header.Get("X-Magnet-Link") - if magnetLink == "" { - magnetLink = r.URL.Query().Get("magnet") - } - - if magnetLink == "" { - http.Error(w, "Magnet link is required", http.StatusBadRequest) - return - } - - // Декодируем magnet ссылку если она закодирована - decodedMagnet, err := url.QueryUnescape(magnetLink) - if err != nil { - decodedMagnet = magnetLink - } - - // Отдаем HTML страницу с плеером - tmpl := ` - - - - - NeoMovies WebTorrent Player - - - - -
-
-
-
Загружаем торрент...
-
-
- -
-
-
-
- -
-
-
-
- - -
- - - -` - - // Создаем template и выполняем его - t, err := template.New("player").Parse(tmpl) - if err != nil { - http.Error(w, "Template error", http.StatusInternalServerError) - return - } - - data := struct { - MagnetLink string - }{ - MagnetLink: strconv.Quote(decodedMagnet), - } - - w.Header().Set("Content-Type", "text/html; charset=utf-8") - err = t.Execute(w, data) - if err != nil { - http.Error(w, "Template execution error", http.StatusInternalServerError) - return - } -} - -// API для получения метаданных фильма/сериала по названию -func (h *WebTorrentHandler) GetMetadata(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query().Get("query") - if query == "" { - http.Error(w, "Query parameter is required", http.StatusBadRequest) - return - } - - // Пытаемся определить тип контента и найти его - metadata, err := h.searchAndBuildMetadata(query) - if err != nil { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(models.APIResponse{ - Success: false, - Message: "Media not found: " + err.Error(), - }) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(models.APIResponse{ - Success: true, - Data: metadata, - }) -} - -func (h *WebTorrentHandler) searchAndBuildMetadata(query string) (*MediaMetadata, error) { - // Сначала пробуем поиск по фильмам - movieResults, err := h.tmdbService.SearchMovies(query, 1, "ru-RU", "", 0) - if err == nil && len(movieResults.Results) > 0 { - movie := movieResults.Results[0] - - // Получаем детальную информацию о фильме - fullMovie, err := h.tmdbService.GetMovie(movie.ID, "ru-RU") - if err == nil { - return &MediaMetadata{ - ID: fullMovie.ID, - Title: fullMovie.Title, - Type: "movie", - Year: extractYear(fullMovie.ReleaseDate), - PosterPath: fullMovie.PosterPath, - BackdropPath: fullMovie.BackdropPath, - Overview: fullMovie.Overview, - Runtime: fullMovie.Runtime, - Genres: fullMovie.Genres, - }, nil - } - } - - // Затем пробуем поиск по сериалам - tvResults, err := h.tmdbService.SearchTV(query, 1, "ru-RU", 0) - if err == nil && len(tvResults.Results) > 0 { - tv := tvResults.Results[0] - - // Получаем детальную информацию о сериале - fullTV, err := h.tmdbService.GetTVShow(tv.ID, "ru-RU") - if err == nil { - metadata := &MediaMetadata{ - ID: fullTV.ID, - Title: fullTV.Name, - Type: "tv", - Year: extractYear(fullTV.FirstAirDate), - PosterPath: fullTV.PosterPath, - BackdropPath: fullTV.BackdropPath, - Overview: fullTV.Overview, - Genres: fullTV.Genres, - } - - // Получаем информацию о сезонах и сериях - var allEpisodes []EpisodeMetadata - for _, season := range fullTV.Seasons { - if season.SeasonNumber == 0 { - continue // Пропускаем спецвыпуски - } - - seasonDetails, err := h.tmdbService.GetTVSeason(fullTV.ID, season.SeasonNumber, "ru-RU") - if err == nil { - var episodes []EpisodeMetadata - for _, episode := range seasonDetails.Episodes { - episodeData := EpisodeMetadata{ - EpisodeNumber: episode.EpisodeNumber, - SeasonNumber: season.SeasonNumber, - Name: episode.Name, - Overview: episode.Overview, - Runtime: episode.Runtime, - StillPath: episode.StillPath, - } - episodes = append(episodes, episodeData) - allEpisodes = append(allEpisodes, episodeData) - } - - metadata.Seasons = append(metadata.Seasons, SeasonMetadata{ - SeasonNumber: season.SeasonNumber, - Name: season.Name, - Episodes: episodes, - }) - } - } - - metadata.Episodes = allEpisodes - return metadata, nil - } - } - - return nil, err -} - -func extractYear(dateString string) int { - if len(dateString) >= 4 { - yearStr := dateString[:4] - if year, err := strconv.Atoi(yearStr); err == nil { - return year - } - } - return 0 -} - -// Проверяем есть ли нужные методы в TMDB сервисе -func (h *WebTorrentHandler) checkMethods() { - // Эти методы должны существовать в TMDBService: - // - SearchMovies - // - SearchTV - // - GetMovie - // - GetTVShow - // - GetTVSeason -} \ No newline at end of file diff --git a/pkg/models/movie.go b/pkg/models/movie.go index 7cd805d..0253f97 100644 --- a/pkg/models/movie.go +++ b/pkg/models/movie.go @@ -1,28 +1,45 @@ package models +// MediaInfo represents media information structure used by handlers and services +type MediaInfo struct { + ID string `json:"id"` + Title string `json:"title"` + OriginalTitle string `json:"original_title,omitempty"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + ReleaseDate string `json:"release_date,omitempty"` + FirstAirDate string `json:"first_air_date,omitempty"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` + MediaType string `json:"media_type"` + Popularity float64 `json:"popularity"` + GenreIDs []int `json:"genre_ids"` +} + type Movie struct { - ID int `json:"id"` - Title string `json:"title"` - OriginalTitle string `json:"original_title"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - ReleaseDate string `json:"release_date"` - GenreIDs []int `json:"genre_ids"` - Genres []Genre `json:"genres"` - VoteAverage float64 `json:"vote_average"` - VoteCount int `json:"vote_count"` - Popularity float64 `json:"popularity"` - Adult bool `json:"adult"` - Video bool `json:"video"` - OriginalLanguage string `json:"original_language"` - Runtime int `json:"runtime,omitempty"` - Budget int64 `json:"budget,omitempty"` - Revenue int64 `json:"revenue,omitempty"` - Status string `json:"status,omitempty"` - Tagline string `json:"tagline,omitempty"` - Homepage string `json:"homepage,omitempty"` - IMDbID string `json:"imdb_id,omitempty"` + ID int `json:"id"` + Title string `json:"title"` + OriginalTitle string `json:"original_title"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + ReleaseDate string `json:"release_date"` + GenreIDs []int `json:"genre_ids"` + Genres []Genre `json:"genres"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` + Popularity float64 `json:"popularity"` + Adult bool `json:"adult"` + Video bool `json:"video"` + OriginalLanguage string `json:"original_language"` + Runtime int `json:"runtime,omitempty"` + Budget int64 `json:"budget,omitempty"` + Revenue int64 `json:"revenue,omitempty"` + Status string `json:"status,omitempty"` + Tagline string `json:"tagline,omitempty"` + Homepage string `json:"homepage,omitempty"` + IMDbID string `json:"imdb_id,omitempty"` BelongsToCollection *Collection `json:"belongs_to_collection,omitempty"` ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"` ProductionCountries []ProductionCountry `json:"production_countries,omitempty"` @@ -30,29 +47,29 @@ type Movie struct { } type TVShow struct { - ID int `json:"id"` - Name string `json:"name"` - OriginalName string `json:"original_name"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - FirstAirDate string `json:"first_air_date"` - LastAirDate string `json:"last_air_date"` - GenreIDs []int `json:"genre_ids"` - Genres []Genre `json:"genres"` - VoteAverage float64 `json:"vote_average"` - VoteCount int `json:"vote_count"` - Popularity float64 `json:"popularity"` - OriginalLanguage string `json:"original_language"` - OriginCountry []string `json:"origin_country"` - NumberOfEpisodes int `json:"number_of_episodes,omitempty"` - NumberOfSeasons int `json:"number_of_seasons,omitempty"` - Status string `json:"status,omitempty"` - Type string `json:"type,omitempty"` - Homepage string `json:"homepage,omitempty"` - InProduction bool `json:"in_production,omitempty"` - Languages []string `json:"languages,omitempty"` - Networks []Network `json:"networks,omitempty"` + ID int `json:"id"` + Name string `json:"name"` + OriginalName string `json:"original_name"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + FirstAirDate string `json:"first_air_date"` + LastAirDate string `json:"last_air_date"` + GenreIDs []int `json:"genre_ids"` + Genres []Genre `json:"genres"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` + Popularity float64 `json:"popularity"` + OriginalLanguage string `json:"original_language"` + OriginCountry []string `json:"origin_country"` + NumberOfEpisodes int `json:"number_of_episodes,omitempty"` + NumberOfSeasons int `json:"number_of_seasons,omitempty"` + Status string `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Homepage string `json:"homepage,omitempty"` + InProduction bool `json:"in_production,omitempty"` + Languages []string `json:"languages,omitempty"` + Networks []Network `json:"networks,omitempty"` ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"` ProductionCountries []ProductionCountry `json:"production_countries,omitempty"` SpokenLanguages []SpokenLanguage `json:"spoken_languages,omitempty"` @@ -63,23 +80,23 @@ type TVShow struct { // MultiSearchResult для мультипоиска type MultiSearchResult struct { - ID int `json:"id"` - MediaType string `json:"media_type"` // "movie" или "tv" - Title string `json:"title,omitempty"` // для фильмов - Name string `json:"name,omitempty"` // для сериалов - OriginalTitle string `json:"original_title,omitempty"` - OriginalName string `json:"original_name,omitempty"` - Overview string `json:"overview"` - PosterPath string `json:"poster_path"` - BackdropPath string `json:"backdrop_path"` - ReleaseDate string `json:"release_date,omitempty"` // для фильмов - FirstAirDate string `json:"first_air_date,omitempty"` // для сериалов - GenreIDs []int `json:"genre_ids"` - VoteAverage float64 `json:"vote_average"` - VoteCount int `json:"vote_count"` - Popularity float64 `json:"popularity"` - Adult bool `json:"adult"` - OriginalLanguage string `json:"original_language"` + ID int `json:"id"` + MediaType string `json:"media_type"` // "movie" или "tv" + Title string `json:"title,omitempty"` // для фильмов + Name string `json:"name,omitempty"` // для сериалов + OriginalTitle string `json:"original_title,omitempty"` + OriginalName string `json:"original_name,omitempty"` + Overview string `json:"overview"` + PosterPath string `json:"poster_path"` + BackdropPath string `json:"backdrop_path"` + ReleaseDate string `json:"release_date,omitempty"` // для фильмов + FirstAirDate string `json:"first_air_date,omitempty"` // для сериалов + GenreIDs []int `json:"genre_ids"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` + Popularity float64 `json:"popularity"` + Adult bool `json:"adult"` + OriginalLanguage string `json:"original_language"` OriginCountry []string `json:"origin_country,omitempty"` } @@ -170,18 +187,18 @@ type SeasonDetails struct { } type Episode struct { - AirDate string `json:"air_date"` - EpisodeNumber int `json:"episode_number"` - ID int `json:"id"` - Name string `json:"name"` - Overview string `json:"overview"` - ProductionCode string `json:"production_code"` - Runtime int `json:"runtime"` - SeasonNumber int `json:"season_number"` - ShowID int `json:"show_id"` - StillPath string `json:"still_path"` - VoteAverage float64 `json:"vote_average"` - VoteCount int `json:"vote_count"` + AirDate string `json:"air_date"` + EpisodeNumber int `json:"episode_number"` + ID int `json:"id"` + Name string `json:"name"` + Overview string `json:"overview"` + ProductionCode string `json:"production_code"` + Runtime int `json:"runtime"` + SeasonNumber int `json:"season_number"` + ShowID int `json:"show_id"` + StillPath string `json:"still_path"` + VoteAverage float64 `json:"vote_average"` + VoteCount int `json:"vote_count"` } type TMDBResponse struct { @@ -199,12 +216,12 @@ type TMDBTVResponse struct { } type SearchParams struct { - Query string `json:"query"` - Page int `json:"page"` - Language string `json:"language"` - Region string `json:"region"` - Year int `json:"year"` - PrimaryReleaseYear int `json:"primary_release_year"` + Query string `json:"query"` + Page int `json:"page"` + Language string `json:"language"` + Region string `json:"region"` + Year int `json:"year"` + PrimaryReleaseYear int `json:"primary_release_year"` } type APIResponse struct { @@ -216,23 +233,23 @@ type APIResponse struct { // Модели для торрентов type TorrentResult struct { - Title string `json:"title"` - Tracker string `json:"tracker"` - Size string `json:"size"` - Seeders int `json:"seeders"` - Peers int `json:"peers"` - Leechers int `json:"leechers"` - Quality string `json:"quality"` - Voice []string `json:"voice,omitempty"` - Types []string `json:"types,omitempty"` - Seasons []int `json:"seasons,omitempty"` - Category string `json:"category"` - MagnetLink string `json:"magnet"` - TorrentLink string `json:"torrent_link,omitempty"` - Details string `json:"details,omitempty"` - PublishDate string `json:"publish_date"` - AddedDate string `json:"added_date,omitempty"` - Source string `json:"source"` + Title string `json:"title"` + Tracker string `json:"tracker"` + Size string `json:"size"` + Seeders int `json:"seeders"` + Peers int `json:"peers"` + Leechers int `json:"leechers"` + Quality string `json:"quality"` + Voice []string `json:"voice,omitempty"` + Types []string `json:"types,omitempty"` + Seasons []int `json:"seasons,omitempty"` + Category string `json:"category"` + MagnetLink string `json:"magnet"` + TorrentLink string `json:"torrent_link,omitempty"` + Details string `json:"details,omitempty"` + PublishDate string `json:"publish_date"` + AddedDate string `json:"added_date,omitempty"` + Source string `json:"source"` } type TorrentSearchResponse struct { @@ -247,16 +264,16 @@ type RedAPIResponse struct { } type RedAPITorrent struct { - Title string `json:"Title"` - Tracker string `json:"Tracker"` - Size interface{} `json:"Size"` // Может быть string или number - Seeders int `json:"Seeders"` - Peers int `json:"Peers"` - MagnetUri string `json:"MagnetUri"` - PublishDate string `json:"PublishDate"` - CategoryDesc string `json:"CategoryDesc"` - Details string `json:"Details"` - Info *RedAPITorrentInfo `json:"Info,omitempty"` + Title string `json:"Title"` + Tracker string `json:"Tracker"` + Size interface{} `json:"Size"` // Может быть string или number + Seeders int `json:"Seeders"` + Peers int `json:"Peers"` + MagnetUri string `json:"MagnetUri"` + PublishDate string `json:"PublishDate"` + CategoryDesc string `json:"CategoryDesc"` + Details string `json:"Details"` + Info *RedAPITorrentInfo `json:"Info,omitempty"` } type RedAPITorrentInfo struct { @@ -301,11 +318,11 @@ type PlayerResponse struct { // Модели для реакций type Reaction struct { - ID string `json:"id" bson:"_id,omitempty"` - UserID string `json:"userId" bson:"userId"` - MediaID string `json:"mediaId" bson:"mediaId"` - Type string `json:"type" bson:"type"` - Created string `json:"created" bson:"created"` + ID string `json:"id" bson:"_id,omitempty"` + UserID string `json:"userId" bson:"userId"` + MediaID string `json:"mediaId" bson:"mediaId"` + Type string `json:"type" bson:"type"` + Created string `json:"created" bson:"created"` } type ReactionCounts struct { @@ -314,4 +331,4 @@ type ReactionCounts struct { Think int `json:"think"` Bore int `json:"bore"` Shit int `json:"shit"` -} \ No newline at end of file +} diff --git a/pkg/services/favorites.go b/pkg/services/favorites.go index b9e67b0..04322cd 100644 --- a/pkg/services/favorites.go +++ b/pkg/services/favorites.go @@ -26,29 +26,29 @@ func NewFavoritesService(db *mongo.Database, tmdb *TMDBService) *FavoritesServic func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) error { collection := s.db.Collection("favorites") - + // Проверяем, не добавлен ли уже в избранное filter := bson.M{ "userId": userID, "mediaId": mediaID, "mediaType": mediaType, } - + var existingFavorite models.Favorite err := collection.FindOne(context.Background(), filter).Decode(&existingFavorite) if err == nil { // Уже в избранном return nil } - + var title, posterPath string - + // Получаем информацию из TMDB в зависимости от типа медиа mediaIDInt, err := strconv.Atoi(mediaID) if err != nil { return fmt.Errorf("invalid media ID: %s", mediaID) } - + if mediaType == "movie" { movie, err := s.tmdb.GetMovie(mediaIDInt, "en-US") if err != nil { @@ -66,12 +66,12 @@ func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) err } else { return fmt.Errorf("invalid media type: %s", mediaType) } - + // Формируем полный URL для постера if posterPath != "" { posterPath = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", posterPath) } - + favorite := models.Favorite{ UserID: userID, MediaID: mediaID, @@ -80,60 +80,97 @@ func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) err PosterPath: posterPath, CreatedAt: time.Now(), } - + + _, err = collection.InsertOne(context.Background(), favorite) + return err +} + +// AddToFavoritesWithInfo adds media to favorites with provided media information +func (s *FavoritesService) AddToFavoritesWithInfo(userID, mediaID, mediaType string, mediaInfo *models.MediaInfo) error { + collection := s.db.Collection("favorites") + + // Проверяем, не добавлен ли уже в избранное + filter := bson.M{ + "userId": userID, + "mediaId": mediaID, + "mediaType": mediaType, + } + + var existingFavorite models.Favorite + err := collection.FindOne(context.Background(), filter).Decode(&existingFavorite) + if err == nil { + // Уже в избранном + return nil + } + + // Формируем полный URL для постера если он есть + posterPath := mediaInfo.PosterPath + if posterPath != "" && posterPath[0] == '/' { + posterPath = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", posterPath) + } + + favorite := models.Favorite{ + UserID: userID, + MediaID: mediaID, + MediaType: mediaType, + Title: mediaInfo.Title, + PosterPath: posterPath, + CreatedAt: time.Now(), + } + _, err = collection.InsertOne(context.Background(), favorite) return err } func (s *FavoritesService) RemoveFromFavorites(userID, mediaID, mediaType string) error { collection := s.db.Collection("favorites") - + filter := bson.M{ "userId": userID, "mediaId": mediaID, "mediaType": mediaType, } - + _, err := collection.DeleteOne(context.Background(), filter) return err } func (s *FavoritesService) GetFavorites(userID string) ([]models.Favorite, error) { collection := s.db.Collection("favorites") - + filter := bson.M{ "userId": userID, } - + cursor, err := collection.Find(context.Background(), filter) if err != nil { return nil, err } defer cursor.Close(context.Background()) - + var favorites []models.Favorite err = cursor.All(context.Background(), &favorites) if err != nil { return nil, err } - + // Возвращаем пустой массив вместо nil если нет избранных if favorites == nil { favorites = []models.Favorite{} } - + return favorites, nil } func (s *FavoritesService) IsFavorite(userID, mediaID, mediaType string) (bool, error) { collection := s.db.Collection("favorites") - + filter := bson.M{ "userId": userID, "mediaId": mediaID, "mediaType": mediaType, } - + var favorite models.Favorite err := collection.FindOne(context.Background(), filter).Decode(&favorite) if err != nil { @@ -142,6 +179,6 @@ func (s *FavoritesService) IsFavorite(userID, mediaID, mediaType string) (bool, } return false, err } - + return true, nil -} \ No newline at end of file +}