bug fixes

This commit is contained in:
2025-08-28 21:25:21 +03:00
parent 04583418a1
commit 59334da140
7 changed files with 364 additions and 902 deletions

View File

@@ -1,160 +1,158 @@
package handler package handler
import ( import (
"log" "log"
"net/http" "net/http"
"sync" "sync"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"neomovies-api/pkg/config" "neomovies-api/pkg/config"
"neomovies-api/pkg/database" "neomovies-api/pkg/database"
handlersPkg "neomovies-api/pkg/handlers" handlersPkg "neomovies-api/pkg/handlers"
"neomovies-api/pkg/middleware" "neomovies-api/pkg/middleware"
"neomovies-api/pkg/services" "neomovies-api/pkg/services"
) )
var ( var (
globalDB *mongo.Database globalDB *mongo.Database
globalCfg *config.Config globalCfg *config.Config
initOnce sync.Once initOnce sync.Once
initError error initError error
) )
func initializeApp() { 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 var err error
globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName) globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName)
if err != nil { if err != nil {
log.Printf("Failed to connect to database: %v", err) log.Printf("Failed to connect to database: %v", err)
initError = err initError = err
return return
} }
log.Println("Successfully connected to database") log.Println("Successfully connected to database")
} }
func Handler(w http.ResponseWriter, r *http.Request) { func Handler(w http.ResponseWriter, r *http.Request) {
initOnce.Do(initializeApp) initOnce.Do(initializeApp)
if initError != nil { if initError != nil {
log.Printf("Initialization error: %v", initError) log.Printf("Initialization error: %v", initError)
http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError) http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError)
return return
} }
tmdbService := services.NewTMDBService(globalCfg.TMDBAccessToken) tmdbService := services.NewTMDBService(globalCfg.TMDBAccessToken)
emailService := services.NewEmailService(globalCfg) emailService := services.NewEmailService(globalCfg)
authService := services.NewAuthService(globalDB, globalCfg.JWTSecret, emailService, globalCfg.BaseURL, globalCfg.GoogleClientID, globalCfg.GoogleClientSecret, globalCfg.GoogleRedirectURL, globalCfg.FrontendURL) authService := services.NewAuthService(globalDB, globalCfg.JWTSecret, emailService, globalCfg.BaseURL, globalCfg.GoogleClientID, globalCfg.GoogleClientSecret, globalCfg.GoogleRedirectURL, globalCfg.FrontendURL)
movieService := services.NewMovieService(globalDB, tmdbService) movieService := services.NewMovieService(globalDB, tmdbService)
tvService := services.NewTVService(globalDB, tmdbService) tvService := services.NewTVService(globalDB, tmdbService)
favoritesService := services.NewFavoritesService(globalDB, tmdbService) favoritesService := services.NewFavoritesService(globalDB, tmdbService)
torrentService := services.NewTorrentServiceWithConfig(globalCfg.RedAPIBaseURL, globalCfg.RedAPIKey) torrentService := services.NewTorrentServiceWithConfig(globalCfg.RedAPIBaseURL, globalCfg.RedAPIKey)
reactionsService := services.NewReactionsService(globalDB) reactionsService := services.NewReactionsService(globalDB)
authHandler := handlersPkg.NewAuthHandler(authService) authHandler := handlersPkg.NewAuthHandler(authService)
movieHandler := handlersPkg.NewMovieHandler(movieService) movieHandler := handlersPkg.NewMovieHandler(movieService)
tvHandler := handlersPkg.NewTVHandler(tvService) tvHandler := handlersPkg.NewTVHandler(tvService)
favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService) favoritesHandler := handlersPkg.NewFavoritesHandler(favoritesService, globalCfg)
docsHandler := handlersPkg.NewDocsHandler() docsHandler := handlersPkg.NewDocsHandler()
searchHandler := handlersPkg.NewSearchHandler(tmdbService) searchHandler := handlersPkg.NewSearchHandler(tmdbService)
categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService) categoriesHandler := handlersPkg.NewCategoriesHandler(tmdbService)
playersHandler := handlersPkg.NewPlayersHandler(globalCfg) playersHandler := handlersPkg.NewPlayersHandler(globalCfg)
webtorrentHandler := handlersPkg.NewWebTorrentHandler(tmdbService) torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService)
torrentsHandler := handlersPkg.NewTorrentsHandler(torrentService, tmdbService) reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService)
reactionsHandler := handlersPkg.NewReactionsHandler(reactionsService) imagesHandler := handlersPkg.NewImagesHandler()
imagesHandler := handlersPkg.NewImagesHandler()
router := mux.NewRouter() router := mux.NewRouter()
router.HandleFunc("/", docsHandler.ServeDocs).Methods("GET") router.HandleFunc("/", docsHandler.ServeDocs).Methods("GET")
router.HandleFunc("/openapi.json", docsHandler.GetOpenAPISpec).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("/health", handlersPkg.HealthCheck).Methods("GET")
api.HandleFunc("/auth/register", authHandler.Register).Methods("POST") api.HandleFunc("/auth/register", authHandler.Register).Methods("POST")
api.HandleFunc("/auth/login", authHandler.Login).Methods("POST") api.HandleFunc("/auth/login", authHandler.Login).Methods("POST")
api.HandleFunc("/auth/verify", authHandler.VerifyEmail).Methods("POST") api.HandleFunc("/auth/verify", authHandler.VerifyEmail).Methods("POST")
api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST") api.HandleFunc("/auth/resend-code", authHandler.ResendVerificationCode).Methods("POST")
api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET")
api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).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", categoriesHandler.GetCategories).Methods("GET")
api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET") api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET")
api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET") api.HandleFunc("/categories/{id}/media", categoriesHandler.GetMediaByCategory).Methods("GET")
api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).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/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET") api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).Methods("GET")
api.HandleFunc("/webtorrent/player", webtorrentHandler.OpenPlayer).Methods("GET") api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/webtorrent/metadata", webtorrentHandler.GetMetadata).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("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).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("/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/search", movieHandler.Search).Methods("GET")
api.HandleFunc("/movies/popular", movieHandler.Popular).Methods("GET") api.HandleFunc("/movies/popular", movieHandler.Popular).Methods("GET")
api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET") api.HandleFunc("/movies/top-rated", movieHandler.TopRated).Methods("GET")
api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET") api.HandleFunc("/movies/upcoming", movieHandler.Upcoming).Methods("GET")
api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET") api.HandleFunc("/movies/now-playing", movieHandler.NowPlaying).Methods("GET")
api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET") api.HandleFunc("/movies/{id}", movieHandler.GetByID).Methods("GET")
api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET") api.HandleFunc("/movies/{id}/recommendations", movieHandler.GetRecommendations).Methods("GET")
api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET") api.HandleFunc("/movies/{id}/similar", movieHandler.GetSimilar).Methods("GET")
api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET") api.HandleFunc("/movies/{id}/external-ids", movieHandler.GetExternalIDs).Methods("GET")
api.HandleFunc("/tv/search", tvHandler.Search).Methods("GET") api.HandleFunc("/tv/search", tvHandler.Search).Methods("GET")
api.HandleFunc("/tv/popular", tvHandler.Popular).Methods("GET") api.HandleFunc("/tv/popular", tvHandler.Popular).Methods("GET")
api.HandleFunc("/tv/top-rated", tvHandler.TopRated).Methods("GET") api.HandleFunc("/tv/top-rated", tvHandler.TopRated).Methods("GET")
api.HandleFunc("/tv/on-the-air", tvHandler.OnTheAir).Methods("GET") api.HandleFunc("/tv/on-the-air", tvHandler.OnTheAir).Methods("GET")
api.HandleFunc("/tv/airing-today", tvHandler.AiringToday).Methods("GET") api.HandleFunc("/tv/airing-today", tvHandler.AiringToday).Methods("GET")
api.HandleFunc("/tv/{id}", tvHandler.GetByID).Methods("GET") api.HandleFunc("/tv/{id}", tvHandler.GetByID).Methods("GET")
api.HandleFunc("/tv/{id}/recommendations", tvHandler.GetRecommendations).Methods("GET") api.HandleFunc("/tv/{id}/recommendations", tvHandler.GetRecommendations).Methods("GET")
api.HandleFunc("/tv/{id}/similar", tvHandler.GetSimilar).Methods("GET") api.HandleFunc("/tv/{id}/similar", tvHandler.GetSimilar).Methods("GET")
api.HandleFunc("/tv/{id}/external-ids", tvHandler.GetExternalIDs).Methods("GET") api.HandleFunc("/tv/{id}/external-ids", tvHandler.GetExternalIDs).Methods("GET")
protected := api.PathPrefix("").Subrouter() protected := api.PathPrefix("").Subrouter()
protected.Use(middleware.JWTAuth(globalCfg.JWTSecret)) protected.Use(middleware.JWTAuth(globalCfg.JWTSecret))
protected.HandleFunc("/favorites", favoritesHandler.GetFavorites).Methods("GET") protected.HandleFunc("/favorites", favoritesHandler.GetFavorites).Methods("GET")
protected.HandleFunc("/favorites/{id}", favoritesHandler.AddToFavorites).Methods("POST") protected.HandleFunc("/favorites/{id}", favoritesHandler.AddToFavorites).Methods("POST")
protected.HandleFunc("/favorites/{id}", favoritesHandler.RemoveFromFavorites).Methods("DELETE") protected.HandleFunc("/favorites/{id}", favoritesHandler.RemoveFromFavorites).Methods("DELETE")
protected.HandleFunc("/favorites/{id}/check", favoritesHandler.CheckIsFavorite).Methods("GET") protected.HandleFunc("/favorites/{id}/check", favoritesHandler.CheckIsFavorite).Methods("GET")
protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET") protected.HandleFunc("/auth/profile", authHandler.GetProfile).Methods("GET")
protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT") protected.HandleFunc("/auth/profile", authHandler.UpdateProfile).Methods("PUT")
protected.HandleFunc("/auth/profile", authHandler.DeleteAccount).Methods("DELETE") 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")
corsHandler := handlers.CORS( protected.HandleFunc("/reactions/{mediaType}/{mediaId}/my-reaction", reactionsHandler.GetMyReaction).Methods("GET")
handlers.AllowedOrigins([]string{"*"}), protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.SetReaction).Methods("POST")
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}), protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE")
handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}), protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET")
handlers.AllowCredentials(),
handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}),
)
corsHandler(router).ServeHTTP(w, r) 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)
}

