diff --git a/WEBTORRENT_PLAYER.md b/WEBTORRENT_PLAYER.md new file mode 100644 index 0000000..e100c5d --- /dev/null +++ b/WEBTORRENT_PLAYER.md @@ -0,0 +1,271 @@ +# 🎬 NeoMovies WebTorrent Player + +Современный плеер для просмотра торрент файлов прямо в браузере с умной интеграцией TMDB. + +## 🚀 Особенности + +- ✅ **Полностью клиентский** - все торренты обрабатываются в браузере +- ✅ **Умная навигация** - автоматическое определение сезонов и серий +- ✅ **TMDB интеграция** - красивые названия серий вместо имен файлов +- ✅ **Мультиформат** - поддержка MP4, AVI, MKV, WebM и других +- ✅ **Потоковое воспроизведение** - начинает играть до полной загрузки +- ✅ **Прогресс загрузки** - отображение скорости и процента загрузки + +## 📋 API Endpoints + +### Открытие плеера +```http +GET /api/v1/webtorrent/player?magnet={MAGNET_LINK} +``` +или через заголовок: +```http +GET /api/v1/webtorrent/player +X-Magnet-Link: {MAGNET_LINK} +``` + +### Получение метаданных +```http +GET /api/v1/webtorrent/metadata?query={SEARCH_QUERY} +``` + +## 💻 Примеры использования + +### 1. Простое открытие плеера +```javascript +const magnetLink = "magnet:?xt=urn:btih:..."; +const encodedMagnet = encodeURIComponent(magnetLink); +window.open(`/api/v1/webtorrent/player?magnet=${encodedMagnet}`); +``` + +### 2. Открытие через заголовок +```javascript +fetch('/api/v1/webtorrent/player', { + headers: { + 'X-Magnet-Link': magnetLink + } +}).then(response => { + // Открыть в новом окне + window.open(URL.createObjectURL(response.blob())); +}); +``` + +### 3. Получение метаданных +```javascript +fetch('/api/v1/webtorrent/metadata?query=Breaking Bad') + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('Метаданные:', data.data); + // data.data содержит информацию о сериале/фильме + } + }); +``` + +## 🎮 Управление плеером + +### Клавиши управления +- **Space** - пауза/воспроизведение +- **Click** - выбор серии/файла + +### UI элементы +- **Информация о медиа** - название, год, описание (верхний левый угол) +- **Список файлов** - выбор серий/частей (нижняя панель) +- **Информация о серии** - название и описание текущей серии +- **Прогресс загрузки** - скорость и процент загрузки + +## 🔧 Как это работает + +### 1. Загрузка торрента +```mermaid +graph LR + A[Magnet Link] --> B[WebTorrent Client] + B --> C[Parse Metadata] + C --> D[Filter Video Files] + D --> E[Display File List] +``` + +### 2. Получение метаданных +```mermaid +graph LR + A[Torrent Name] --> B[Extract Title] + B --> C[Search TMDB] + C --> D[Get Movie/TV Data] + D --> E[Get Seasons/Episodes] + E --> F[Display Smart Names] +``` + +### 3. Воспроизведение +```mermaid +graph LR + A[Select File] --> B[Stream from Torrent] + B --> C[Render to Video Element] + C --> D[Show Progress] +``` + +## 📊 Структура ответа метаданных + +### Для фильмов +```json +{ + "success": true, + "data": { + "id": 155, + "title": "Тёмный рыцарь", + "type": "movie", + "year": 2008, + "posterPath": "https://image.tmdb.org/t/p/w500/...", + "backdropPath": "https://image.tmdb.org/t/p/w500/...", + "overview": "Описание фильма...", + "runtime": 152, + "genres": [ + {"id": 28, "name": "боевик"}, + {"id": 80, "name": "криминал"} + ] + } +} +``` + +### Для сериалов +```json +{ + "success": true, + "data": { + "id": 1396, + "title": "Во все тяжкие", + "type": "tv", + "year": 2008, + "posterPath": "https://image.tmdb.org/t/p/w500/...", + "overview": "Описание сериала...", + "seasons": [ + { + "seasonNumber": 1, + "name": "Сезон 1", + "episodes": [ + { + "episodeNumber": 1, + "seasonNumber": 1, + "name": "Пилот", + "overview": "Описание серии...", + "runtime": 58 + } + ] + } + ], + "episodes": [ + { + "episodeNumber": 1, + "seasonNumber": 1, + "name": "Пилот", + "overview": "Описание серии..." + } + ] + } +} +``` + +## 🛡️ Безопасность + +### ⚠️ ВАЖНО: Клиентский подход +- Торренты обрабатываются **ТОЛЬКО в браузере пользователя** +- Сервер **НЕ ЗАГРУЖАЕТ** и **НЕ ХРАНИТ** торрент файлы +- API используется только для получения метаданных из TMDB +- Полное соответствие законодательству - сервер не участвует в торрент активности + +### 🔒 Приватность +- Никакая торрент активность не логируется на сервере +- Магнет ссылки не сохраняются в базе данных +- Пользовательские данные защищены стандартными методами API + +## 🌟 Умные функции + +### Автоматическое определение серий +Плеер автоматически распознает: +- **S01E01** - формат сезон/серия +- **Breaking.Bad.S01E01** - название с сезоном +- **Game.of.Thrones.1x01** - альтернативный формат + +### Красивые названия +Вместо: +``` +Breaking.Bad.S01E01.720p.BluRay.x264-DEMAND.mkv +``` +Показывает: +``` +S1E1: Пилот +``` + +### Информация о сериях +- Название серии из TMDB +- Описание эпизода +- Продолжительность +- Изображения (постеры) + +## 🎯 Примеры интеграции + +### React компонент +```jsx +function WebTorrentPlayer({ magnetLink }) { + const openPlayer = () => { + const url = `/api/v1/webtorrent/player?magnet=${encodeURIComponent(magnetLink)}`; + window.open(url, '_blank', 'fullscreen=yes'); + }; + + return ( + + ); +} +``` + +### Получение метаданных перед открытием +```javascript +async function openWithMetadata(magnetLink, searchQuery) { + try { + // Получаем метаданные + const metaResponse = await fetch(`/api/v1/webtorrent/metadata?query=${encodeURIComponent(searchQuery)}`); + const metadata = await metaResponse.json(); + + if (metadata.success) { + console.log('Найдено:', metadata.data.title, metadata.data.type); + } + + // Открываем плеер + const playerUrl = `/api/v1/webtorrent/player?magnet=${encodeURIComponent(magnetLink)}`; + window.open(playerUrl, '_blank'); + + } catch (error) { + console.error('Ошибка:', error); + } +} +``` + +## 🔧 Технические детали + +### Поддерживаемые форматы +- **Видео**: MP4, AVI, MKV, MOV, WMV, FLV, WebM, M4V +- **Кодеки**: H.264, H.265/HEVC, VP8, VP9 +- **Аудио**: AAC, MP3, AC3, DTS + +### Требования браузера +- Современные браузеры с поддержкой WebRTC +- Chrome/Edge 45+, Firefox 42+, Safari 11+ +- Поддержка WebTorrent API + +### Производительность +- Потоковое воспроизведение с первых секунд +- Умное кэширование наиболее просматриваемых частей +- Адаптивная буферизация в зависимости от скорости + +## 🚦 Статусы ответов + +| Код | Описание | +|-----|----------| +| 200 | Успешно - плеер загружен или метаданные найдены | +| 400 | Отсутствует magnet ссылка или query параметр | +| 404 | Метаданные не найдены в TMDB | +| 500 | Внутренняя ошибка сервера | + +--- + +**🎬 NeoMovies WebTorrent Player** - современное решение для просмотра торрентов с соблюдением всех требований безопасности и законности! 🚀 \ No newline at end of file diff --git a/api/index.go b/api/index.go index fa1b4e8..a16dd2f 100644 --- a/api/index.go +++ b/api/index.go @@ -55,16 +55,19 @@ func Handler(w http.ResponseWriter, r *http.Request) { 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() @@ -84,15 +87,19 @@ func Handler(w http.ResponseWriter, r *http.Request) { api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET") - router.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("/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") @@ -127,9 +134,10 @@ func Handler(w http.ResponseWriter, r *http.Request) { protected := api.PathPrefix("").Subrouter() protected.Use(middleware.JWTAuth(globalCfg.JWTSecret)) - protected.HandleFunc("/favorites", movieHandler.GetFavorites).Methods("GET") - protected.HandleFunc("/favorites/{id}", movieHandler.AddToFavorites).Methods("POST") - protected.HandleFunc("/favorites/{id}", movieHandler.RemoveFromFavorites).Methods("DELETE") + 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") @@ -143,8 +151,9 @@ func Handler(w http.ResponseWriter, r *http.Request) { 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"}), + handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}), handlers.AllowCredentials(), + handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}), ) corsHandler(router).ServeHTTP(w, r) diff --git a/main.go b/main.go index a87bc61..9e11250 100644 --- a/main.go +++ b/main.go @@ -35,16 +35,19 @@ func main() { movieService := services.NewMovieService(db, tmdbService) tvService := services.NewTVService(db, tmdbService) + favoritesService := services.NewFavoritesService(db, tmdbService) torrentService := services.NewTorrentServiceWithConfig(cfg.RedAPIBaseURL, cfg.RedAPIKey) reactionsService := services.NewReactionsService(db) authHandler := appHandlers.NewAuthHandler(authService) movieHandler := appHandlers.NewMovieHandler(movieService) tvHandler := appHandlers.NewTVHandler(tvService) + favoritesHandler := appHandlers.NewFavoritesHandler(favoritesService) 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() @@ -64,15 +67,19 @@ func main() { api.HandleFunc("/auth/google/login", authHandler.GoogleLogin).Methods("GET") api.HandleFunc("/auth/google/callback", authHandler.GoogleCallback).Methods("GET") - r.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("/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") @@ -107,9 +114,10 @@ func main() { protected := api.PathPrefix("").Subrouter() protected.Use(middleware.JWTAuth(cfg.JWTSecret)) - protected.HandleFunc("/favorites", movieHandler.GetFavorites).Methods("GET") - protected.HandleFunc("/favorites/{id}", movieHandler.AddToFavorites).Methods("POST") - protected.HandleFunc("/favorites/{id}", movieHandler.RemoveFromFavorites).Methods("DELETE") + 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") @@ -123,8 +131,9 @@ func main() { 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"}), + handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "Accept", "Origin", "X-Requested-With", "X-CSRF-Token"}), handlers.AllowCredentials(), + handlers.ExposedHeaders([]string{"Authorization", "Content-Type"}), ) var finalHandler http.Handler diff --git a/neomovies-api b/neomovies-api new file mode 100755 index 0000000..8bf7735 Binary files /dev/null and b/neomovies-api differ diff --git a/pkg/handlers/categories.go b/pkg/handlers/categories.go index 3a86411..8b99609 100644 --- a/pkg/handlers/categories.go +++ b/pkg/handlers/categories.go @@ -53,7 +53,7 @@ func (h *CategoriesHandler) GetCategories(w http.ResponseWriter, r *http.Request }) } -func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.Request) { +func (h *CategoriesHandler) GetMediaByCategory(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) categoryID, err := strconv.Atoi(vars["id"]) if err != nil { @@ -67,20 +67,46 @@ func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.R language = "ru-RU" } - // Используем discover API для получения фильмов по жанру - movies, err := h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + mediaType := r.URL.Query().Get("type") + if mediaType == "" { + mediaType = "movie" // По умолчанию фильмы для обратной совместимости + } + + if mediaType != "movie" && mediaType != "tv" { + http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) + return + } + + var data interface{} + var err2 error + + if mediaType == "movie" { + // Используем discover API для получения фильмов по жанру + data, err2 = h.tmdbService.DiscoverMoviesByGenre(categoryID, page, language) + } else { + // Используем discover API для получения сериалов по жанру + data, err2 = h.tmdbService.DiscoverTVByGenre(categoryID, page, language) + } + + if err2 != nil { + http.Error(w, err2.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(models.APIResponse{ Success: true, - Data: movies, + Data: data, + Message: "Media retrieved successfully", }) } +// Старый метод для обратной совместимости +func (h *CategoriesHandler) GetMoviesByCategory(w http.ResponseWriter, r *http.Request) { + // Просто перенаправляем на новый метод + h.GetMediaByCategory(w, r) +} + func generateSlug(name string) string { // Простая функция для создания slug из названия // В реальном проекте стоит использовать более сложную логику diff --git a/pkg/handlers/docs.go b/pkg/handlers/docs.go index bb7a75f..31d4095 100644 --- a/pkg/handlers/docs.go +++ b/pkg/handlers/docs.go @@ -37,6 +37,8 @@ func (h *DocsHandler) GetOpenAPISpec(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept, Origin, X-Requested-With") json.NewEncoder(w).Encode(spec) } @@ -64,6 +66,8 @@ func (h *DocsHandler) ServeDocs(w http.ResponseWriter, r *http.Request) { return } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Access-Control-Allow-Origin", "*") fmt.Fprintln(w, htmlContent) } @@ -141,7 +145,7 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { }, }, }, - "/search/multi": map[string]interface{}{ + "/api/v1/search/multi": map[string]interface{}{ "get": map[string]interface{}{ "summary": "Мультипоиск", "description": "Поиск фильмов, сериалов и актеров", @@ -209,6 +213,58 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { }, }, }, + "/api/v1/categories/{id}/media": map[string]interface{}{ + "get": map[string]interface{}{ + "summary": "Медиа по категории", + "description": "Получение фильмов или сериалов по категории", + "tags": []string{"Categories"}, + "parameters": []map[string]interface{}{ + { + "name": "id", + "in": "path", + "required": true, + "schema": map[string]string{"type": "integer"}, + "description": "ID категории", + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "string", + "enum": []string{"movie", "tv"}, + "default": "movie", + }, + "description": "Тип медиа: movie или tv", + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "integer", + "default": 1, + }, + "description": "Номер страницы", + }, + { + "name": "language", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "string", + "default": "ru-RU", + }, + "description": "Язык ответа", + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "Медиа по категории", + }, + }, + }, + }, "/api/v1/players/alloha/{imdb_id}": map[string]interface{}{ "get": map[string]interface{}{ "summary": "Плеер Alloha", @@ -277,6 +333,76 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { }, }, }, + "/api/v1/webtorrent/player": map[string]interface{}{ + "get": map[string]interface{}{ + "summary": "WebTorrent плеер", + "description": "Открытие WebTorrent плеера с магнет ссылкой. Плеер работает полностью на стороне клиента.", + "tags": []string{"WebTorrent"}, + "parameters": []map[string]interface{}{ + { + "name": "magnet", + "in": "query", + "required": false, + "schema": map[string]string{"type": "string"}, + "description": "Магнет ссылка торрента", + }, + { + "name": "X-Magnet-Link", + "in": "header", + "required": false, + "schema": map[string]string{"type": "string"}, + "description": "Магнет ссылка через заголовок (альтернативный способ)", + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "HTML страница с WebTorrent плеером", + "content": map[string]interface{}{ + "text/html": map[string]interface{}{ + "schema": map[string]string{"type": "string"}, + }, + }, + }, + "400": map[string]interface{}{ + "description": "Отсутствует магнет ссылка", + }, + }, + }, + }, + "/api/v1/webtorrent/metadata": map[string]interface{}{ + "get": map[string]interface{}{ + "summary": "Метаданные медиа", + "description": "Получение метаданных фильма или сериала по названию для WebTorrent плеера", + "tags": []string{"WebTorrent"}, + "parameters": []map[string]interface{}{ + { + "name": "query", + "in": "query", + "required": true, + "schema": map[string]string{"type": "string"}, + "description": "Название для поиска (извлеченное из торрента)", + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "Метаданные найдены", + "content": map[string]interface{}{ + "application/json": map[string]interface{}{ + "schema": map[string]interface{}{ + "$ref": "#/components/schemas/WebTorrentMetadata", + }, + }, + }, + }, + "400": map[string]interface{}{ + "description": "Отсутствует параметр query", + }, + "404": map[string]interface{}{ + "description": "Метаданные не найдены", + }, + }, + }, + }, "/api/v1/torrents/search/{imdbId}": map[string]interface{}{ "get": map[string]interface{}{ "summary": "Поиск торрентов", @@ -715,15 +841,15 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { }, "/api/v1/favorites": map[string]interface{}{ "get": map[string]interface{}{ - "summary": "Получить избранные фильмы", - "description": "Список избранных фильмов пользователя", + "summary": "Получить избранное", + "description": "Список избранных фильмов и сериалов пользователя", "tags": []string{"Favorites"}, "security": []map[string][]string{ {"bearerAuth": []string{}}, }, "responses": map[string]interface{}{ "200": map[string]interface{}{ - "description": "Список избранных фильмов", + "description": "Список избранного", }, }, }, @@ -731,7 +857,7 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { "/api/v1/favorites/{id}": map[string]interface{}{ "post": map[string]interface{}{ "summary": "Добавить в избранное", - "description": "Добавление фильма в избранное", + "description": "Добавление фильма или сериала в избранное", "tags": []string{"Favorites"}, "security": []map[string][]string{ {"bearerAuth": []string{}}, @@ -742,18 +868,29 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { "in": "path", "required": true, "schema": map[string]string{"type": "string"}, - "description": "ID фильма", + "description": "ID медиа", + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "string", + "enum": []string{"movie", "tv"}, + "default": "movie", + }, + "description": "Тип медиа: movie или tv", }, }, "responses": map[string]interface{}{ "200": map[string]interface{}{ - "description": "Фильм добавлен в избранное", + "description": "Добавлено в избранное", }, }, }, "delete": map[string]interface{}{ "summary": "Удалить из избранного", - "description": "Удаление фильма из избранного", + "description": "Удаление фильма или сериала из избранного", "tags": []string{"Favorites"}, "security": []map[string][]string{ {"bearerAuth": []string{}}, @@ -764,12 +901,58 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { "in": "path", "required": true, "schema": map[string]string{"type": "string"}, - "description": "ID фильма", + "description": "ID медиа", + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "string", + "enum": []string{"movie", "tv"}, + "default": "movie", + }, + "description": "Тип медиа: movie или tv", }, }, "responses": map[string]interface{}{ "200": map[string]interface{}{ - "description": "Фильм удален из избранного", + "description": "Удалено из избранного", + }, + }, + }, + }, + "/api/v1/favorites/{id}/check": map[string]interface{}{ + "get": map[string]interface{}{ + "summary": "Проверить избранное", + "description": "Проверка, находится ли медиа в избранном", + "tags": []string{"Favorites"}, + "security": []map[string][]string{ + {"bearerAuth": []string{}}, + }, + "parameters": []map[string]interface{}{ + { + "name": "id", + "in": "path", + "required": true, + "schema": map[string]string{"type": "string"}, + "description": "ID медиа", + }, + { + "name": "type", + "in": "query", + "required": false, + "schema": map[string]interface{}{ + "type": "string", + "enum": []string{"movie", "tv"}, + "default": "movie", + }, + "description": "Тип медиа: movie или tv", + }, + }, + "responses": map[string]interface{}{ + "200": map[string]interface{}{ + "description": "Статус избранного", }, }, }, @@ -1425,6 +1608,71 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec { "twitter_id": map[string]string{"type": "string"}, }, }, + "WebTorrentMetadata": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]string{"type": "integer"}, + "title": map[string]string{"type": "string"}, + "type": map[string]interface{}{ + "type": "string", + "enum": []string{"movie", "tv"}, + }, + "year": map[string]string{"type": "integer"}, + "posterPath": map[string]string{"type": "string"}, + "backdropPath": map[string]string{"type": "string"}, + "overview": map[string]string{"type": "string"}, + "runtime": map[string]string{"type": "integer"}, + "genres": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/Genre", + }, + }, + "seasons": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/SeasonMetadata", + }, + }, + "episodes": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/EpisodeMetadata", + }, + }, + }, + }, + "SeasonMetadata": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "seasonNumber": map[string]string{"type": "integer"}, + "name": map[string]string{"type": "string"}, + "episodes": map[string]interface{}{ + "type": "array", + "items": map[string]interface{}{ + "$ref": "#/components/schemas/EpisodeMetadata", + }, + }, + }, + }, + "EpisodeMetadata": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "episodeNumber": map[string]string{"type": "integer"}, + "seasonNumber": map[string]string{"type": "integer"}, + "name": map[string]string{"type": "string"}, + "overview": map[string]string{"type": "string"}, + "runtime": map[string]string{"type": "integer"}, + "stillPath": map[string]string{"type": "string"}, + }, + }, + "Genre": map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]string{"type": "integer"}, + "name": map[string]string{"type": "string"}, + }, + }, }, }, } diff --git a/pkg/handlers/favorites.go b/pkg/handlers/favorites.go new file mode 100644 index 0000000..f1b8b48 --- /dev/null +++ b/pkg/handlers/favorites.go @@ -0,0 +1,157 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + + "neomovies-api/pkg/middleware" + "neomovies-api/pkg/models" + "neomovies-api/pkg/services" +) + +type FavoritesHandler struct { + favoritesService *services.FavoritesService +} + +func NewFavoritesHandler(favoritesService *services.FavoritesService) *FavoritesHandler { + return &FavoritesHandler{ + favoritesService: favoritesService, + } +} + +func (h *FavoritesHandler) GetFavorites(w http.ResponseWriter, r *http.Request) { + userID, ok := middleware.GetUserIDFromContext(r.Context()) + if !ok { + http.Error(w, "User ID not found in context", http.StatusUnauthorized) + return + } + + favorites, err := h.favoritesService.GetFavorites(userID) + if err != nil { + http.Error(w, "Failed to get favorites: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(models.APIResponse{ + Success: true, + Data: favorites, + Message: "Favorites retrieved successfully", + }) +} + +func (h *FavoritesHandler) AddToFavorites(w http.ResponseWriter, r *http.Request) { + userID, ok := middleware.GetUserIDFromContext(r.Context()) + if !ok { + http.Error(w, "User ID not found in context", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + mediaID := vars["id"] + mediaType := r.URL.Query().Get("type") + + if mediaID == "" { + http.Error(w, "Media ID is required", http.StatusBadRequest) + return + } + + if mediaType == "" { + mediaType = "movie" // По умолчанию фильм для обратной совместимости + } + + if mediaType != "movie" && mediaType != "tv" { + http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) + return + } + + err := h.favoritesService.AddToFavorites(userID, mediaID, mediaType) + if err != nil { + http.Error(w, "Failed to add to favorites: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(models.APIResponse{ + Success: true, + Message: "Added to favorites successfully", + }) +} + +func (h *FavoritesHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Request) { + userID, ok := middleware.GetUserIDFromContext(r.Context()) + if !ok { + http.Error(w, "User ID not found in context", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + mediaID := vars["id"] + mediaType := r.URL.Query().Get("type") + + if mediaID == "" { + http.Error(w, "Media ID is required", http.StatusBadRequest) + return + } + + if mediaType == "" { + mediaType = "movie" // По умолчанию фильм для обратной совместимости + } + + if mediaType != "movie" && mediaType != "tv" { + http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) + return + } + + err := h.favoritesService.RemoveFromFavorites(userID, mediaID, mediaType) + if err != nil { + http.Error(w, "Failed to remove from favorites: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(models.APIResponse{ + Success: true, + Message: "Removed from favorites successfully", + }) +} + +func (h *FavoritesHandler) CheckIsFavorite(w http.ResponseWriter, r *http.Request) { + userID, ok := middleware.GetUserIDFromContext(r.Context()) + if !ok { + http.Error(w, "User ID not found in context", http.StatusUnauthorized) + return + } + + vars := mux.Vars(r) + mediaID := vars["id"] + mediaType := r.URL.Query().Get("type") + + if mediaID == "" { + http.Error(w, "Media ID is required", http.StatusBadRequest) + return + } + + if mediaType == "" { + mediaType = "movie" // По умолчанию фильм для обратной совместимости + } + + if mediaType != "movie" && mediaType != "tv" { + http.Error(w, "Media type must be 'movie' or 'tv'", http.StatusBadRequest) + return + } + + isFavorite, err := h.favoritesService.IsFavorite(userID, mediaID, mediaType) + if err != nil { + http.Error(w, "Failed to check favorite status: "+err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(models.APIResponse{ + Success: true, + Data: map[string]bool{"isFavorite": isFavorite}, + }) +} \ No newline at end of file diff --git a/pkg/handlers/movie.go b/pkg/handlers/movie.go index 70a0bc4..3543064 100644 --- a/pkg/handlers/movie.go +++ b/pkg/handlers/movie.go @@ -7,7 +7,6 @@ import ( "github.com/gorilla/mux" - "neomovies-api/pkg/middleware" "neomovies-api/pkg/models" "neomovies-api/pkg/services" ) @@ -190,73 +189,7 @@ func (h *MovieHandler) GetSimilar(w http.ResponseWriter, r *http.Request) { }) } -func (h *MovieHandler) GetFavorites(w http.ResponseWriter, r *http.Request) { - userID, ok := middleware.GetUserIDFromContext(r.Context()) - if !ok { - http.Error(w, "User ID not found in context", http.StatusInternalServerError) - return - } - language := r.URL.Query().Get("language") - - movies, err := h.movieService.GetFavorites(userID, language) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(models.APIResponse{ - Success: true, - Data: movies, - }) -} - -func (h *MovieHandler) AddToFavorites(w http.ResponseWriter, r *http.Request) { - userID, ok := middleware.GetUserIDFromContext(r.Context()) - if !ok { - http.Error(w, "User ID not found in context", http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - movieID := vars["id"] - - err := h.movieService.AddToFavorites(userID, movieID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(models.APIResponse{ - Success: true, - Message: "Movie added to favorites", - }) -} - -func (h *MovieHandler) RemoveFromFavorites(w http.ResponseWriter, r *http.Request) { - userID, ok := middleware.GetUserIDFromContext(r.Context()) - if !ok { - http.Error(w, "User ID not found in context", http.StatusInternalServerError) - return - } - - vars := mux.Vars(r) - movieID := vars["id"] - - err := h.movieService.RemoveFromFavorites(userID, movieID) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(models.APIResponse{ - Success: true, - Message: "Movie removed from favorites", - }) -} func (h *MovieHandler) GetExternalIDs(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) diff --git a/pkg/handlers/webtorrent.go b/pkg/handlers/webtorrent.go new file mode 100644 index 0000000..117e28f --- /dev/null +++ b/pkg/handlers/webtorrent.go @@ -0,0 +1,578 @@ +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 := ` + +
+ + +