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

16
main.go
View File

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

View File

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

View File

@@ -8,10 +8,10 @@ import (
"github.com/gorilla/mux"
"neomovies-api/pkg/config"
"neomovies-api/pkg/middleware"
"neomovies-api/pkg/models"
"neomovies-api/pkg/services"
"neomovies-api/pkg/config"
)
type FavoritesHandler struct {
@@ -22,26 +22,10 @@ type FavoritesHandler struct {
func NewFavoritesHandler(favoritesService *services.FavoritesService, cfg *config.Config) *FavoritesHandler {
return &FavoritesHandler{
favoritesService: favoritesService,
config: cfg,
config: cfg,
}
}
type MediaInfo struct {
ID string `json:"id"`
Title string `json:"title"`
OriginalTitle string `json:"original_title,omitempty"`
Overview string `json:"overview"`
PosterPath string `json:"poster_path"`
BackdropPath string `json:"backdrop_path"`
ReleaseDate string `json:"release_date,omitempty"`
FirstAirDate string `json:"first_air_date,omitempty"`
VoteAverage float64 `json:"vote_average"`
VoteCount int `json:"vote_count"`
MediaType string `json:"media_type"`
Popularity float64 `json:"popularity"`
GenreIDs []int `json:"genre_ids"`
}
func (h *FavoritesHandler) GetFavorites(w http.ResponseWriter, r *http.Request) {
userID, ok := middleware.GetUserIDFromContext(r.Context())
if !ok {
@@ -185,12 +169,12 @@ func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Reques
}
// fetchMediaInfoRussian получает информацию о медиа на русском языке из TMDB
func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*MediaInfo, error) {
func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*models.MediaInfo, error) {
var url string
if mediaType == "movie" {
url = fmt.Sprintf("https://api.themoviedb.org/3/movie/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAPIKey)
url = fmt.Sprintf("https://api.themoviedb.org/3/movie/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken)
} else {
url = fmt.Sprintf("https://api.themoviedb.org/3/tv/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAPIKey)
url = fmt.Sprintf("https://api.themoviedb.org/3/tv/%s?api_key=%s&language=ru-RU", mediaID, h.config.TMDBAccessToken)
}
resp, err := http.Get(url)
@@ -213,7 +197,7 @@ func (h *FavoritesHandler) fetchMediaInfoRussian(mediaID, mediaType string) (*Me
return nil, fmt.Errorf("failed to parse TMDB response: %w", err)
}
mediaInfo := &MediaInfo{
mediaInfo := &models.MediaInfo{
ID: mediaID,
MediaType: mediaType,
}

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

View File

@@ -85,6 +85,43 @@ func (s *FavoritesService) AddToFavorites(userID, mediaID, mediaType string) err
return err
}
// AddToFavoritesWithInfo adds media to favorites with provided media information
func (s *FavoritesService) AddToFavoritesWithInfo(userID, mediaID, mediaType string, mediaInfo *models.MediaInfo) error {
collection := s.db.Collection("favorites")
// Проверяем, не добавлен ли уже в избранное
filter := bson.M{
"userId": userID,
"mediaId": mediaID,
"mediaType": mediaType,
}
var existingFavorite models.Favorite
err := collection.FindOne(context.Background(), filter).Decode(&existingFavorite)
if err == nil {
// Уже в избранном
return nil
}
// Формируем полный URL для постера если он есть
posterPath := mediaInfo.PosterPath
if posterPath != "" && posterPath[0] == '/' {
posterPath = fmt.Sprintf("https://image.tmdb.org/t/p/w500%s", posterPath)
}
favorite := models.Favorite{
UserID: userID,
MediaID: mediaID,
MediaType: mediaType,
Title: mediaInfo.Title,
PosterPath: posterPath,
CreatedAt: time.Now(),
}
_, err = collection.InsertOne(context.Background(), favorite)
return err
}
func (s *FavoritesService) RemoveFromFavorites(userID, mediaID, mediaType string) error {
collection := s.db.Collection("favorites")