mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-28 01:48:51 +05:00
bug fixes
This commit is contained in:
242
api/index.go
242
api/index.go
@@ -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
18
main.go
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user