This commit is contained in:
2025-11-22 19:52:58 +02:00
parent 5a4b6a9396
commit 2928c3417b
9 changed files with 3353 additions and 1145 deletions

View File

@@ -18,6 +18,12 @@ ALLOHA_TOKEN=your_alloha_token
REDAPI_BASE_URL=http://redapi.cfhttp.top
REDAPI_KEY=your_redapi_key
COLLAPS_API_HOST=https://api.bhcesh.me
COLLAPS_TOKEN=your_collaps_token
VEOVEO_HOST=https://api.veoveo.app
VEOVEO_TOKEN=
JWT_SECRET=your_jwt_secret_key
GMAIL_USER=your_gmail@gmail.com
@@ -28,7 +34,7 @@ GOOGLE_CLIENT_SECRET=your_google_client_secret
GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback
BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001
FRONTEND_URL=http://localhost:5173
PORT=3000
NODE_ENV=development

View File

@@ -1,7 +1,6 @@
package handler
import (
"log"
"net/http"
"sync"
@@ -34,19 +33,15 @@ func initializeApp() {
var err error
globalDB, err = database.Connect(globalCfg.MongoURI, globalCfg.MongoDBName)
if err != nil {
log.Printf("Failed to connect to database: %v", err)
initError = err
return
}
log.Println("Successfully connected to database")
}
func Handler(w http.ResponseWriter, r *http.Request) {
initOnce.Do(initializeApp)
if initError != nil {
log.Printf("Initialization error: %v", initError)
http.Error(w, "Application initialization failed: "+initError.Error(), http.StatusInternalServerError)
return
}
@@ -114,6 +109,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET")
api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET")
api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET")
api.HandleFunc("/torrents/by-title", torrentsHandler.SearchByTitle).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET")

18
main.go
View File

@@ -2,6 +2,8 @@ package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
@@ -18,6 +20,18 @@ import (
)
func main() {
// Инициализация логирования в файл
logFile, err := os.OpenFile("neomovies-api.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Printf("Failed to open log file: %v\n", err)
} else {
// Пишем логи одновременно в файл и в консоль
multiWriter := io.MultiWriter(os.Stdout, logFile)
log.SetOutput(multiWriter)
log.SetFlags(log.LstdFlags | log.Lshortfile)
defer logFile.Close()
}
if err := godotenv.Load(); err != nil {
_ = err
}
@@ -84,13 +98,15 @@ func main() {
api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET")
api.HandleFunc("/players/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).Methods("GET")
api.HandleFunc("/players/collaps/{id_type}/{id}", playersHandler.GetCollapsPlayer).Methods("GET")
api.HandleFunc("/torrents/search/by-title", torrentsHandler.SearchByTitle).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/torrents/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET")
api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET")
api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET")

1988
neomovies-api.log Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,78 +1,82 @@
package config
import (
"log"
"os"
)
type Config struct {
MongoURI string
MongoDBName string
TMDBAccessToken string
JWTSecret string
Port string
BaseURL string
NodeEnv string
GmailUser string
GmailPassword string
LumexURL string
AllohaToken string
RedAPIBaseURL string
RedAPIKey string
GoogleClientID string
GoogleClientSecret string
GoogleRedirectURL string
FrontendURL string
VibixHost string
VibixToken string
KPAPIKey string
HDVBToken string
KPAPIBaseURL string
}
func New() *Config {
mongoURI := getMongoURI()
return &Config{
MongoURI: mongoURI,
MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName),
TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""),
JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret),
Port: getEnv(EnvPort, DefaultPort),
BaseURL: getEnv(EnvBaseURL, DefaultBaseURL),
NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv),
GmailUser: getEnv(EnvGmailUser, ""),
GmailPassword: getEnv(EnvGmailPassword, ""),
LumexURL: getEnv(EnvLumexURL, ""),
AllohaToken: getEnv(EnvAllohaToken, ""),
RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase),
RedAPIKey: getEnv(EnvRedAPIKey, ""),
GoogleClientID: getEnv(EnvGoogleClientID, ""),
GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""),
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
}
}
func getMongoURI() string {
for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} {
if value := os.Getenv(envVar); value != "" {
log.Printf("DEBUG: Using %s for MongoDB connection", envVar)
return value
}
}
log.Printf("DEBUG: No MongoDB URI environment variable found")
return ""
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
package config
import (
"os"
)
type Config struct {
MongoURI string
MongoDBName string
TMDBAccessToken string
JWTSecret string
Port string
BaseURL string
NodeEnv string
GmailUser string
GmailPassword string
LumexURL string
AllohaToken string
RedAPIBaseURL string
RedAPIKey string
GoogleClientID string
GoogleClientSecret string
GoogleRedirectURL string
FrontendURL string
VibixHost string
VibixToken string
KPAPIKey string
HDVBToken string
KPAPIBaseURL string
CollapsAPIHost string
CollapsToken string
VeoVeoHost string
VeoVeoToken string
}
func New() *Config {
mongoURI := getMongoURI()
return &Config{
MongoURI: mongoURI,
MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName),
TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""),
JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret),
Port: getEnv(EnvPort, DefaultPort),
BaseURL: getEnv(EnvBaseURL, DefaultBaseURL),
NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv),
GmailUser: getEnv(EnvGmailUser, ""),
GmailPassword: getEnv(EnvGmailPassword, ""),
LumexURL: getEnv(EnvLumexURL, ""),
AllohaToken: getEnv(EnvAllohaToken, ""),
RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase),
RedAPIKey: getEnv(EnvRedAPIKey, ""),
GoogleClientID: getEnv(EnvGoogleClientID, ""),
GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""),
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
CollapsAPIHost: getEnv("COLLAPS_API_HOST", ""),
CollapsToken: getEnv("COLLAPS_TOKEN", ""),
VeoVeoHost: getEnv("VEOVEO_HOST", ""),
VeoVeoToken: getEnv("VEOVEO_TOKEN", ""),
}
}
func getMongoURI() string {
for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} {
if value := os.Getenv(envVar); value != "" {
return value
}
}
return ""
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,207 +1,207 @@
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"neomovies-api/pkg/models"
"neomovies-api/pkg/services"
)
type TorrentsHandler struct {
torrentService *services.TorrentService
tmdbService *services.TMDBService
}
func NewTorrentsHandler(torrentService *services.TorrentService, tmdbService *services.TMDBService) *TorrentsHandler {
return &TorrentsHandler{
torrentService: torrentService,
tmdbService: tmdbService,
}
}
// SearchTorrents - поиск торрентов по IMDB ID
func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
imdbID := vars["imdbId"]
if imdbID == "" {
http.Error(w, "IMDB ID is required", http.StatusBadRequest)
return
}
// Параметры запроса
mediaType := r.URL.Query().Get("type")
if mediaType == "" {
mediaType = "movie"
}
// Создаем опции поиска
options := &models.TorrentSearchOptions{
ContentType: mediaType,
}
// Качество
if quality := r.URL.Query().Get("quality"); quality != "" {
options.Quality = strings.Split(quality, ",")
}
// Минимальное и максимальное качество
options.MinQuality = r.URL.Query().Get("minQuality")
options.MaxQuality = r.URL.Query().Get("maxQuality")
// Исключаемые качества
if excludeQualities := r.URL.Query().Get("excludeQualities"); excludeQualities != "" {
options.ExcludeQualities = strings.Split(excludeQualities, ",")
}
// HDR
if hdr := r.URL.Query().Get("hdr"); hdr != "" {
if hdrBool, err := strconv.ParseBool(hdr); err == nil {
options.HDR = &hdrBool
}
}
// HEVC
if hevc := r.URL.Query().Get("hevc"); hevc != "" {
if hevcBool, err := strconv.ParseBool(hevc); err == nil {
options.HEVC = &hevcBool
}
}
// Сортировка
options.SortBy = r.URL.Query().Get("sortBy")
if options.SortBy == "" {
options.SortBy = "seeders"
}
options.SortOrder = r.URL.Query().Get("sortOrder")
if options.SortOrder == "" {
options.SortOrder = "desc"
}
// Группировка
if groupByQuality := r.URL.Query().Get("groupByQuality"); groupByQuality == "true" {
options.GroupByQuality = true
}
if groupBySeason := r.URL.Query().Get("groupBySeason"); groupBySeason == "true" {
options.GroupBySeason = true
}
// Сезон для сериалов
if season := r.URL.Query().Get("season"); season != "" {
if seasonInt, err := strconv.Atoi(season); err == nil {
options.Season = &seasonInt
}
}
// Поиск торрентов
results, err := h.torrentService.SearchTorrentsByIMDbID(h.tmdbService, imdbID, mediaType, options)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Формируем ответ с группировкой если необходимо
response := map[string]interface{}{
"imdbId": imdbID,
"type": mediaType,
"total": results.Total,
}
if options.Season != nil {
response["season"] = *options.Season
}
// Применяем группировку если запрошена
if options.GroupByQuality && options.GroupBySeason {
// Группируем сначала по сезонам, затем по качеству внутри каждого сезона
seasonGroups := h.torrentService.GroupBySeason(results.Results)
finalGroups := make(map[string]map[string][]models.TorrentResult)
for season, torrents := range seasonGroups {
qualityGroups := h.torrentService.GroupByQuality(torrents)
finalGroups[season] = qualityGroups
}
response["grouped"] = true
response["groups"] = finalGroups
} else if options.GroupByQuality {
groups := h.torrentService.GroupByQuality(results.Results)
response["grouped"] = true
response["groups"] = groups
} else if options.GroupBySeason {
groups := h.torrentService.GroupBySeason(results.Results)
response["grouped"] = true
response["groups"] = groups
} else {
response["grouped"] = false
response["results"] = results.Results
}
if len(results.Results) == 0 {
response["error"] = "No torrents found for this IMDB ID"
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(response)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}
// SearchMovies - поиск фильмов по названию
func (h *TorrentsHandler) SearchMovies(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest)
return
}
results, err := h.torrentService.SearchMovies(title, originalTitle, year)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"title": title,
"originalTitle": originalTitle,
"year": year,
"type": "movie",
"total": results.Total,
"results": results.Results,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}
// SearchSeries - поиск сериалов по названию с поддержкой сезонов
func (h *TorrentsHandler) SearchSeries(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest)
return
}
package handlers
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/gorilla/mux"
"neomovies-api/pkg/models"
"neomovies-api/pkg/services"
)
type TorrentsHandler struct {
torrentService *services.TorrentService
tmdbService *services.TMDBService
}
func NewTorrentsHandler(torrentService *services.TorrentService, tmdbService *services.TMDBService) *TorrentsHandler {
return &TorrentsHandler{
torrentService: torrentService,
tmdbService: tmdbService,
}
}
// SearchTorrents - поиск торрентов по IMDB ID
func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
imdbID := vars["imdbId"]
if imdbID == "" {
http.Error(w, "IMDB ID is required", http.StatusBadRequest)
return
}
// Параметры запроса
mediaType := r.URL.Query().Get("type")
if mediaType == "" {
mediaType = "movie"
}
// Создаем опции поиска
options := &models.TorrentSearchOptions{
ContentType: mediaType,
}
// Качество
if quality := r.URL.Query().Get("quality"); quality != "" {
options.Quality = strings.Split(quality, ",")
}
// Минимальное и максимальное качество
options.MinQuality = r.URL.Query().Get("minQuality")
options.MaxQuality = r.URL.Query().Get("maxQuality")
// Исключаемые качества
if excludeQualities := r.URL.Query().Get("excludeQualities"); excludeQualities != "" {
options.ExcludeQualities = strings.Split(excludeQualities, ",")
}
// HDR
if hdr := r.URL.Query().Get("hdr"); hdr != "" {
if hdrBool, err := strconv.ParseBool(hdr); err == nil {
options.HDR = &hdrBool
}
}
// HEVC
if hevc := r.URL.Query().Get("hevc"); hevc != "" {
if hevcBool, err := strconv.ParseBool(hevc); err == nil {
options.HEVC = &hevcBool
}
}
// Сортировка
options.SortBy = r.URL.Query().Get("sortBy")
if options.SortBy == "" {
options.SortBy = "seeders"
}
options.SortOrder = r.URL.Query().Get("sortOrder")
if options.SortOrder == "" {
options.SortOrder = "desc"
}
// Группировка
if groupByQuality := r.URL.Query().Get("groupByQuality"); groupByQuality == "true" {
options.GroupByQuality = true
}
if groupBySeason := r.URL.Query().Get("groupBySeason"); groupBySeason == "true" {
options.GroupBySeason = true
}
// Сезон для сериалов
if season := r.URL.Query().Get("season"); season != "" {
if seasonInt, err := strconv.Atoi(season); err == nil {
options.Season = &seasonInt
}
}
// Поиск торрентов
results, err := h.torrentService.SearchTorrentsByIMDbID(h.tmdbService, imdbID, mediaType, options)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Формируем ответ с группировкой если необходимо
response := map[string]interface{}{
"imdbId": imdbID,
"type": mediaType,
"total": results.Total,
}
if options.Season != nil {
response["season"] = *options.Season
}
// Применяем группировку если запрошена
if options.GroupByQuality && options.GroupBySeason {
// Группируем сначала по сезонам, затем по качеству внутри каждого сезона
seasonGroups := h.torrentService.GroupBySeason(results.Results)
finalGroups := make(map[string]map[string][]models.TorrentResult)
for season, torrents := range seasonGroups {
qualityGroups := h.torrentService.GroupByQuality(torrents)
finalGroups[season] = qualityGroups
}
response["grouped"] = true
response["groups"] = finalGroups
} else if options.GroupByQuality {
groups := h.torrentService.GroupByQuality(results.Results)
response["grouped"] = true
response["groups"] = groups
} else if options.GroupBySeason {
groups := h.torrentService.GroupBySeason(results.Results)
response["grouped"] = true
response["groups"] = groups
} else {
response["grouped"] = false
response["results"] = results.Results
}
if len(results.Results) == 0 {
response["error"] = "No torrents found for this IMDB ID"
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(response)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}
// SearchMovies - поиск фильмов по названию
func (h *TorrentsHandler) SearchMovies(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest)
return
}
results, err := h.torrentService.SearchMovies(title, originalTitle, year)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
response := map[string]interface{}{
"title": title,
"originalTitle": originalTitle,
"year": year,
"type": "movie",
"total": results.Total,
"results": results.Results,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}
// SearchSeries - поиск сериалов по названию с поддержкой сезонов
func (h *TorrentsHandler) SearchSeries(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest)
return
}
var season *int
if seasonStr := r.URL.Query().Get("season"); seasonStr != "" {
@@ -365,3 +365,57 @@ func (h *TorrentsHandler) SearchByQuery(w http.ResponseWriter, r *http.Request)
Data: response,
})
}
// SearchByTitle - поиск торрентов по названию фильма/сериала (новый метод)
func (h *TorrentsHandler) SearchByTitle(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle")
yearStr := r.URL.Query().Get("year")
contentType := r.URL.Query().Get("type")
if title == "" && originalTitle == "" {
http.Error(w, "Title or originalTitle is required", http.StatusBadRequest)
return
}
if contentType == "" {
contentType = "movie"
}
// Парсим год
year := 0
if yearStr != "" {
if y, err := strconv.Atoi(yearStr); err == nil {
year = y
}
}
// Определяем является ли это сериалом
isSerial := contentType == "series" || contentType == "tv"
// Поиск торрентов
results, err := h.torrentService.SearchTorrentsByTitle(title, originalTitle, year, isSerial)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Применяем фильтрацию по типу контента
results.Results = h.torrentService.FilterByContentType(results.Results, contentType)
results.Total = len(results.Results)
response := map[string]interface{}{
"title": title,
"originalTitle": originalTitle,
"year": year,
"type": contentType,
"total": results.Total,
"results": results.Results,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{
Success: true,
Data: response,
})
}