18
main.go
View File

@@ -18,7 +18,9 @@ import (
) )
func main() { func main() {
if err := godotenv.Load(); err != nil { _ = err } if err := godotenv.Load(); err != nil {
_ = err
}
cfg := config.New() cfg := config.New()
@@ -42,12 +44,11 @@ func main() {
authHandler := appHandlers.NewAuthHandler(authService) authHandler := appHandlers.NewAuthHandler(authService)
movieHandler := appHandlers.NewMovieHandler(movieService) movieHandler := appHandlers.NewMovieHandler(movieService)
tvHandler := appHandlers.NewTVHandler(tvService) tvHandler := appHandlers.NewTVHandler(tvService)
favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService) favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService, cfg)
docsHandler := appHandlers.NewDocsHandler() docsHandler := appHandlers.NewDocsHandler()
searchHandler := appHandlers.NewSearchHandler(tmdbService) searchHandler := appHandlers.NewSearchHandler(tmdbService)
categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService) categoriesHandler := appHandlers.NewCategoriesHandler(tmdbService)
playersHandler := appHandlers.NewPlayersHandler(cfg) playersHandler := appHandlers.NewPlayersHandler(cfg)
webtorrentHandler := appHandlers.NewWebTorrentHandler(tmdbService)
torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService) torrentsHandler := appHandlers.NewTorrentsHandler(torrentService, tmdbService)
reactionsHandler := appHandlers.NewReactionsHandler(reactionsService) reactionsHandler := appHandlers.NewReactionsHandler(reactionsService)
imagesHandler := appHandlers.NewImagesHandler() imagesHandler := appHandlers.NewImagesHandler()
@@ -75,10 +76,7 @@ func main() {
api.HandleFunc("/players/alloha/{imdb_id}", playersHandler.GetAllohaPlayer).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/lumex/{imdb_id}", playersHandler.GetLumexPlayer).Methods("GET")
api.HandleFunc("/players/vibix/{imdb_id}", playersHandler.GetVibixPlayer).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/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
@@ -154,10 +152,12 @@ func main() {
} }
port := cfg.Port port := cfg.Port
if port == "" { port = "3000" } if port == "" {
port = "3000"
}
if err := http.ListenAndServe(":"+port, finalHandler); err != nil { if err := http.ListenAndServe(":"+port, finalHandler); err != nil {
fmt.Printf("❌ Server failed to start: %v\n", err) fmt.Printf("❌ Server failed to start: %v\n", err)
os.Exit(1) os.Exit(1)
} }
} }

