mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-10-27 17:38:51 +05:00
Bug fix
This commit is contained in:
52
README.md
52
README.md
@@ -1,48 +1,16 @@
|
|||||||
# Neo Movies API (Go Version) 🎬
|
# Neo Movies API
|
||||||
|
|
||||||
> Современный API для поиска фильмов и сериалов, портированный с Node.js на Go
|
REST API для поиска и получения информации о фильмах, использующий TMDB API.
|
||||||
|
|
||||||
## 🚀 Особенности
|
## Особенности
|
||||||
|
|
||||||
- ⚡ **Высокая производительность** - написан на Go
|
- Поиск фильмов
|
||||||
- 🔒 **JWT аутентификация** с email верификацией
|
- Информация о фильмах
|
||||||
- 🎭 **TMDB API интеграция** для данных о фильмах/сериалах
|
- Популярные фильмы
|
||||||
- 📧 **Email уведомления** через Gmail SMTP
|
- Топ рейтинговые фильмы
|
||||||
- 🔍 **Полнотекстовый поиск** фильмов и сериалов
|
- Предстоящие фильмы
|
||||||
- ⭐ **Система избранного** для пользователей
|
- Swagger документация
|
||||||
- 🎨 **Современная документация** с Scalar API Reference
|
- Поддержка русского языка
|
||||||
- 🌐 **CORS поддержка** для фронтенд интеграции
|
|
||||||
- ☁️ **Готов к деплою на Vercel**
|
|
||||||
|
|
||||||
## 📚 Основные функции
|
|
||||||
|
|
||||||
### 🔐 Аутентификация
|
|
||||||
- **Регистрация** с email верификацией (6-значный код)
|
|
||||||
- **Авторизация** JWT токенами
|
|
||||||
- **Управление профилем** пользователя
|
|
||||||
- **Email подтверждение** обязательно для входа
|
|
||||||
|
|
||||||
### 🎬 TMDB интеграция
|
|
||||||
- Поиск фильмов и сериалов
|
|
||||||
- Популярные, топ-рейтинговые, предстоящие
|
|
||||||
- Детальная информация с трейлерами и актерами
|
|
||||||
- Рекомендации и похожие фильмы
|
|
||||||
- Мультипоиск по всем типам контента
|
|
||||||
|
|
||||||
### ⭐ Пользовательские функции
|
|
||||||
- Добавление фильмов в избранное
|
|
||||||
- Персональные списки
|
|
||||||
- История просмотров
|
|
||||||
|
|
||||||
### 🎭 Плееры
|
|
||||||
- **Alloha Player** интеграция
|
|
||||||
- **Lumex Player** интеграция
|
|
||||||
|
|
||||||
### 📦 Дополнительно
|
|
||||||
- **Торренты** - поиск по IMDB ID с фильтрацией
|
|
||||||
- **Реакции** - лайки/дизлайки с внешним API
|
|
||||||
- **Изображения** - прокси для TMDB с кэшированием
|
|
||||||
- **Категории** - жанры и фильмы по категориям
|
|
||||||
|
|
||||||
## 🛠 Быстрый старт
|
## 🛠 Быстрый старт
|
||||||
|
|
||||||
|
|||||||
59
main.go
59
main.go
@@ -1,7 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
"neomovies-api/pkg/database"
|
"neomovies-api/pkg/database"
|
||||||
appHandlers "neomovies-api/pkg/handlers"
|
appHandlers "neomovies-api/pkg/handlers"
|
||||||
"neomovies-api/pkg/middleware"
|
"neomovies-api/pkg/middleware"
|
||||||
|
"neomovies-api/pkg/monitor"
|
||||||
"neomovies-api/pkg/services"
|
"neomovies-api/pkg/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Загружаем переменные окружения
|
// Загружаем переменные окружения
|
||||||
if err := godotenv.Load(); err != nil {
|
if err := godotenv.Load(); err != nil {
|
||||||
log.Println("Warning: .env file not found")
|
// Не выводим предупреждение в продакшене
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализируем конфигурацию
|
// Инициализируем конфигурацию
|
||||||
@@ -28,14 +29,15 @@ func main() {
|
|||||||
// Подключаемся к базе данных
|
// Подключаемся к базе данных
|
||||||
db, err := database.Connect(cfg.MongoURI)
|
db, err := database.Connect(cfg.MongoURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Failed to connect to database:", err)
|
fmt.Printf("❌ Failed to connect to database: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer database.Disconnect()
|
defer database.Disconnect()
|
||||||
|
|
||||||
// Инициализируем сервисы
|
// Инициализируем сервисы
|
||||||
tmdbService := services.NewTMDBService(cfg.TMDBAccessToken)
|
tmdbService := services.NewTMDBService(cfg.TMDBAccessToken)
|
||||||
emailService := services.NewEmailService(cfg)
|
emailService := services.NewEmailService(cfg)
|
||||||
authService := services.NewAuthService(db, cfg.JWTSecret, emailService)
|
authService := services.NewAuthService(db, cfg.JWTSecret, emailService, cfg.BaseURL)
|
||||||
movieService := services.NewMovieService(db, tmdbService)
|
movieService := services.NewMovieService(db, tmdbService)
|
||||||
tvService := services.NewTVService(db, tmdbService)
|
tvService := services.NewTVService(db, tmdbService)
|
||||||
torrentService := services.NewTorrentService()
|
torrentService := services.NewTorrentService()
|
||||||
@@ -75,11 +77,11 @@ func main() {
|
|||||||
|
|
||||||
// Категории
|
// Категории
|
||||||
api.HandleFunc("/categories", categoriesHandler.GetCategories).Methods("GET")
|
api.HandleFunc("/categories", categoriesHandler.GetCategories).Methods("GET")
|
||||||
api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET")// Плееры - ИСПРАВЛЕНО: добавлены параметры {imdb_id}
|
api.HandleFunc("/categories/{id}/movies", categoriesHandler.GetMoviesByCategory).Methods("GET")
|
||||||
|
|
||||||
// Плееры
|
// Плееры - ИСПРАВЛЕНО: добавлены параметры {imdb_id}
|
||||||
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("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
|
api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
|
||||||
@@ -95,7 +97,7 @@ func main() {
|
|||||||
// Изображения (прокси для TMDB)
|
// Изображения (прокси для TMDB)
|
||||||
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")
|
||||||
@@ -104,6 +106,7 @@ func main() {
|
|||||||
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("/tv/search", tvHandler.Search).Methods("GET")
|
api.HandleFunc("/tv/search", tvHandler.Search).Methods("GET")
|
||||||
@@ -114,6 +117,7 @@ func main() {
|
|||||||
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")
|
||||||
|
|
||||||
// Приватные маршруты (требуют авторизации)
|
// Приватные маршруты (требуют авторизации)
|
||||||
protected := api.PathPrefix("").Subrouter()
|
protected := api.PathPrefix("").Subrouter()
|
||||||
@@ -134,22 +138,43 @@ func main() {
|
|||||||
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE")
|
protected.HandleFunc("/reactions/{mediaType}/{mediaId}", reactionsHandler.RemoveReaction).Methods("DELETE")
|
||||||
protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET")
|
protected.HandleFunc("/reactions/my", reactionsHandler.GetMyReactions).Methods("GET")
|
||||||
|
|
||||||
// CORS и другие middleware
|
// CORS middleware
|
||||||
corsHandler := handlers.CORS(
|
corsHandler := handlers.CORS(
|
||||||
handlers.AllowedOrigins([]string{"*"}),
|
handlers.AllowedOrigins([]string{"*"}),
|
||||||
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}),
|
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"}),
|
||||||
handlers.AllowCredentials(),
|
handlers.AllowCredentials(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Применяем мониторинг запросов только в development
|
||||||
|
var finalHandler http.Handler
|
||||||
|
if cfg.NodeEnv == "development" {
|
||||||
|
// Добавляем middleware для мониторинга запросов
|
||||||
|
r.Use(monitor.RequestMonitor())
|
||||||
|
finalHandler = corsHandler(r)
|
||||||
|
|
||||||
|
// Выводим заголовок мониторинга
|
||||||
|
fmt.Println("\n🚀 NeoMovies API Server")
|
||||||
|
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
fmt.Printf("📡 Server: http://localhost:%s\n", cfg.Port)
|
||||||
|
fmt.Printf("📚 Docs: http://localhost:%s/\n", cfg.Port)
|
||||||
|
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
fmt.Printf("%-6s %-3s │ %-60s │ %8s\n", "METHOD", "CODE", "ENDPOINT", "TIME")
|
||||||
|
fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
||||||
|
} else {
|
||||||
|
finalHandler = corsHandler(r)
|
||||||
|
fmt.Printf("✅ Server starting on port %s\n", cfg.Port)
|
||||||
|
}
|
||||||
|
|
||||||
// Определяем порт
|
// Определяем порт
|
||||||
port := os.Getenv("PORT")
|
port := cfg.Port
|
||||||
if port == "" {
|
if port == "" {
|
||||||
port = "3000"
|
port = "3000"
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Server starting on port %s", port)
|
// Запускаем сервер
|
||||||
log.Printf("API documentation available at: http://localhost:%s/", port)
|
if err := http.ListenAndServe(":"+port, finalHandler); err != nil {
|
||||||
|
fmt.Printf("❌ Server failed to start: %v\n", err)
|
||||||
log.Fatal(http.ListenAndServe(":"+port, corsHandler(r)))
|
os.Exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,16 +259,30 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
|
|||||||
},
|
},
|
||||||
"/api/v1/torrents/search/{imdbId}": map[string]interface{}{
|
"/api/v1/torrents/search/{imdbId}": map[string]interface{}{
|
||||||
"get": map[string]interface{}{
|
"get": map[string]interface{}{
|
||||||
"summary": "Поиск торрентов",
|
"summary": "Поиск торрентов",
|
||||||
"description": "Поиск торрентов по IMDB ID",
|
"description": "Поиск торрентов по IMDB ID",
|
||||||
"tags": []string{"Torrents"},
|
"tags": []string{"Torrents"},
|
||||||
"parameters": []map[string]interface{}{
|
"parameters": []map[string]interface{}{
|
||||||
{
|
{
|
||||||
"name": "imdbId",
|
"name": "imdbId",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": map[string]string{"type": "string"},
|
"schema": map[string]string{"type": "string"},
|
||||||
"description": "IMDB ID фильма",
|
"description": "IMDB ID фильма или сериала",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": map[string]interface{}{"type": "string", "enum": []string{"movie", "tv", "serial"}},
|
||||||
|
"description": "Тип контента: movie (фильм) или tv/serial (сериал)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "season",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": map[string]interface{}{"type": "integer"},
|
||||||
|
"description": "Номер сезона (для сериалов)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"responses": map[string]interface{}{
|
"responses": map[string]interface{}{
|
||||||
@@ -545,6 +559,38 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// --- Добавленный блок для DELETE-запроса ---
|
||||||
|
"delete": map[string]interface{}{
|
||||||
|
"summary": "Удалить аккаунт пользователя",
|
||||||
|
"description": "Полное и безвозвратное удаление аккаунта пользователя и всех связанных с ним данных (избранное, реакции)",
|
||||||
|
"tags": []string{"Authentication"},
|
||||||
|
"security": []map[string][]string{
|
||||||
|
{"bearerAuth": []string{}},
|
||||||
|
},
|
||||||
|
"responses": map[string]interface{}{
|
||||||
|
"200": map[string]interface{}{
|
||||||
|
"description": "Аккаунт успешно удален",
|
||||||
|
"content": map[string]interface{}{
|
||||||
|
"application/json": map[string]interface{}{
|
||||||
|
"schema": map[string]interface{}{
|
||||||
|
"type": "object",
|
||||||
|
"properties": map[string]interface{}{
|
||||||
|
"success": map[string]interface{}{"type": "boolean"},
|
||||||
|
"message": map[string]interface{}{"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"401": map[string]interface{}{
|
||||||
|
"description": "Неавторизованный запрос",
|
||||||
|
},
|
||||||
|
"500": map[string]interface{}{
|
||||||
|
"description": "Внутренняя ошибка сервера",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// ------------------------------------------
|
||||||
},
|
},
|
||||||
"/api/v1/movies/search": map[string]interface{}{
|
"/api/v1/movies/search": map[string]interface{}{
|
||||||
"get": map[string]interface{}{
|
"get": map[string]interface{}{
|
||||||
@@ -861,11 +907,13 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
|
|||||||
"name": "page",
|
"name": "page",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": map[string]string{"type": "integer", "default": "1"},
|
"schema": map[string]string{"type": "integer", "default": "1"},
|
||||||
|
"description": "Номер страницы",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "language",
|
"name": "language",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"schema": map[string]string{"type": "string", "default": "ru-RU"},
|
"schema": map[string]string{"type": "string", "default": "ru-RU"},
|
||||||
|
"description": "Язык ответа",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"responses": map[string]interface{}{
|
"responses": map[string]interface{}{
|
||||||
@@ -1329,4 +1377,4 @@ func getOpenAPISpecWithURL(baseURL string) *OpenAPISpec {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
@@ -17,133 +20,56 @@ import (
|
|||||||
"neomovies-api/pkg/models"
|
"neomovies-api/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AuthService contains the database connection, JWT secret, and email service.
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
db *mongo.Database
|
db *mongo.Database
|
||||||
jwtSecret string
|
jwtSecret string
|
||||||
emailService *EmailService
|
emailService *EmailService
|
||||||
|
cubAPIURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService) *AuthService {
|
// Reaction represents a reaction entry in the database.
|
||||||
|
type Reaction struct {
|
||||||
|
MediaID string `bson:"mediaId"`
|
||||||
|
Type string `bson:"type"`
|
||||||
|
UserID primitive.ObjectID `bson:"userId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuthService creates and initializes a new AuthService.
|
||||||
|
func NewAuthService(db *mongo.Database, jwtSecret string, emailService *EmailService, cubAPIURL string) *AuthService {
|
||||||
service := &AuthService{
|
service := &AuthService{
|
||||||
db: db,
|
db: db,
|
||||||
jwtSecret: jwtSecret,
|
jwtSecret: jwtSecret,
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
|
cubAPIURL: cubAPIURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запускаем тест подключения к базе данных
|
|
||||||
go service.testDatabaseConnection()
|
|
||||||
|
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
|
|
||||||
// testDatabaseConnection тестирует подключение к базе данных и выводит информацию о пользователях
|
// generateVerificationCode creates a 6-digit verification code.
|
||||||
func (s *AuthService) testDatabaseConnection() {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
fmt.Println("=== DATABASE CONNECTION TEST ===")
|
|
||||||
|
|
||||||
// Проверяем подключение
|
|
||||||
err := s.db.Client().Ping(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ Database connection failed: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("✅ Database connection successful\n")
|
|
||||||
fmt.Printf("📊 Database name: %s\n", s.db.Name())
|
|
||||||
|
|
||||||
// Получаем список всех коллекций
|
|
||||||
collections, err := s.db.ListCollectionNames(ctx, bson.M{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ Failed to list collections: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("📁 Available collections: %v\n", collections)
|
|
||||||
|
|
||||||
// Проверяем коллекцию users
|
|
||||||
collection := s.db.Collection("users")
|
|
||||||
|
|
||||||
// Подсчитываем количество документов
|
|
||||||
count, err := collection.CountDocuments(ctx, bson.M{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ Failed to count users: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("👥 Total users in database: %d\n", count)
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
// Показываем всех пользователей
|
|
||||||
cursor, err := collection.Find(ctx, bson.M{})
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ Failed to find users: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer cursor.Close(ctx)
|
|
||||||
|
|
||||||
var users []bson.M
|
|
||||||
if err := cursor.All(ctx, &users); err != nil {
|
|
||||||
fmt.Printf("❌ Failed to decode users: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("📋 All users in database:\n")
|
|
||||||
for i, user := range users {
|
|
||||||
fmt.Printf(" %d. Email: %s, Name: %s, Verified: %v\n",
|
|
||||||
i+1,
|
|
||||||
user["email"],
|
|
||||||
user["name"],
|
|
||||||
user["verified"])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Тестируем поиск конкретного пользователя
|
|
||||||
fmt.Printf("\n🔍 Testing specific user search:\n")
|
|
||||||
testEmails := []string{"neo.movies.mail@gmail.com", "fenixoffc@gmail.com", "test@example.com"}
|
|
||||||
|
|
||||||
for _, email := range testEmails {
|
|
||||||
var user bson.M
|
|
||||||
err := collection.FindOne(ctx, bson.M{"email": email}).Decode(&user)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" ❌ User %s: NOT FOUND (%v)\n", email, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✅ User %s: FOUND (Name: %s, Verified: %v)\n",
|
|
||||||
email,
|
|
||||||
user["name"],
|
|
||||||
user["verified"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("=== END DATABASE TEST ===")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Генерация 6-значного кода
|
|
||||||
func (s *AuthService) generateVerificationCode() string {
|
func (s *AuthService) generateVerificationCode() string {
|
||||||
return fmt.Sprintf("%06d", rand.Intn(900000)+100000)
|
return fmt.Sprintf("%06d", rand.Intn(900000)+100000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Register registers a new user.
|
||||||
func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface{}, error) {
|
func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface{}, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
// Проверяем, не существует ли уже пользователь с таким email
|
|
||||||
var existingUser models.User
|
var existingUser models.User
|
||||||
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&existingUser)
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&existingUser)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil, errors.New("email already registered")
|
return nil, errors.New("email already registered")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Хешируем пароль
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем код верификации
|
|
||||||
code := s.generateVerificationCode()
|
code := s.generateVerificationCode()
|
||||||
codeExpires := time.Now().Add(10 * time.Minute) // 10 минут
|
codeExpires := time.Now().Add(10 * time.Minute)
|
||||||
|
|
||||||
// Создаем нового пользователя (НЕ ВЕРИФИЦИРОВАННОГО)
|
|
||||||
user := models.User{
|
user := models.User{
|
||||||
ID: primitive.NewObjectID(),
|
ID: primitive.NewObjectID(),
|
||||||
Email: req.Email,
|
Email: req.Email,
|
||||||
@@ -164,7 +90,6 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем код верификации на email
|
|
||||||
if s.emailService != nil {
|
if s.emailService != nil {
|
||||||
go s.emailService.SendVerificationEmail(user.Email, code)
|
go s.emailService.SendVerificationEmail(user.Email, code)
|
||||||
}
|
}
|
||||||
@@ -175,33 +100,25 @@ func (s *AuthService) Register(req models.RegisterRequest) (map[string]interface
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Login authenticates a user.
|
||||||
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
|
func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
fmt.Printf("🔍 Login attempt for email: %s\n", req.Email)
|
|
||||||
fmt.Printf("📊 Database name: %s\n", s.db.Name())
|
|
||||||
fmt.Printf("📁 Collection name: %s\n", collection.Name())
|
|
||||||
|
|
||||||
// Находим пользователя по email (точно как в JavaScript)
|
|
||||||
var user models.User
|
var user models.User
|
||||||
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
|
err := collection.FindOne(context.Background(), bson.M{"email": req.Email}).Decode(&user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("❌ User not found: %v\n", err)
|
|
||||||
return nil, errors.New("User not found")
|
return nil, errors.New("User not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем верификацию email (точно как в JavaScript)
|
|
||||||
if !user.Verified {
|
if !user.Verified {
|
||||||
return nil, errors.New("Account not activated. Please verify your email.")
|
return nil, errors.New("Account not activated. Please verify your email.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем пароль (точно как в JavaScript)
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
|
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Invalid password")
|
return nil, errors.New("Invalid password")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем JWT токен
|
|
||||||
token, err := s.generateJWT(user.ID.Hex())
|
token, err := s.generateJWT(user.ID.Hex())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -213,6 +130,7 @@ func (s *AuthService) Login(req models.LoginRequest) (*models.AuthResponse, erro
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUserByID retrieves a user by their ID.
|
||||||
func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
|
func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
@@ -230,6 +148,7 @@ func (s *AuthService) GetUserByID(userID string) (*models.User, error) {
|
|||||||
return &user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUser updates a user's information.
|
||||||
func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, error) {
|
func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
@@ -252,10 +171,11 @@ func (s *AuthService) UpdateUser(userID string, updates bson.M) (*models.User, e
|
|||||||
return s.GetUserByID(userID)
|
return s.GetUserByID(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// generateJWT generates a new JWT for a given user ID.
|
||||||
func (s *AuthService) generateJWT(userID string) (string, error) {
|
func (s *AuthService) generateJWT(userID string) (string, error) {
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(), // 7 дней
|
"exp": time.Now().Add(time.Hour * 24 * 7).Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
"jti": uuid.New().String(),
|
"jti": uuid.New().String(),
|
||||||
}
|
}
|
||||||
@@ -264,7 +184,7 @@ func (s *AuthService) generateJWT(userID string) (string, error) {
|
|||||||
return token.SignedString([]byte(s.jwtSecret))
|
return token.SignedString([]byte(s.jwtSecret))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Верификация email
|
// VerifyEmail verifies a user's email with a code.
|
||||||
func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) {
|
func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]interface{}, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
@@ -281,12 +201,10 @@ func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]int
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем код и срок действия
|
|
||||||
if user.VerificationCode != req.Code || user.VerificationExpires.Before(time.Now()) {
|
if user.VerificationCode != req.Code || user.VerificationExpires.Before(time.Now()) {
|
||||||
return nil, errors.New("invalid or expired verification code")
|
return nil, errors.New("invalid or expired verification code")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Верифицируем пользователя
|
|
||||||
_, err = collection.UpdateOne(
|
_, err = collection.UpdateOne(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
bson.M{"email": req.Email},
|
bson.M{"email": req.Email},
|
||||||
@@ -308,7 +226,7 @@ func (s *AuthService) VerifyEmail(req models.VerifyEmailRequest) (map[string]int
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Повторная отправка кода верификации
|
// ResendVerificationCode sends a new verification email.
|
||||||
func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[string]interface{}, error) {
|
func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[string]interface{}, error) {
|
||||||
collection := s.db.Collection("users")
|
collection := s.db.Collection("users")
|
||||||
|
|
||||||
@@ -322,11 +240,9 @@ func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[
|
|||||||
return nil, errors.New("email already verified")
|
return nil, errors.New("email already verified")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Генерируем новый код
|
|
||||||
code := s.generateVerificationCode()
|
code := s.generateVerificationCode()
|
||||||
codeExpires := time.Now().Add(10 * time.Minute)
|
codeExpires := time.Now().Add(10 * time.Minute)
|
||||||
|
|
||||||
// Обновляем код в базе
|
|
||||||
_, err = collection.UpdateOne(
|
_, err = collection.UpdateOne(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
bson.M{"email": req.Email},
|
bson.M{"email": req.Email},
|
||||||
@@ -341,7 +257,6 @@ func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отправляем новый код на email
|
|
||||||
if s.emailService != nil {
|
if s.emailService != nil {
|
||||||
go s.emailService.SendVerificationEmail(user.Email, code)
|
go s.emailService.SendVerificationEmail(user.Email, code)
|
||||||
}
|
}
|
||||||
@@ -350,4 +265,77 @@ func (s *AuthService) ResendVerificationCode(req models.ResendCodeRequest) (map[
|
|||||||
"success": true,
|
"success": true,
|
||||||
"message": "Verification code sent to your email",
|
"message": "Verification code sent to your email",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteAccount deletes a user and all associated data.
|
||||||
|
func (s *AuthService) DeleteAccount(ctx context.Context, userID string) error {
|
||||||
|
objectID, err := primitive.ObjectIDFromHex(userID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid user ID format: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Find user reactions and remove them from cub.rip
|
||||||
|
if s.cubAPIURL != "" {
|
||||||
|
reactionsCollection := s.db.Collection("reactions")
|
||||||
|
var userReactions []Reaction
|
||||||
|
cursor, err := reactionsCollection.Find(ctx, bson.M{"userId": objectID})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find user reactions: %w", err)
|
||||||
|
}
|
||||||
|
if err = cursor.All(ctx, &userReactions); err != nil {
|
||||||
|
return fmt.Errorf("failed to decode user reactions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
for _, reaction := range userReactions {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(r Reaction) {
|
||||||
|
defer wg.Done()
|
||||||
|
url := fmt.Sprintf("%s/reactions/remove/%s/%s", s.cubAPIURL, r.MediaID, r.Type)
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", url, nil) // or "DELETE"
|
||||||
|
if err != nil {
|
||||||
|
// Log the error but don't stop the process
|
||||||
|
fmt.Printf("failed to create request for cub.rip: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to send request to cub.rip: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
fmt.Printf("cub.rip API responded with status %d: %s\n", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
}(reaction)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Delete all user-related data from the database
|
||||||
|
usersCollection := s.db.Collection("users")
|
||||||
|
favoritesCollection := s.db.Collection("favorites")
|
||||||
|
reactionsCollection := s.db.Collection("reactions")
|
||||||
|
|
||||||
|
_, err = usersCollection.DeleteOne(ctx, bson.M{"_id": objectID})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = favoritesCollection.DeleteMany(ctx, bson.M{"userId": objectID})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user favorites: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = reactionsCollection.DeleteMany(ctx, bson.M{"userId": objectID})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete user reactions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewTorrentService() *TorrentService {
|
|||||||
// SearchTorrents - основной метод поиска торрентов через RedAPI
|
// SearchTorrents - основной метод поиска торрентов через RedAPI
|
||||||
func (s *TorrentService) SearchTorrents(params map[string]string) (*models.TorrentSearchResponse, error) {
|
func (s *TorrentService) SearchTorrents(params map[string]string) (*models.TorrentSearchResponse, error) {
|
||||||
searchParams := url.Values{}
|
searchParams := url.Values{}
|
||||||
|
|
||||||
// Добавляем все параметры поиска
|
// Добавляем все параметры поиска
|
||||||
for key, value := range params {
|
for key, value := range params {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
@@ -43,13 +43,13 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.apiKey != "" {
|
if s.apiKey != "" {
|
||||||
searchParams.Add("apikey", s.apiKey)
|
searchParams.Add("apikey", s.apiKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?%s", s.baseURL, searchParams.Encode())
|
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?%s", s.baseURL, searchParams.Encode())
|
||||||
|
|
||||||
resp, err := s.client.Get(searchURL)
|
resp, err := s.client.Get(searchURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to search torrents: %w", err)
|
return nil, fmt.Errorf("failed to search torrents: %w", err)
|
||||||
@@ -67,7 +67,7 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
|
|||||||
}
|
}
|
||||||
|
|
||||||
results := s.parseRedAPIResults(redAPIResponse)
|
results := s.parseRedAPIResults(redAPIResponse)
|
||||||
|
|
||||||
return &models.TorrentSearchResponse{
|
return &models.TorrentSearchResponse{
|
||||||
Query: params["query"],
|
Query: params["query"],
|
||||||
Results: results,
|
Results: results,
|
||||||
@@ -78,7 +78,7 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
|
|||||||
// parseRedAPIResults преобразует результаты RedAPI в наш формат
|
// parseRedAPIResults преобразует результаты RedAPI в наш формат
|
||||||
func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult {
|
func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult {
|
||||||
var results []models.TorrentResult
|
var results []models.TorrentResult
|
||||||
|
|
||||||
for _, torrent := range data.Results {
|
for _, torrent := range data.Results {
|
||||||
// Обрабатываем размер - может быть строкой или числом
|
// Обрабатываем размер - может быть строкой или числом
|
||||||
var sizeStr string
|
var sizeStr string
|
||||||
@@ -92,7 +92,7 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
|
|||||||
default:
|
default:
|
||||||
sizeStr = ""
|
sizeStr = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
result := models.TorrentResult{
|
result := models.TorrentResult{
|
||||||
Title: torrent.Title,
|
Title: torrent.Title,
|
||||||
Tracker: torrent.Tracker,
|
Tracker: torrent.Tracker,
|
||||||
@@ -105,7 +105,7 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
|
|||||||
Details: torrent.Details,
|
Details: torrent.Details,
|
||||||
Source: "RedAPI",
|
Source: "RedAPI",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем информацию из Info если она есть
|
// Добавляем информацию из Info если она есть
|
||||||
if torrent.Info != nil {
|
if torrent.Info != nil {
|
||||||
// Обрабатываем качество - может быть строкой или числом
|
// Обрабатываем качество - может быть строкой или числом
|
||||||
@@ -117,56 +117,55 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
|
|||||||
case int:
|
case int:
|
||||||
result.Quality = fmt.Sprintf("%dp", v)
|
result.Quality = fmt.Sprintf("%dp", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Voice = torrent.Info.Voices
|
result.Voice = torrent.Info.Voices
|
||||||
result.Types = torrent.Info.Types
|
result.Types = torrent.Info.Types
|
||||||
result.Seasons = torrent.Info.Seasons
|
result.Seasons = torrent.Info.Seasons
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если качество не определено через Info, пытаемся извлечь из названия
|
// Если качество не определено через Info, пытаемся извлечь из названия
|
||||||
if result.Quality == "" {
|
if result.Quality == "" {
|
||||||
result.Quality = s.ExtractQuality(result.Title)
|
result.Quality = s.ExtractQuality(result.Title)
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchTorrentsByIMDbID - поиск по IMDB ID с поддержкой всех функций
|
// SearchTorrentsByIMDbID - поиск по IMDB ID с поддержкой всех функций
|
||||||
func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID, mediaType string, options *models.TorrentSearchOptions) (*models.TorrentSearchResponse, error) {
|
func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID, mediaType string, options *models.TorrentSearchOptions) (*models.TorrentSearchResponse, error) {
|
||||||
// Получаем информацию о фильме/сериале из TMDB
|
// Получаем информацию о фильме/сериале из TMDB
|
||||||
|
// ИСПРАВЛЕНО: Теперь присваиваются все 4 возвращаемых значения
|
||||||
title, originalTitle, year, err := s.getTitleFromTMDB(tmdbService, imdbID, mediaType)
|
title, originalTitle, year, err := s.getTitleFromTMDB(tmdbService, imdbID, mediaType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get title from TMDB: %w", err)
|
return nil, fmt.Errorf("failed to get title from TMDB: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Формируем параметры поиска
|
// Создаем параметры поиска для RedAPI
|
||||||
params := make(map[string]string)
|
params := map[string]string{
|
||||||
params["imdb"] = imdbID
|
"imdb": imdbID,
|
||||||
params["title"] = title
|
"query": title,
|
||||||
params["title_original"] = originalTitle
|
"title_original": originalTitle,
|
||||||
params["year"] = year
|
"year": year,
|
||||||
|
}
|
||||||
// Устанавливаем тип контента и категорию
|
|
||||||
|
// Определяем тип контента для API
|
||||||
switch mediaType {
|
switch mediaType {
|
||||||
case "movie":
|
case "movie":
|
||||||
params["is_serial"] = "1"
|
params["is_serial"] = "1"
|
||||||
params["category"] = "2000"
|
params["category"] = "2000"
|
||||||
case "tv", "series":
|
case "serial", "series", "tv":
|
||||||
params["is_serial"] = "2"
|
params["is_serial"] = "2"
|
||||||
params["category"] = "5000"
|
params["category"] = "5000"
|
||||||
case "anime":
|
case "anime":
|
||||||
params["is_serial"] = "5"
|
params["is_serial"] = "5"
|
||||||
params["category"] = "5070"
|
params["category"] = "5070"
|
||||||
default:
|
|
||||||
params["is_serial"] = "1"
|
|
||||||
params["category"] = "2000"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Добавляем сезон если указан
|
// Добавляем сезон, если он указан
|
||||||
if options != nil && options.Season != nil {
|
if options != nil && options.Season != nil && *options.Season > 0 {
|
||||||
params["season"] = strconv.Itoa(*options.Season)
|
params["season"] = strconv.Itoa(*options.Season)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +180,40 @@ func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID
|
|||||||
response.Results = s.FilterByContentType(response.Results, options.ContentType)
|
response.Results = s.FilterByContentType(response.Results, options.ContentType)
|
||||||
response.Results = s.FilterTorrents(response.Results, options)
|
response.Results = s.FilterTorrents(response.Results, options)
|
||||||
response.Results = s.sortTorrents(response.Results, options.SortBy, options.SortOrder)
|
response.Results = s.sortTorrents(response.Results, options.SortBy, options.SortOrder)
|
||||||
response.Total = len(response.Results)
|
}
|
||||||
|
response.Total = len(response.Results)
|
||||||
|
|
||||||
|
// Fallback для сериалов, если результатов мало
|
||||||
|
if len(response.Results) < 5 && (mediaType == "serial" || mediaType == "series" || mediaType == "tv") && options != nil && options.Season != nil {
|
||||||
|
paramsNoSeason := map[string]string{
|
||||||
|
"imdb": imdbID,
|
||||||
|
"query": title,
|
||||||
|
"title_original": originalTitle,
|
||||||
|
"year": year,
|
||||||
|
"is_serial": "2",
|
||||||
|
"category": "5000",
|
||||||
|
}
|
||||||
|
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
|
||||||
|
if err == nil {
|
||||||
|
filtered := s.filterBySeason(fallbackResp.Results, *options.Season)
|
||||||
|
all := append(response.Results, filtered...)
|
||||||
|
unique := make([]models.TorrentResult, 0, len(all))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, t := range all {
|
||||||
|
if !seen[t.MagnetLink] {
|
||||||
|
unique = append(unique, t)
|
||||||
|
seen[t.MagnetLink] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
response.Results = unique
|
||||||
|
response.Total = len(response.Results)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// SearchMovies - поиск фильмов с дополнительной фильтрацией
|
// SearchMovies - поиск фильмов с дополнительной фильтрацией
|
||||||
func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
|
func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
|
||||||
params := map[string]string{
|
params := map[string]string{
|
||||||
@@ -196,15 +223,15 @@ func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*model
|
|||||||
"is_serial": "1",
|
"is_serial": "1",
|
||||||
"category": "2000",
|
"category": "2000",
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := s.SearchTorrents(params)
|
response, err := s.SearchTorrents(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Results = s.FilterByContentType(response.Results, "movie")
|
response.Results = s.FilterByContentType(response.Results, "movie")
|
||||||
response.Total = len(response.Results)
|
response.Total = len(response.Results)
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,15 +331,15 @@ func (s *TorrentService) SearchAnime(title, originalTitle, year string) (*models
|
|||||||
"is_serial": "5",
|
"is_serial": "5",
|
||||||
"category": "5070",
|
"category": "5070",
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := s.SearchTorrents(params)
|
response, err := s.SearchTorrents(params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Results = s.FilterByContentType(response.Results, "anime")
|
response.Results = s.FilterByContentType(response.Results, "anime")
|
||||||
response.Total = len(response.Results)
|
response.Total = len(response.Results)
|
||||||
|
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +358,7 @@ type AllohaResponse struct {
|
|||||||
func (s *TorrentService) getMovieInfoByIMDB(imdbID string) (string, string, string, error) {
|
func (s *TorrentService) getMovieInfoByIMDB(imdbID string) (string, string, string, error) {
|
||||||
// Используем тот же токен что и в JavaScript версии
|
// Используем тот же токен что и в JavaScript версии
|
||||||
endpoint := fmt.Sprintf("https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1&imdb=%s", imdbID)
|
endpoint := fmt.Sprintf("https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1&imdb=%s", imdbID)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", endpoint, nil)
|
req, err := http.NewRequest("GET", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
@@ -377,7 +404,7 @@ func (s *TorrentService) getTitleFromTMDB(tmdbService *TMDBService, imdbID, medi
|
|||||||
|
|
||||||
// Если Alloha API не работает, пробуем TMDB API
|
// Если Alloha API не работает, пробуем TMDB API
|
||||||
endpoint := fmt.Sprintf("https://api.themoviedb.org/3/find/%s", imdbID)
|
endpoint := fmt.Sprintf("https://api.themoviedb.org/3/find/%s", imdbID)
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", endpoint, nil)
|
req, err := http.NewRequest("GET", endpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", "", err
|
return "", "", "", err
|
||||||
@@ -444,14 +471,12 @@ func (s *TorrentService) getTitleFromTMDB(tmdbService *TMDBService, imdbID, medi
|
|||||||
return "", "", "", fmt.Errorf("no results found for IMDB ID: %s", imdbID)
|
return "", "", "", fmt.Errorf("no results found for IMDB ID: %s", imdbID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterByContentType - фильтрация по типу контента
|
// FilterByContentType - фильтрация по типу контента (как в JS)
|
||||||
func (s *TorrentService) FilterByContentType(results []models.TorrentResult, contentType string) []models.TorrentResult {
|
func (s *TorrentService) FilterByContentType(results []models.TorrentResult, contentType string) []models.TorrentResult {
|
||||||
if contentType == "" {
|
if contentType == "" {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
var filtered []models.TorrentResult
|
var filtered []models.TorrentResult
|
||||||
|
|
||||||
for _, torrent := range results {
|
for _, torrent := range results {
|
||||||
// Фильтрация по полю types, если оно есть
|
// Фильтрация по полю types, если оно есть
|
||||||
if len(torrent.Types) > 0 {
|
if len(torrent.Types) > 0 {
|
||||||
@@ -460,7 +485,7 @@ func (s *TorrentService) FilterByContentType(results []models.TorrentResult, con
|
|||||||
if s.containsAny(torrent.Types, []string{"movie", "multfilm", "documovie"}) {
|
if s.containsAny(torrent.Types, []string{"movie", "multfilm", "documovie"}) {
|
||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
case "serial":
|
case "serial", "series", "tv":
|
||||||
if s.containsAny(torrent.Types, []string{"serial", "multserial", "docuserial", "tvshow"}) {
|
if s.containsAny(torrent.Types, []string{"serial", "multserial", "docuserial", "tvshow"}) {
|
||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
@@ -471,7 +496,6 @@ func (s *TorrentService) FilterByContentType(results []models.TorrentResult, con
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Фильтрация по названию, если types недоступно
|
// Фильтрация по названию, если types недоступно
|
||||||
title := strings.ToLower(torrent.Title)
|
title := strings.ToLower(torrent.Title)
|
||||||
switch contentType {
|
switch contentType {
|
||||||
@@ -479,7 +503,7 @@ func (s *TorrentService) FilterByContentType(results []models.TorrentResult, con
|
|||||||
if !regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
|
if !regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
|
||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
case "serial":
|
case "serial", "series", "tv":
|
||||||
if regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
|
if regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
|
||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
@@ -491,7 +515,6 @@ func (s *TorrentService) FilterByContentType(results []models.TorrentResult, con
|
|||||||
filtered = append(filtered, torrent)
|
filtered = append(filtered, torrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +602,7 @@ func (s *TorrentService) matchesSeason(torrent models.TorrentResult, season int)
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем в названии
|
// Проверяем в названии
|
||||||
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
||||||
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
||||||
@@ -594,14 +617,14 @@ func (s *TorrentService) matchesSeason(torrent models.TorrentResult, season int)
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractQuality - извлечение качества из названия
|
// ExtractQuality - извлечение качества из названия
|
||||||
func (s *TorrentService) ExtractQuality(title string) string {
|
func (s *TorrentService) ExtractQuality(title string) string {
|
||||||
title = strings.ToUpper(title)
|
title = strings.ToUpper(title)
|
||||||
|
|
||||||
qualityPatterns := []struct {
|
qualityPatterns := []struct {
|
||||||
pattern string
|
pattern string
|
||||||
quality string
|
quality string
|
||||||
@@ -613,7 +636,7 @@ func (s *TorrentService) ExtractQuality(title string) string {
|
|||||||
{`480P`, "480p"},
|
{`480P`, "480p"},
|
||||||
{`360P`, "360p"},
|
{`360P`, "360p"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, qp := range qualityPatterns {
|
for _, qp := range qualityPatterns {
|
||||||
if matched, _ := regexp.MatchString(qp.pattern, title); matched {
|
if matched, _ := regexp.MatchString(qp.pattern, title); matched {
|
||||||
if qp.quality == "2160p" {
|
if qp.quality == "2160p" {
|
||||||
@@ -622,7 +645,7 @@ func (s *TorrentService) ExtractQuality(title string) string {
|
|||||||
return qp.quality
|
return qp.quality
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,14 +660,16 @@ func (s *TorrentService) sortTorrents(torrents []models.TorrentResult, sortBy, s
|
|||||||
|
|
||||||
sort.Slice(torrents, func(i, j int) bool {
|
sort.Slice(torrents, func(i, j int) bool {
|
||||||
var less bool
|
var less bool
|
||||||
|
|
||||||
switch sortBy {
|
switch sortBy {
|
||||||
case "seeders":
|
case "seeders":
|
||||||
less = torrents[i].Seeders < torrents[j].Seeders
|
less = torrents[i].Seeders < torrents[j].Seeders
|
||||||
case "size":
|
case "size":
|
||||||
less = s.compareSizes(torrents[i].Size, torrents[j].Size)
|
less = s.compareSizes(torrents[i].Size, torrents[j].Size)
|
||||||
case "date":
|
case "date":
|
||||||
less = torrents[i].PublishDate < torrents[j].PublishDate
|
t1, _ := time.Parse(time.RFC3339, torrents[i].PublishDate)
|
||||||
|
t2, _ := time.Parse(time.RFC3339, torrents[j].PublishDate)
|
||||||
|
less = t1.Before(t2)
|
||||||
default:
|
default:
|
||||||
less = torrents[i].Seeders < torrents[j].Seeders
|
less = torrents[i].Seeders < torrents[j].Seeders
|
||||||
}
|
}
|
||||||
@@ -661,43 +686,43 @@ func (s *TorrentService) sortTorrents(torrents []models.TorrentResult, sortBy, s
|
|||||||
// GroupByQuality - группировка по качеству
|
// GroupByQuality - группировка по качеству
|
||||||
func (s *TorrentService) GroupByQuality(results []models.TorrentResult) map[string][]models.TorrentResult {
|
func (s *TorrentService) GroupByQuality(results []models.TorrentResult) map[string][]models.TorrentResult {
|
||||||
groups := make(map[string][]models.TorrentResult)
|
groups := make(map[string][]models.TorrentResult)
|
||||||
|
|
||||||
for _, torrent := range results {
|
for _, torrent := range results {
|
||||||
quality := torrent.Quality
|
quality := torrent.Quality
|
||||||
if quality == "" {
|
if quality == "" {
|
||||||
quality = "unknown"
|
quality = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Объединяем 4K и 2160p в одну группу
|
// Объединяем 4K и 2160p в одну группу
|
||||||
if quality == "2160p" {
|
if quality == "2160p" {
|
||||||
quality = "4K"
|
quality = "4K"
|
||||||
}
|
}
|
||||||
|
|
||||||
groups[quality] = append(groups[quality], torrent)
|
groups[quality] = append(groups[quality], torrent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем торренты внутри каждой группы по сидам
|
// Сортируем торренты внутри каждой группы по сидам
|
||||||
for quality := range groups {
|
for quality := range groups {
|
||||||
sort.Slice(groups[quality], func(i, j int) bool {
|
sort.Slice(groups[quality], func(i, j int) bool {
|
||||||
return groups[quality][i].Seeders > groups[quality][j].Seeders
|
return groups[quality][i].Seeders > groups[quality][j].Seeders
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupBySeason - группировка по сезонам
|
// GroupBySeason - группировка по сезонам
|
||||||
func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[string][]models.TorrentResult {
|
func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[string][]models.TorrentResult {
|
||||||
groups := make(map[string][]models.TorrentResult)
|
groups := make(map[string][]models.TorrentResult)
|
||||||
|
|
||||||
for _, torrent := range results {
|
for _, torrent := range results {
|
||||||
seasons := make(map[int]bool)
|
seasons := make(map[int]bool)
|
||||||
|
|
||||||
// Извлекаем сезоны из поля seasons
|
// Извлекаем сезоны из поля seasons
|
||||||
for _, season := range torrent.Seasons {
|
for _, season := range torrent.Seasons {
|
||||||
seasons[season] = true
|
seasons[season] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Извлекаем сезоны из названия
|
// Извлекаем сезоны из названия
|
||||||
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
||||||
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
||||||
@@ -712,7 +737,7 @@ func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[strin
|
|||||||
seasons[seasonNumber] = true
|
seasons[seasonNumber] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если сезоны не найдены, добавляем в группу "unknown"
|
// Если сезоны не найдены, добавляем в группу "unknown"
|
||||||
if len(seasons) == 0 {
|
if len(seasons) == 0 {
|
||||||
groups["Неизвестно"] = append(groups["Неизвестно"], torrent)
|
groups["Неизвестно"] = append(groups["Неизвестно"], torrent)
|
||||||
@@ -734,14 +759,14 @@ func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сортируем торренты внутри каждой группы по сидам
|
// Сортируем торренты внутри каждой группы по сидам
|
||||||
for season := range groups {
|
for season := range groups {
|
||||||
sort.Slice(groups[season], func(i, j int) bool {
|
sort.Slice(groups[season], func(i, j int) bool {
|
||||||
return groups[season][i].Seeders > groups[season][j].Seeders
|
return groups[season][i].Seeders > groups[season][j].Seeders
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,15 +776,15 @@ func (s *TorrentService) GetAvailableSeasons(title, originalTitle, year string)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
seasonsSet := make(map[int]bool)
|
seasonsSet := make(map[int]bool)
|
||||||
|
|
||||||
for _, torrent := range response.Results {
|
for _, torrent := range response.Results {
|
||||||
// Извлекаем из поля seasons
|
// Извлекаем из поля seasons
|
||||||
for _, season := range torrent.Seasons {
|
for _, season := range torrent.Seasons {
|
||||||
seasonsSet[season] = true
|
seasonsSet[season] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Извлекаем из названия
|
// Извлекаем из названия
|
||||||
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
|
||||||
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
|
||||||
@@ -775,25 +800,99 @@ func (s *TorrentService) GetAvailableSeasons(title, originalTitle, year string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var seasons []int
|
var seasons []int
|
||||||
for season := range seasonsSet {
|
for season := range seasonsSet {
|
||||||
seasons = append(seasons, season)
|
seasons = append(seasons, season)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Ints(seasons)
|
sort.Ints(seasons)
|
||||||
return seasons, nil
|
return seasons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции
|
// SearchByImdb - поиск по IMDB ID (movie/serial/anime).
|
||||||
|
func (s *TorrentService) SearchByImdb(imdbID, contentType string, season *int) ([]models.TorrentResult, error) {
|
||||||
|
if imdbID == "" || !strings.HasPrefix(imdbID, "tt") {
|
||||||
|
return nil, fmt.Errorf("Неверный формат IMDB ID. Должен быть в формате tt1234567")
|
||||||
|
}
|
||||||
|
|
||||||
|
// НЕ добавляем title, originalTitle, year, чтобы запрос не был слишком строгим.
|
||||||
|
params := map[string]string{
|
||||||
|
"imdb": imdbID,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем тип контента для API
|
||||||
|
switch contentType {
|
||||||
|
case "movie":
|
||||||
|
params["is_serial"] = "1"
|
||||||
|
params["category"] = "2000"
|
||||||
|
case "serial", "series", "tv":
|
||||||
|
params["is_serial"] = "2"
|
||||||
|
params["category"] = "5000"
|
||||||
|
case "anime":
|
||||||
|
params["is_serial"] = "5"
|
||||||
|
params["category"] = "5070"
|
||||||
|
default:
|
||||||
|
// Значение по умолчанию на случай неизвестного типа
|
||||||
|
params["is_serial"] = "1"
|
||||||
|
params["category"] = "2000"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Параметр season можно оставить, он полезен
|
||||||
|
if season != nil && *season > 0 {
|
||||||
|
params["season"] = strconv.Itoa(*season)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := s.SearchTorrents(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results := resp.Results
|
||||||
|
|
||||||
|
// Fallback для сериалов: если указан сезон и результатов мало, ищем без сезона и фильтруем на клиенте
|
||||||
|
if s.contains([]string{"serial", "series", "tv"}, contentType) && season != nil && len(results) < 5 {
|
||||||
|
paramsNoSeason := map[string]string{
|
||||||
|
"imdb": imdbID,
|
||||||
|
"is_serial": "2",
|
||||||
|
"category": "5000",
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
|
||||||
|
if err == nil {
|
||||||
|
filtered := s.filterBySeason(fallbackResp.Results, *season)
|
||||||
|
// Объединяем и убираем дубликаты по MagnetLink
|
||||||
|
all := append(results, filtered...)
|
||||||
|
unique := make([]models.TorrentResult, 0, len(all))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, t := range all {
|
||||||
|
if !seen[t.MagnetLink] {
|
||||||
|
unique = append(unique, t)
|
||||||
|
seen[t.MagnetLink] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = unique
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Финальная фильтрация по типу контента на стороне клиента для надежности
|
||||||
|
results = s.FilterByContentType(results, contentType)
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ############# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ #############
|
||||||
|
|
||||||
func (s *TorrentService) qualityMeetsMinimum(quality, minQuality string) bool {
|
func (s *TorrentService) qualityMeetsMinimum(quality, minQuality string) bool {
|
||||||
qualityOrder := map[string]int{
|
qualityOrder := map[string]int{
|
||||||
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
|
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLevel := qualityOrder[strings.ToLower(quality)]
|
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
|
||||||
minLevel := qualityOrder[strings.ToLower(minQuality)]
|
minLevel, ok2 := qualityOrder[strings.ToLower(minQuality)]
|
||||||
|
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return true // Если качество не определено, не фильтруем
|
||||||
|
}
|
||||||
|
|
||||||
return currentLevel >= minLevel
|
return currentLevel >= minLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -801,21 +900,32 @@ func (s *TorrentService) qualityMeetsMaximum(quality, maxQuality string) bool {
|
|||||||
qualityOrder := map[string]int{
|
qualityOrder := map[string]int{
|
||||||
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
|
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLevel := qualityOrder[strings.ToLower(quality)]
|
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
|
||||||
maxLevel := qualityOrder[strings.ToLower(maxQuality)]
|
maxLevel, ok2 := qualityOrder[strings.ToLower(maxQuality)]
|
||||||
|
|
||||||
|
if !ok1 || !ok2 {
|
||||||
|
return true // Если качество не определено, не фильтруем
|
||||||
|
}
|
||||||
|
|
||||||
return currentLevel <= maxLevel
|
return currentLevel <= maxLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TorrentService) parseSize(sizeStr string) int64 {
|
||||||
|
val, err := strconv.ParseInt(sizeStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TorrentService) compareSizes(size1, size2 string) bool {
|
func (s *TorrentService) compareSizes(size1, size2 string) bool {
|
||||||
// Простое сравнение размеров (можно улучшить)
|
return s.parseSize(size1) < s.parseSize(size2)
|
||||||
return len(size1) < len(size2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *TorrentService) contains(slice []string, item string) bool {
|
func (s *TorrentService) contains(slice []string, item string) bool {
|
||||||
for _, s := range slice {
|
for _, s := range slice {
|
||||||
if s == item {
|
if strings.EqualFold(s, item) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -829,4 +939,4 @@ func (s *TorrentService) containsAny(slice []string, items []string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user