View File

@@ -266,6 +266,7 @@ type TorrentSearchResponse struct {
// RedAPI специфичные структуры
type RedAPIResponse struct {
Results []RedAPITorrent `json:"Results"`
Jacred bool `json:"jacred,omitempty"` // Флаг для определения источника
}
type RedAPITorrent struct {
@@ -277,15 +278,22 @@ type RedAPITorrent struct {
MagnetUri string `json:"MagnetUri"`
PublishDate string `json:"PublishDate"`
CategoryDesc string `json:"CategoryDesc"`
Category []int `json:"Category,omitempty"` // Для jacred
Details string `json:"Details"`
Info *RedAPITorrentInfo `json:"Info,omitempty"`
Languages []string `json:"languages,omitempty"` // Для jacred
FFprobe []interface{} `json:"ffprobe,omitempty"` // Для jacred
}
type RedAPITorrentInfo struct {
Quality interface{} `json:"quality,omitempty"` // Может быть string или number
Voices []string `json:"voices,omitempty"`
Types []string `json:"types,omitempty"`
Seasons []int `json:"seasons,omitempty"`
Quality interface{} `json:"quality,omitempty"` // Может быть string или number
VideoType string `json:"videotype,omitempty"`
Voices []string `json:"voices,omitempty"`
Types []string `json:"types,omitempty"`
Seasons []int `json:"seasons,omitempty"`
Name string `json:"name,omitempty"`
OriginalName string `json:"originalname,omitempty"`
Released int `json:"relased,omitempty"` // Примечание: опечатка в API
}
// Alloha API структуры для получения информации о фильмах

View File

@@ -82,11 +82,70 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
}, nil
}
// SearchTorrentsByTitle - поиск торрентов по названию фильма/сериала
func (s *TorrentService) SearchTorrentsByTitle(title, originalTitle string, year int, isSerial bool) (*models.TorrentSearchResponse, error) {
searchParams := url.Values{}
// Добавляем основное название
if title != "" {
searchParams.Add("title", title)
}
// Добавляем оригинальное название
if originalTitle != "" {
searchParams.Add("title_original", originalTitle)
}
// Добавляем год
if year > 0 {
searchParams.Add("year", strconv.Itoa(year))
}
// Указываем тип контента
if isSerial {
searchParams.Add("is_serial", "2")
} else {
searchParams.Add("is_serial", "1")
}
if s.apiKey != "" {
searchParams.Add("apikey", s.apiKey)
}
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?%s", s.baseURL, searchParams.Encode())
resp, err := s.client.Get(searchURL)
if err != nil {
return nil, fmt.Errorf("failed to search torrents by title: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
var redAPIResponse models.RedAPIResponse
if err := json.Unmarshal(body, &redAPIResponse); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
results := s.parseRedAPIResults(redAPIResponse)
return &models.TorrentSearchResponse{
Query: title,
Results: results,
Total: len(results),
}, nil
}
// parseRedAPIResults преобразует результаты RedAPI в наш формат
func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult {
var results []models.TorrentResult
for _, torrent := range data.Results {
for i, torrent := range data.Results {
_ = i // для избежания неиспользуемой переменной
var sizeStr string
switch v := torrent.Size.(type) {
case string:
@@ -112,14 +171,27 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
Source: "RedAPI",
}
// Обработка Info для обоих форматов (RedAPI и jacred)
if torrent.Info != nil {
switch v := torrent.Info.Quality.(type) {
case string:
result.Quality = v
case float64:
result.Quality = fmt.Sprintf("%.0fp", v)
case int:
result.Quality = fmt.Sprintf("%dp", v)
// Обработка качества
if torrent.Info.Quality != nil {
switch v := torrent.Info.Quality.(type) {
case string:
result.Quality = v
case float64:
// Для jacred качество приходит как число (1080, 720 и т.д.)
if v >= 2160 {
result.Quality = "4K"
} else {
result.Quality = fmt.Sprintf("%.0fp", v)
}
case int:
if v >= 2160 {
result.Quality = "4K"
} else {
result.Quality = fmt.Sprintf("%dp", v)
}
}
}
result.Voice = torrent.Info.Voices
@@ -127,10 +199,10 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
result.Seasons = torrent.Info.Seasons
}
// Если качество все еще не определено, извлекаем из названия
if result.Quality == "" {
result.Quality = s.ExtractQuality(result.Title)
}
results = append(results, result)
}