View File

@@ -6,46 +6,50 @@ import (
) )
type Config struct { type Config struct {
MongoURI string MongoURI string
MongoDBName string MongoDBName string
TMDBAccessToken string TMDBAccessToken string
JWTSecret string JWTSecret string
Port string Port string
BaseURL string BaseURL string
NodeEnv string NodeEnv string
GmailUser string GmailUser string
GmailPassword string GmailPassword string
LumexURL string LumexURL string
AllohaToken string AllohaToken string
RedAPIBaseURL string RedAPIBaseURL string
RedAPIKey string RedAPIKey string
GoogleClientID string GoogleClientID string
GoogleClientSecret string GoogleClientSecret string
GoogleRedirectURL string GoogleRedirectURL string
FrontendURL string FrontendURL string
VibixHost string
VibixToken string
} }
func New() *Config { func New() *Config {
mongoURI := getMongoURI() mongoURI := getMongoURI()
return &Config{ return &Config{
MongoURI: mongoURI, MongoURI: mongoURI,
MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName), MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName),
TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""), TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""),
JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret), JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret),
Port: getEnv(EnvPort, DefaultPort), Port: getEnv(EnvPort, DefaultPort),
BaseURL: getEnv(EnvBaseURL, DefaultBaseURL), BaseURL: getEnv(EnvBaseURL, DefaultBaseURL),
NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv), NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv),
GmailUser: getEnv(EnvGmailUser, ""), GmailUser: getEnv(EnvGmailUser, ""),
GmailPassword: getEnv(EnvGmailPassword, ""), GmailPassword: getEnv(EnvGmailPassword, ""),
LumexURL: getEnv(EnvLumexURL, ""), LumexURL: getEnv(EnvLumexURL, ""),
AllohaToken: getEnv(EnvAllohaToken, ""), AllohaToken: getEnv(EnvAllohaToken, ""),
RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase), RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase),
RedAPIKey: getEnv(EnvRedAPIKey, ""), RedAPIKey: getEnv(EnvRedAPIKey, ""),
GoogleClientID: getEnv(EnvGoogleClientID, ""), GoogleClientID: getEnv(EnvGoogleClientID, ""),
GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""), GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""),
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""), GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
FrontendURL: getEnv(EnvFrontendURL, ""), FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
} }
} }
@@ -65,4 +69,4 @@ func getEnv(key, defaultValue string) string {
return value return value
} }
return defaultValue return defaultValue
} }

View File

@@ -8,10 +8,10 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"neomovies-api/pkg/config"
"neomovies-api/pkg/middleware" "neomovies-api/pkg/middleware"
"neomovies-api/pkg/models" "neomovies-api/pkg/models"
"neomovies-api/pkg/services" "neomovies-api/pkg/services"
"neomovies-api/pkg/config"
) )
type FavoritesHandler struct { type FavoritesHandler struct {
@@ -22,26 +22,10 @@ type FavoritesHandler struct {
func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler { func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler {
return &FavoritesHandler{ return &FavoritesHandler{
favoritesService: favoritesService, 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) { func (h *FavoritesHandler) GetFavorites(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context()) userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok { if !ok {
@@ -73,16 +57,16 @@ func (h *FavoritesHandler) AddToFavorites(w http.ResponseWriter, r *http.Request
vars := mux.Vars(r) vars := mux.Vars(r)
mediaID := vars["id"] mediaID := vars["id"]
mediaType := r.URL.Query().Get("type") mediaType := r.URL.Query().Get("type")
if mediaID == "" { if mediaID == "" {
http.Error(w, "Media ID is required", http.StatusBadRequest) http.Error(w, "Media ID is required", http.StatusBadRequest)
return return
} }
if mediaType == "" { if mediaType == "" {
mediaType = "movie" // По умолчанию фильм для обратной совместимости mediaType = "movie" // По умолчанию фильм для обратной совместимости
} }
if mediaType != "movie" && mediaType != "tv" { if mediaType != "movie" && mediaType != "tv" {
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
return return
@@ -118,16 +102,16 @@ func (h *FavoritesHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Re
vars := mux.Vars(r) vars := mux.Vars(r)
mediaID := vars["id"] mediaID := vars["id"]
mediaType := r.URL.Query().Get("type") mediaType := r.URL.Query().Get("type")
if mediaID == "" { if mediaID == "" {
http.Error(w, "Media ID is required", http.StatusBadRequest) http.Error(w, "Media ID is required", http.StatusBadRequest)
return return
} }
if mediaType == "" { if mediaType == "" {
mediaType = "movie" // По умолчанию фильм для обратной совместимости mediaType = "movie" // По умолчанию фильм для обратной совместимости
} }
if mediaType != "movie" && mediaType != "tv" { if mediaType != "movie" && mediaType != "tv" {
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
return return
@@ -156,16 +140,16 @@ func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Reques
vars := mux.Vars(r) vars := mux.Vars(r)
mediaID := vars["id"] mediaID := vars["id"]
mediaType := r.URL.Query().Get("type") mediaType := r.URL.Query().Get("type")
if mediaID == "" { if mediaID == "" {
http.Error(w, "Media ID is required", http.StatusBadRequest) http.Error(w, "Media ID is required", http.StatusBadRequest)
return return
} }
if mediaType == "" { if mediaType == "" {
mediaType = "movie" // По умолчанию фильм для обратной совместимости mediaType = "movie" // По умолчанию фильм для обратной совместимости
} }
if mediaType != "movie" && mediaType != "tv" { if mediaType != "movie" && mediaType != "tv" {
http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest)
return return
@@ -185,12 +169,12 @@ func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Reques
} }
// fetchMediaInfoRussian получает информацию о медиа на русском языке из TMDB // fetchMediaInfoRussian получает информацию о медиа на русском языке из TMDB
func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*MediaInfo, error) { func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*models.MediaInfo, error) {
var url string var url string
if mediaType == "movie" { 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 { } 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) 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) return nil, fmt.Errorf("failed to parse TMDB response: %w", err)
} }
mediaInfo := &MediaInfo{ mediaInfo := &models.MediaInfo{
ID: mediaID, ID: mediaID,
MediaType: mediaType, MediaType: mediaType,
} }
@@ -273,4 +257,4 @@ func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*Me
} }
return mediaInfo, nil return mediaInfo, nil
} }

View File

@@ -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 := `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NeoMovies WebTorrent Player</title>
<script src="https://cdn.jsdelivr.net/npm/webtorrent@latest/webtorrent.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
color: #fff;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
overflow: hidden;
}
.player-container {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 100;
}
.loading-spinner {
border: 4px solid #333;
border-top: 4px solid #fff;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.media-info {
position: absolute;
top: 20px;
left: 20px;
z-index: 50;
background: rgba(0,0,0,0.8);
padding: 15px;
border-radius: 8px;
max-width: 400px;
display: none;
}
.media-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 5px;
}
.media-overview {
font-size: 14px;
color: #ccc;
line-height: 1.4;
}
.controls {
position: absolute;
bottom: 20px;
left: 20px;
right: 20px;
z-index: 50;
background: rgba(0,0,0,0.8);
padding: 15px;
border-radius: 8px;
display: none;
}
.file-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
}
.file-item {
background: #333;
border: none;
color: #fff;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background 0.2s;
}
.file-item:hover {
background: #555;
}
.file-item.active {
background: #007bff;
}
.episode-info {
font-size: 14px;
margin-bottom: 10px;
color: #ccc;
}
video {
width: 100%;
height: 100%;
object-fit: contain;
}
.error {
color: #ff4444;
text-align: center;
padding: 20px;
}
</style>
</head>
<body>
<div class="player-container">
<div class="loading" id="loading">
<div class="loading-spinner"></div>
<div>Загружаем торрент...</div>
<div id="loadingProgress" style="margin-top: 10px; font-size: 12px;"></div>
</div>
<div class="media-info" id="mediaInfo">
<div class="media-title" id="mediaTitle"></div>
<div class="media-overview" id="mediaOverview"></div>
</div>
<div class="controls" id="controls">
<div class="episode-info" id="episodeInfo"></div>
<div class="file-list" id="fileList"></div>
</div>
<video id="videoPlayer" controls style="display: none;"></video>
</div>
<script>
const magnetLink = {{.MagnetLink}};
const client = new WebTorrent();
let currentTorrent = null;
let mediaMetadata = null;
const elements = {
loading: document.getElementById('loading'),
mediaInfo: document.getElementById('mediaInfo'),
mediaTitle: document.getElementById('mediaTitle'),
mediaOverview: document.getElementById('mediaOverview'),
controls: document.getElementById('controls'),
episodeInfo: document.getElementById('episodeInfo'),
fileList: document.getElementById('fileList'),
videoPlayer: document.getElementById('videoPlayer'),
loadingProgress: document.getElementById('loadingProgress')
};
// Загружаем торрент
client.add(magnetLink, onTorrent);
function onTorrent(torrent) {
currentTorrent = torrent;
console.log('Торрент загружен:', torrent.name);
// Получаем метаданные через API
fetchMediaMetadata(torrent.name);
// Фильтруем только видео файлы
const videoFiles = torrent.files.filter(file =>
/\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i.test(file.name)
);
if (videoFiles.length === 0) {
showError('Видео файлы не найдены в торренте');
return;
}
// Показываем список файлов
renderFileList(videoFiles);
// Автоматически выбираем первый файл
if (videoFiles.length > 0) {
playFile(videoFiles[0], 0);
}
elements.loading.style.display = 'none';
elements.controls.style.display = 'block';
}
function fetchMediaMetadata(torrentName) {
// Извлекаем название для поиска из имени торрента
const searchQuery = extractTitleFromTorrentName(torrentName);
fetch('/api/v1/webtorrent/metadata?query=' + encodeURIComponent(searchQuery))
.then(response => response.json())
.then(data => {
if (data.success && data.data) {
mediaMetadata = data.data;
displayMediaInfo(mediaMetadata);
}
})
.catch(error => console.log('Метаданные не найдены:', error));
}
function extractTitleFromTorrentName(name) {
// Убираем расширения файлов, качество, кодеки и т.д.
let title = name
.replace(/\.(mp4|avi|mkv|mov|wmv|flv|webm|m4v)$/i, '')
.replace(/\b(1080p|720p|480p|4K|BluRay|WEBRip|DVDRip|HDTV|x264|x265|HEVC|DTS|AC3)\b/gi, '')
.replace(/\b(S\d{1,2}E\d{1,2}|\d{4})\b/g, '')
.replace(/[\.\-_\[\]()]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return title;
}
function displayMediaInfo(metadata) {
elements.mediaTitle.textContent = metadata.title + (metadata.year ? ' (' + metadata.year + ')' : '');
elements.mediaOverview.textContent = metadata.overview || '';
elements.mediaInfo.style.display = 'block';
}
function renderFileList(files) {
elements.fileList.innerHTML = '';
files.forEach((file, index) => {
const button = document.createElement('button');
button.className = 'file-item';
button.textContent = getDisplayName(file.name, index);
button.onclick = () => playFile(file, index);
elements.fileList.appendChild(button);
});
}
function getDisplayName(fileName, index) {
if (!mediaMetadata) {
return fileName;
}
// Для сериалов пытаемся определить сезон и серию
if (mediaMetadata.type === 'tv') {
const episodeMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (episodeMatch) {
const season = parseInt(episodeMatch[1]);
const episode = parseInt(episodeMatch[2]);
const episodeData = mediaMetadata.episodes?.find(ep =>
ep.seasonNumber === season && ep.episodeNumber === episode
);
if (episodeData) {
return 'S' + season + 'E' + episode + ': ' + episodeData.name;
}
}
}
return mediaMetadata.title + ' - Файл ' + (index + 1);
}
function playFile(file, index) {
// Убираем активный класс со всех кнопок
document.querySelectorAll('.file-item').forEach(btn => btn.classList.remove('active'));
// Добавляем активный класс к выбранной кнопке
document.querySelectorAll('.file-item')[index].classList.add('active');
// Обновляем информацию о серии
updateEpisodeInfo(file.name, index);
// Воспроизводим файл
file.renderTo(elements.videoPlayer, (err) => {
if (err) {
showError('Ошибка воспроизведения: ' + err.message);
} else {
elements.videoPlayer.style.display = 'block';
}
});
}
function updateEpisodeInfo(fileName, index) {
if (!mediaMetadata) {
elements.episodeInfo.textContent = 'Файл: ' + fileName;
return;
}
if (mediaMetadata.type === 'tv') {
const episodeMatch = fileName.match(/S(\d{1,2})E(\d{1,2})/i);
if (episodeMatch) {
const season = parseInt(episodeMatch[1]);
const episode = parseInt(episodeMatch[2]);
const episodeData = mediaMetadata.episodes?.find(ep =>
ep.seasonNumber === season && ep.episodeNumber === episode
);
if (episodeData) {
elements.episodeInfo.innerHTML =
'<strong>Сезон ' + season + ', Серия ' + episode + '</strong><br>' +
episodeData.name +
(episodeData.overview ? '<br><span style="color: #999; font-size: 12px;">' + episodeData.overview + '</span>' : '');
return;
}
}
}
elements.episodeInfo.textContent = mediaMetadata.title + ' - Часть ' + (index + 1);
}
function showError(message) {
elements.loading.innerHTML = '<div class="error">' + message + '</div>';
}
// Обработка прогресса загрузки
client.on('torrent', (torrent) => {
torrent.on('download', () => {
const progress = Math.round(torrent.progress * 100);
const downloadSpeed = (torrent.downloadSpeed / 1024 / 1024).toFixed(1);
elements.loadingProgress.textContent =
'Прогресс: ' + progress + '% | Скорость: ' + downloadSpeed + ' MB/s';
});
});
// Глобальная обработка ошибок
client.on('error', (err) => {
showError('Ошибка торрент клиента: ' + err.message);
});
// Управление с клавиатуры
document.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
if (elements.videoPlayer.paused) {
elements.videoPlayer.play();
} else {
elements.videoPlayer.pause();
}
}
});
</script>
</body>
</html>`
// Создаем 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
}

View File

@@ -1,28 +1,45 @@
package models 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 { type Movie struct {
ID int `json:"id"` ID int `json:"id"`
Title string `json:"title"` Title string `json:"title"`
OriginalTitle string `json:"original_title"` OriginalTitle string `json:"original_title"`
Overview string `json:"overview"` Overview string `json:"overview"`
PosterPath string `json:"poster_path"` PosterPath string `json:"poster_path"`
BackdropPath string `json:"backdrop_path"` BackdropPath string `json:"backdrop_path"`
ReleaseDate string `json:"release_date"` ReleaseDate string `json:"release_date"`
GenreIDs []int `json:"genre_ids"` GenreIDs []int `json:"genre_ids"`
Genres []Genre `json:"genres"` Genres []Genre `json:"genres"`
VoteAverage float64 `json:"vote_average"` VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
Popularity float64 `json:"popularity"` Popularity float64 `json:"popularity"`
Adult bool `json:"adult"` Adult bool `json:"adult"`
Video bool `json:"video"` Video bool `json:"video"`
OriginalLanguage string `json:"original_language"` OriginalLanguage string `json:"original_language"`
Runtime int `json:"runtime,omitempty"` Runtime int `json:"runtime,omitempty"`
Budget int64 `json:"budget,omitempty"` Budget int64 `json:"budget,omitempty"`
Revenue int64 `json:"revenue,omitempty"` Revenue int64 `json:"revenue,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Tagline string `json:"tagline,omitempty"` Tagline string `json:"tagline,omitempty"`
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
IMDbID string `json:"imdb_id,omitempty"` IMDbID string `json:"imdb_id,omitempty"`
BelongsToCollection *Collection `json:"belongs_to_collection,omitempty"` BelongsToCollection *Collection `json:"belongs_to_collection,omitempty"`
ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"` ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"`
ProductionCountries []ProductionCountry `json:"production_countries,omitempty"` ProductionCountries []ProductionCountry `json:"production_countries,omitempty"`
@@ -30,29 +47,29 @@ type Movie struct {
} }
type TVShow struct { type TVShow struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
OriginalName string `json:"original_name"` OriginalName string `json:"original_name"`
Overview string `json:"overview"` Overview string `json:"overview"`
PosterPath string `json:"poster_path"` PosterPath string `json:"poster_path"`
BackdropPath string `json:"backdrop_path"` BackdropPath string `json:"backdrop_path"`
FirstAirDate string `json:"first_air_date"` FirstAirDate string `json:"first_air_date"`
LastAirDate string `json:"last_air_date"` LastAirDate string `json:"last_air_date"`
GenreIDs []int `json:"genre_ids"` GenreIDs []int `json:"genre_ids"`
Genres []Genre `json:"genres"` Genres []Genre `json:"genres"`
VoteAverage float64 `json:"vote_average"` VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
Popularity float64 `json:"popularity"` Popularity float64 `json:"popularity"`
OriginalLanguage string `json:"original_language"` OriginalLanguage string `json:"original_language"`
OriginCountry []string `json:"origin_country"` OriginCountry []string `json:"origin_country"`
NumberOfEpisodes int `json:"number_of_episodes,omitempty"` NumberOfEpisodes int `json:"number_of_episodes,omitempty"`
NumberOfSeasons int `json:"number_of_seasons,omitempty"` NumberOfSeasons int `json:"number_of_seasons,omitempty"`
Status string `json:"status,omitempty"` Status string `json:"status,omitempty"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
Homepage string `json:"homepage,omitempty"` Homepage string `json:"homepage,omitempty"`
InProduction bool `json:"in_production,omitempty"` InProduction bool `json:"in_production,omitempty"`
Languages []string `json:"languages,omitempty"` Languages []string `json:"languages,omitempty"`
Networks []Network `json:"networks,omitempty"` Networks []Network `json:"networks,omitempty"`
ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"` ProductionCompanies []ProductionCompany `json:"production_companies,omitempty"`
ProductionCountries []ProductionCountry `json:"production_countries,omitempty"` ProductionCountries []ProductionCountry `json:"production_countries,omitempty"`
SpokenLanguages []SpokenLanguage `json:"spoken_languages,omitempty"` SpokenLanguages []SpokenLanguage `json:"spoken_languages,omitempty"`
@@ -63,23 +80,23 @@ type TVShow struct {
// MultiSearchResult для мультипоиска // MultiSearchResult для мультипоиска
type MultiSearchResult struct { type MultiSearchResult struct {
ID int `json:"id"` ID int `json:"id"`
MediaType string `json:"media_type"` // "movie" или "tv" MediaType string `json:"media_type"` // "movie" или "tv"
Title string `json:"title,omitempty"` // для фильмов Title string `json:"title,omitempty"` // для фильмов
Name string `json:"name,omitempty"` // для сериалов Name string `json:"name,omitempty"` // для сериалов
OriginalTitle string `json:"original_title,omitempty"` OriginalTitle string `json:"original_title,omitempty"`
OriginalName string `json:"original_name,omitempty"` OriginalName string `json:"original_name,omitempty"`
Overview string `json:"overview"` Overview string `json:"overview"`
PosterPath string `json:"poster_path"` PosterPath string `json:"poster_path"`
BackdropPath string `json:"backdrop_path"` BackdropPath string `json:"backdrop_path"`
ReleaseDate string `json:"release_date,omitempty"` // для фильмов ReleaseDate string `json:"release_date,omitempty"` // для фильмов
FirstAirDate string `json:"first_air_date,omitempty"` // для сериалов FirstAirDate string `json:"first_air_date,omitempty"` // для сериалов
GenreIDs []int `json:"genre_ids"` GenreIDs []int `json:"genre_ids"`
VoteAverage float64 `json:"vote_average"` VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
Popularity float64 `json:"popularity"` Popularity float64 `json:"popularity"`
Adult bool `json:"adult"` Adult bool `json:"adult"`
OriginalLanguage string `json:"original_language"` OriginalLanguage string `json:"original_language"`
OriginCountry []string `json:"origin_country,omitempty"` OriginCountry []string `json:"origin_country,omitempty"`
} }
@@ -170,18 +187,18 @@ type SeasonDetails struct {
} }
type Episode struct { type Episode struct {
AirDate string `json:"air_date"` AirDate string `json:"air_date"`
EpisodeNumber int `json:"episode_number"` EpisodeNumber int `json:"episode_number"`
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Overview string `json:"overview"` Overview string `json:"overview"`
ProductionCode string `json:"production_code"` ProductionCode string `json:"production_code"`
Runtime int `json:"runtime"` Runtime int `json:"runtime"`
SeasonNumber int `json:"season_number"` SeasonNumber int `json:"season_number"`
ShowID int `json:"show_id"` ShowID int `json:"show_id"`
StillPath string `json:"still_path"` StillPath string `json:"still_path"`
VoteAverage float64 `json:"vote_average"` VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"` VoteCount int `json:"vote_count"`
} }
type TMDBResponse struct { type TMDBResponse struct {
@@ -199,12 +216,12 @@ type TMDBTVResponse struct {
} }
type SearchParams struct { type SearchParams struct {
Query string `json:"query"` Query string `json:"query"`
Page int `json:"page"` Page int `json:"page"`
Language string `json:"language"` Language string `json:"language"`
Region string `json:"region"` Region string `json:"region"`
Year int `json:"year"` Year int `json:"year"`
PrimaryReleaseYear int `json:"primary_release_year"` PrimaryReleaseYear int `json:"primary_release_year"`
} }
type APIResponse struct { type APIResponse struct {
@@ -216,23 +233,23 @@ type APIResponse struct {
// Модели для торрентов // Модели для торрентов
type TorrentResult struct { type TorrentResult struct {
Title string `json:"title"` Title string `json:"title"`
Tracker string `json:"tracker"` Tracker string `json:"tracker"`
Size string `json:"size"` Size string `json:"size"`
Seeders int `json:"seeders"` Seeders int `json:"seeders"`
Peers int `json:"peers"` Peers int `json:"peers"`
Leechers int `json:"leechers"` Leechers int `json:"leechers"`
Quality string `json:"quality"` Quality string `json:"quality"`
Voice []string `json:"voice,omitempty"` Voice []string `json:"voice,omitempty"`
Types []string `json:"types,omitempty"` Types []string `json:"types,omitempty"`
Seasons []int `json:"seasons,omitempty"` Seasons []int `json:"seasons,omitempty"`
Category string `json:"category"` Category string `json:"category"`
MagnetLink string `json:"magnet"` MagnetLink string `json:"magnet"`
TorrentLink string `json:"torrent_link,omitempty"` TorrentLink string `json:"torrent_link,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
PublishDate string `json:"publish_date"` PublishDate string `json:"publish_date"`
AddedDate string `json:"added_date,omitempty"` AddedDate string `json:"added_date,omitempty"`
Source string `json:"source"` Source string `json:"source"`
} }
type TorrentSearchResponse struct { type TorrentSearchResponse struct {
@@ -247,16 +264,16 @@ type RedAPIResponse struct {
} }
type RedAPITorrent struct { type RedAPITorrent struct {
Title string `json:"Title"` Title string `json:"Title"`
Tracker string `json:"Tracker"` Tracker string `json:"Tracker"`
Size interface{} `json:"Size"` // Может быть string или number Size interface{} `json:"Size"` // Может быть string или number
Seeders int `json:"Seeders"` Seeders int `json:"Seeders"`
Peers int `json:"Peers"` Peers int `json:"Peers"`
MagnetUri string `json:"MagnetUri"` MagnetUri string `json:"MagnetUri"`
PublishDate string `json:"PublishDate"` PublishDate string `json:"PublishDate"`
CategoryDesc string `json:"CategoryDesc"` CategoryDesc string `json:"CategoryDesc"`
Details string `json:"Details"` Details string `json:"Details"`
Info *RedAPITorrentInfo `json:"Info,omitempty"` Info *RedAPITorrentInfo `json:"Info,omitempty"`
} }
type RedAPITorrentInfo struct { type RedAPITorrentInfo struct {
@@ -301,11 +318,11 @@ type PlayerResponse struct {
// Модели для реакций // Модели для реакций
type Reaction struct { type Reaction struct {
ID string `json:"id" bson:"_id,omitempty"` ID string `json:"id" bson:"_id,omitempty"`
UserID string `json:"userId" bson:"userId"` UserID string `json:"userId" bson:"userId"`
MediaID string `json:"mediaId" bson:"mediaId"` MediaID string `json:"mediaId" bson:"mediaId"`
Type string `json:"type" bson:"type"` Type string `json:"type" bson:"type"`
Created string `json:"created" bson:"created"` Created string `json:"created" bson:"created"`
} }
type ReactionCounts struct { type ReactionCounts struct {
@@ -314,4 +331,4 @@ type ReactionCounts struct {
Think int `json:"think"` Think int `json:"think"`
Bore int `json:"bore"` Bore int `json:"bore"`
Shit int `json:"shit"` Shit int `json:"shit"`
} }

View File

@@ -26,29 +26,29 @@ func NewFavoritesService(db *mongo.Database, tmdb *TMDBService) *FavoritesServic
func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) error { func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) error {
collection := s.db.Collection("favorites") collection := s.db.Collection("favorites")
// Проверяем, не добавлен ли уже в избранное // Проверяем, не добавлен ли уже в избранное
filter := bson.M{ filter := bson.M{
"userId": userID, "userId": userID,
"mediaId": mediaID, "mediaId": mediaID,
"mediaType": mediaType, "mediaType": mediaType,
} }
var existingFavorite models.Favorite var existingFavorite models.Favorite
err := collection.FindOne(context.Background(), filter).Decode(&existingFavorite) err := collection.FindOne(context.Background(), filter).Decode(&existingFavorite)
if err == nil { if err == nil {
// Уже в избранном // Уже в избранном
return nil return nil
} }
var title, posterPath string var title, posterPath string
// Получаем информацию из TMDB в зависимости от типа медиа // Получаем информацию из TMDB в зависимости от типа медиа
mediaIDInt, err := strconv.Atoi(mediaID) mediaIDInt, err := strconv.Atoi(mediaID)
if err != nil { if err != nil {
return fmt.Errorf("invalid media ID: %s", mediaID) return fmt.Errorf("invalid media ID: %s", mediaID)
} }
if mediaType == "movie" { if mediaType == "movie" {
movie, err := s.tmdb.GetMovie(mediaIDInt, "en-US") movie, err := s.tmdb.GetMovie(mediaIDInt, "en-US")
if err != nil { if err != nil {
@@ -66,12 +66,12 @@ func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) err
} else { } else {
return fmt.Errorf("invalid media type: %s", mediaType) return fmt.Errorf("invalid media type: %s", mediaType)
} }
// Формируем полный URL для постера // Формируем полный URL для постера
if posterPath != "" { if posterPath != "" {
posterPath = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", posterPath) posterPath = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", posterPath)
} }
favorite := models.Favorite{ favorite := models.Favorite{
UserID: userID, UserID: userID,
MediaID: mediaID, MediaID: mediaID,
@@ -80,60 +80,97 @@ func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) err
PosterPath: posterPath, PosterPath: posterPath,
CreatedAt: time.Now(), 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) _, err = collection.InsertOne(context.Background(), favorite)
return err return err
} }
func (s *FavoritesService) RemoveFromFavorites(userID, mediaID, mediaType string) error { func (s *FavoritesService) RemoveFromFavorites(userID, mediaID, mediaType string) error {
collection := s.db.Collection("favorites") collection := s.db.Collection("favorites")
filter := bson.M{ filter := bson.M{
"userId": userID, "userId": userID,
"mediaId": mediaID, "mediaId": mediaID,
"mediaType": mediaType, "mediaType": mediaType,
} }
_, err := collection.DeleteOne(context.Background(), filter) _, err := collection.DeleteOne(context.Background(), filter)
return err return err
} }
func (s *FavoritesService) GetFavorites(userID string) ([]models.Favorite, error) { func (s *FavoritesService) GetFavorites(userID string) ([]models.Favorite, error) {
collection := s.db.Collection("favorites") collection := s.db.Collection("favorites")
filter := bson.M{ filter := bson.M{
"userId": userID, "userId": userID,
} }
cursor, err := collection.Find(context.Background(), filter) cursor, err := collection.Find(context.Background(), filter)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer cursor.Close(context.Background()) defer cursor.Close(context.Background())
var favorites []models.Favorite var favorites []models.Favorite
err = cursor.All(context.Background(), &favorites) err = cursor.All(context.Background(), &favorites)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Возвращаем пустой массив вместо nil если нет избранных // Возвращаем пустой массив вместо nil если нет избранных
if favorites == nil { if favorites == nil {
favorites = []models.Favorite{} favorites = []models.Favorite{}
} }
return favorites, nil return favorites, nil
} }
func (s *FavoritesService) IsFavorite(userID, mediaID, mediaType string) (bool, error) { func (s *FavoritesService) IsFavorite(userID, mediaID, mediaType string) (bool, error) {
collection := s.db.Collection("favorites") collection := s.db.Collection("favorites")
filter := bson.M{ filter := bson.M{
"userId": userID, "userId": userID,
"mediaId": mediaID, "mediaId": mediaID,
"mediaType": mediaType, "mediaType": mediaType,
} }
var favorite models.Favorite var favorite models.Favorite
err := collection.FindOne(context.Background(), filter).Decode(&favorite) err := collection.FindOne(context.Background(), filter).Decode(&favorite)
if err != nil { if err != nil {
@@ -142,6 +179,6 @@ func (s *FavoritesService) IsFavorite(userID, mediaID, mediaType string) (bool,
} }
return false, err return false, err
} }
return true, nil return true, nil
} }