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_BASE_URL=http://redapi.cfhttp.top
REDAPI_KEY=your_redapi_key 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 JWT_SECRET=your_jwt_secret_key
GMAIL_USER=your_gmail@gmail.com 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 GOOGLE_REDIRECT_URL=http://localhost:3000/api/v1/auth/google/callback
BASE_URL=http://localhost:3000 BASE_URL=http://localhost:3000
FRONTEND_URL=http://localhost:3001 FRONTEND_URL=http://localhost:5173
PORT=3000 PORT=3000
NODE_ENV=development NODE_ENV=development

View File

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

18
main.go
View File

@@ -2,6 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"io"
"log"
"net/http" "net/http"
"os" "os"
@@ -18,6 +20,18 @@ import (
) )
func main() { 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 { if err := godotenv.Load(); err != nil {
_ = err _ = err
} }
@@ -84,13 +98,15 @@ func main() {
api.HandleFunc("/players/vidlink/movie/{imdb_id}", playersHandler.GetVidlinkMoviePlayer).Methods("GET") 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/vidlink/tv/{tmdb_id}", playersHandler.GetVidlinkTVPlayer).Methods("GET")
api.HandleFunc("/players/hdvb/{id_type}/{id}", playersHandler.GetHDVBPlayer).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/search/{imdbId}", torrentsHandler.SearchTorrents).Methods("GET")
api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET") api.HandleFunc("/torrents/movies", torrentsHandler.SearchMovies).Methods("GET")
api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET") api.HandleFunc("/torrents/series", torrentsHandler.SearchSeries).Methods("GET")
api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET") api.HandleFunc("/torrents/anime", torrentsHandler.SearchAnime).Methods("GET")
api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET") api.HandleFunc("/torrents/seasons", torrentsHandler.GetAvailableSeasons).Methods("GET")
api.HandleFunc("/torrents/search", torrentsHandler.SearchByQuery).Methods("GET")
api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET") api.HandleFunc("/reactions/{mediaType}/{mediaId}/counts", reactionsHandler.GetReactionCounts).Methods("GET")

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 package config
import ( import (
"log" "os"
"os" )
)
type Config struct {
type Config struct { MongoURI string
MongoURI string MongoDBName string
MongoDBName string TMDBAccessToken string
TMDBAccessToken string JWTSecret string
JWTSecret string Port string
Port string BaseURL string
BaseURL string NodeEnv string
NodeEnv string GmailUser string
GmailUser string GmailPassword string
GmailPassword string LumexURL string
LumexURL string AllohaToken string
AllohaToken string RedAPIBaseURL string
RedAPIBaseURL string RedAPIKey string
RedAPIKey string GoogleClientID string
GoogleClientID string GoogleClientSecret string
GoogleClientSecret string GoogleRedirectURL string
GoogleRedirectURL string FrontendURL string
FrontendURL string VibixHost string
VibixHost string VibixToken string
VibixToken string KPAPIKey string
KPAPIKey string HDVBToken string
HDVBToken string KPAPIBaseURL string
KPAPIBaseURL string CollapsAPIHost string
} CollapsToken string
VeoVeoHost string
func New() *Config { VeoVeoToken string
mongoURI := getMongoURI() }
return &Config{ func New() *Config {
MongoURI: mongoURI, mongoURI := getMongoURI()
MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName),
TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""), return &Config{
JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret), MongoURI: mongoURI,
Port: getEnv(EnvPort, DefaultPort), MongoDBName: getEnv(EnvMongoDBName, DefaultMongoDBName),
BaseURL: getEnv(EnvBaseURL, DefaultBaseURL), TMDBAccessToken: getEnv(EnvTMDBAccessToken, ""),
NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv), JWTSecret: getEnv(EnvJWTSecret, DefaultJWTSecret),
GmailUser: getEnv(EnvGmailUser, ""), Port: getEnv(EnvPort, DefaultPort),
GmailPassword: getEnv(EnvGmailPassword, ""), BaseURL: getEnv(EnvBaseURL, DefaultBaseURL),
LumexURL: getEnv(EnvLumexURL, ""), NodeEnv: getEnv(EnvNodeEnv, DefaultNodeEnv),
AllohaToken: getEnv(EnvAllohaToken, ""), GmailUser: getEnv(EnvGmailUser, ""),
RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase), GmailPassword: getEnv(EnvGmailPassword, ""),
RedAPIKey: getEnv(EnvRedAPIKey, ""), LumexURL: getEnv(EnvLumexURL, ""),
GoogleClientID: getEnv(EnvGoogleClientID, ""), AllohaToken: getEnv(EnvAllohaToken, ""),
GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""), RedAPIBaseURL: getEnv(EnvRedAPIBaseURL, DefaultRedAPIBase),
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""), RedAPIKey: getEnv(EnvRedAPIKey, ""),
FrontendURL: getEnv(EnvFrontendURL, ""), GoogleClientID: getEnv(EnvGoogleClientID, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost), GoogleClientSecret: getEnv(EnvGoogleClientSecret, ""),
VibixToken: getEnv(EnvVibixToken, ""), GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
KPAPIKey: getEnv(EnvKPAPIKey, ""), FrontendURL: getEnv(EnvFrontendURL, ""),
HDVBToken: getEnv(EnvHDVBToken, ""), VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase), KPAPIKey: getEnv(EnvKPAPIKey, ""),
} HDVBToken: getEnv(EnvHDVBToken, ""),
} KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase),
CollapsAPIHost: getEnv("COLLAPS_API_HOST", ""),
func getMongoURI() string { CollapsToken: getEnv("COLLAPS_TOKEN", ""),
for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} { VeoVeoHost: getEnv("VEOVEO_HOST", ""),
if value := os.Getenv(envVar); value != "" { VeoVeoToken: getEnv("VEOVEO_TOKEN", ""),
log.Printf("DEBUG: Using %s for MongoDB connection", envVar) }
return value }
}
} func getMongoURI() string {
log.Printf("DEBUG: No MongoDB URI environment variable found") for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} {
return "" if value := os.Getenv(envVar); value != "" {
} return value
}
func getEnv(key, defaultValue string) string { }
if value := os.Getenv(key); value != "" { return ""
return value }
}
return defaultValue 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 package handlers
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"neomovies-api/pkg/models" "neomovies-api/pkg/models"
"neomovies-api/pkg/services" "neomovies-api/pkg/services"
) )
type TorrentsHandler struct { type TorrentsHandler struct {
torrentService *services.TorrentService torrentService *services.TorrentService
tmdbService *services.TMDBService tmdbService *services.TMDBService
} }
func NewTorrentsHandler(torrentService *services.TorrentService, tmdbService *services.TMDBService) *TorrentsHandler { func NewTorrentsHandler(torrentService *services.TorrentService, tmdbService *services.TMDBService) *TorrentsHandler {
return &TorrentsHandler{ return &TorrentsHandler{
torrentService: torrentService, torrentService: torrentService,
tmdbService: tmdbService, tmdbService: tmdbService,
} }
} }
// SearchTorrents - поиск торрентов по IMDB ID // SearchTorrents - поиск торрентов по IMDB ID
func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request) { func (h *TorrentsHandler) SearchTorrents(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
imdbID := vars["imdbId"] imdbID := vars["imdbId"]
if imdbID == "" { if imdbID == "" {
http.Error(w, "IMDB ID is required", http.StatusBadRequest) http.Error(w, "IMDB ID is required", http.StatusBadRequest)
return return
} }
// Параметры запроса // Параметры запроса
mediaType := r.URL.Query().Get("type") mediaType := r.URL.Query().Get("type")
if mediaType == "" { if mediaType == "" {
mediaType = "movie" mediaType = "movie"
} }
// Создаем опции поиска // Создаем опции поиска
options := &models.TorrentSearchOptions{ options := &models.TorrentSearchOptions{
ContentType: mediaType, ContentType: mediaType,
} }
// Качество // Качество
if quality := r.URL.Query().Get("quality"); quality != "" { if quality := r.URL.Query().Get("quality"); quality != "" {
options.Quality = strings.Split(quality, ",") options.Quality = strings.Split(quality, ",")
} }
// Минимальное и максимальное качество // Минимальное и максимальное качество
options.MinQuality = r.URL.Query().Get("minQuality") options.MinQuality = r.URL.Query().Get("minQuality")
options.MaxQuality = r.URL.Query().Get("maxQuality") options.MaxQuality = r.URL.Query().Get("maxQuality")
// Исключаемые качества // Исключаемые качества
if excludeQualities := r.URL.Query().Get("excludeQualities"); excludeQualities != "" { if excludeQualities := r.URL.Query().Get("excludeQualities"); excludeQualities != "" {
options.ExcludeQualities = strings.Split(excludeQualities, ",") options.ExcludeQualities = strings.Split(excludeQualities, ",")
} }
// HDR // HDR
if hdr := r.URL.Query().Get("hdr"); hdr != "" { if hdr := r.URL.Query().Get("hdr"); hdr != "" {
if hdrBool, err := strconv.ParseBool(hdr); err == nil { if hdrBool, err := strconv.ParseBool(hdr); err == nil {
options.HDR = &hdrBool options.HDR = &hdrBool
} }
} }
// HEVC // HEVC
if hevc := r.URL.Query().Get("hevc"); hevc != "" { if hevc := r.URL.Query().Get("hevc"); hevc != "" {
if hevcBool, err := strconv.ParseBool(hevc); err == nil { if hevcBool, err := strconv.ParseBool(hevc); err == nil {
options.HEVC = &hevcBool options.HEVC = &hevcBool
} }
} }
// Сортировка // Сортировка
options.SortBy = r.URL.Query().Get("sortBy") options.SortBy = r.URL.Query().Get("sortBy")
if options.SortBy == "" { if options.SortBy == "" {
options.SortBy = "seeders" options.SortBy = "seeders"
} }
options.SortOrder = r.URL.Query().Get("sortOrder") options.SortOrder = r.URL.Query().Get("sortOrder")
if options.SortOrder == "" { if options.SortOrder == "" {
options.SortOrder = "desc" options.SortOrder = "desc"
} }
// Группировка // Группировка
if groupByQuality := r.URL.Query().Get("groupByQuality"); groupByQuality == "true" { if groupByQuality := r.URL.Query().Get("groupByQuality"); groupByQuality == "true" {
options.GroupByQuality = true options.GroupByQuality = true
} }
if groupBySeason := r.URL.Query().Get("groupBySeason"); groupBySeason == "true" { if groupBySeason := r.URL.Query().Get("groupBySeason"); groupBySeason == "true" {
options.GroupBySeason = true options.GroupBySeason = true
} }
// Сезон для сериалов // Сезон для сериалов
if season := r.URL.Query().Get("season"); season != "" { if season := r.URL.Query().Get("season"); season != "" {
if seasonInt, err := strconv.Atoi(season); err == nil { if seasonInt, err := strconv.Atoi(season); err == nil {
options.Season = &seasonInt options.Season = &seasonInt
} }
} }
// Поиск торрентов // Поиск торрентов
results, err := h.torrentService.SearchTorrentsByIMDbID(h.tmdbService, imdbID, mediaType, options) results, err := h.torrentService.SearchTorrentsByIMDbID(h.tmdbService, imdbID, mediaType, options)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Формируем ответ с группировкой если необходимо // Формируем ответ с группировкой если необходимо
response := map[string]interface{}{ response := map[string]interface{}{
"imdbId": imdbID, "imdbId": imdbID,
"type": mediaType, "type": mediaType,
"total": results.Total, "total": results.Total,
} }
if options.Season != nil { if options.Season != nil {
response["season"] = *options.Season response["season"] = *options.Season
} }
// Применяем группировку если запрошена // Применяем группировку если запрошена
if options.GroupByQuality && options.GroupBySeason { if options.GroupByQuality && options.GroupBySeason {
// Группируем сначала по сезонам, затем по качеству внутри каждого сезона // Группируем сначала по сезонам, затем по качеству внутри каждого сезона
seasonGroups := h.torrentService.GroupBySeason(results.Results) seasonGroups := h.torrentService.GroupBySeason(results.Results)
finalGroups := make(map[string]map[string][]models.TorrentResult) finalGroups := make(map[string]map[string][]models.TorrentResult)
for season, torrents := range seasonGroups { for season, torrents := range seasonGroups {
qualityGroups := h.torrentService.GroupByQuality(torrents) qualityGroups := h.torrentService.GroupByQuality(torrents)
finalGroups[season] = qualityGroups finalGroups[season] = qualityGroups
} }
response["grouped"] = true response["grouped"] = true
response["groups"] = finalGroups response["groups"] = finalGroups
} else if options.GroupByQuality { } else if options.GroupByQuality {
groups := h.torrentService.GroupByQuality(results.Results) groups := h.torrentService.GroupByQuality(results.Results)
response["grouped"] = true response["grouped"] = true
response["groups"] = groups response["groups"] = groups
} else if options.GroupBySeason { } else if options.GroupBySeason {
groups := h.torrentService.GroupBySeason(results.Results) groups := h.torrentService.GroupBySeason(results.Results)
response["grouped"] = true response["grouped"] = true
response["groups"] = groups response["groups"] = groups
} else { } else {
response["grouped"] = false response["grouped"] = false
response["results"] = results.Results response["results"] = results.Results
} }
if len(results.Results) == 0 { if len(results.Results) == 0 {
response["error"] = "No torrents found for this IMDB ID" response["error"] = "No torrents found for this IMDB ID"
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
return return
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{ json.NewEncoder(w).Encode(models.APIResponse{
Success: true, Success: true,
Data: response, Data: response,
}) })
} }
// SearchMovies - поиск фильмов по названию // SearchMovies - поиск фильмов по названию
func (h *TorrentsHandler) SearchMovies(w http.ResponseWriter, r *http.Request) { func (h *TorrentsHandler) SearchMovies(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title") title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle") originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year") year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" { if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest) http.Error(w, "Title or original title is required", http.StatusBadRequest)
return return
} }
results, err := h.torrentService.SearchMovies(title, originalTitle, year) results, err := h.torrentService.SearchMovies(title, originalTitle, year)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
response := map[string]interface{}{ response := map[string]interface{}{
"title": title, "title": title,
"originalTitle": originalTitle, "originalTitle": originalTitle,
"year": year, "year": year,
"type": "movie", "type": "movie",
"total": results.Total, "total": results.Total,
"results": results.Results, "results": results.Results,
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(models.APIResponse{ json.NewEncoder(w).Encode(models.APIResponse{
Success: true, Success: true,
Data: response, Data: response,
}) })
} }
// SearchSeries - поиск сериалов по названию с поддержкой сезонов // SearchSeries - поиск сериалов по названию с поддержкой сезонов
func (h *TorrentsHandler) SearchSeries(w http.ResponseWriter, r *http.Request) { func (h *TorrentsHandler) SearchSeries(w http.ResponseWriter, r *http.Request) {
title := r.URL.Query().Get("title") title := r.URL.Query().Get("title")
originalTitle := r.URL.Query().Get("originalTitle") originalTitle := r.URL.Query().Get("originalTitle")
year := r.URL.Query().Get("year") year := r.URL.Query().Get("year")
if title == "" && originalTitle == "" { if title == "" && originalTitle == "" {
http.Error(w, "Title or original title is required", http.StatusBadRequest) http.Error(w, "Title or original title is required", http.StatusBadRequest)
return return
} }
var season *int var season *int
if seasonStr := r.URL.Query().Get("season"); seasonStr != "" { if seasonStr := r.URL.Query().Get("season"); seasonStr != "" {
@@ -365,3 +365,57 @@ func (h *TorrentsHandler) SearchByQuery(w http.ResponseWriter, r *http.Request)
Data: response, 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 специфичные структуры // RedAPI специфичные структуры
type RedAPIResponse struct { type RedAPIResponse struct {
Results []RedAPITorrent `json:"Results"` Results []RedAPITorrent `json:"Results"`
Jacred bool `json:"jacred,omitempty"` // Флаг для определения источника
} }
type RedAPITorrent struct { type RedAPITorrent struct {
@@ -277,15 +278,22 @@ type RedAPITorrent struct {
MagnetUri string `json:"MagnetUri"` MagnetUri string `json:"MagnetUri"`
PublishDate string `json:"PublishDate"` PublishDate string `json:"PublishDate"`
CategoryDesc string `json:"CategoryDesc"` CategoryDesc string `json:"CategoryDesc"`
Category []int `json:"Category,omitempty"` // Для jacred
Details string `json:"Details"` Details string `json:"Details"`
Info *RedAPITorrentInfo `json:"Info,omitempty"` Info *RedAPITorrentInfo `json:"Info,omitempty"`
Languages []string `json:"languages,omitempty"` // Для jacred
FFprobe []interface{} `json:"ffprobe,omitempty"` // Для jacred
} }
type RedAPITorrentInfo struct { type RedAPITorrentInfo struct {
Quality interface{} `json:"quality,omitempty"` // Может быть string или number Quality interface{} `json:"quality,omitempty"` // Может быть string или number
Voices []string `json:"voices,omitempty"` VideoType string `json:"videotype,omitempty"`
Types []string `json:"types,omitempty"` Voices []string `json:"voices,omitempty"`
Seasons []int `json:"seasons,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 структуры для получения информации о фильмах // Alloha API структуры для получения информации о фильмах

View File

@@ -82,11 +82,70 @@ func (s *TorrentService) SearchTorrents(params map[string]string) (*models.Torre
}, nil }, 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 в наш формат // 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 i, torrent := range data.Results {
_ = i // для избежания неиспользуемой переменной
var sizeStr string var sizeStr string
switch v := torrent.Size.(type) { switch v := torrent.Size.(type) {
case string: case string:
@@ -112,14 +171,27 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
Source: "RedAPI", Source: "RedAPI",
} }
// Обработка Info для обоих форматов (RedAPI и jacred)
if torrent.Info != nil { if torrent.Info != nil {
switch v := torrent.Info.Quality.(type) { // Обработка качества
case string: if torrent.Info.Quality != nil {
result.Quality = v switch v := torrent.Info.Quality.(type) {
case float64: case string:
result.Quality = fmt.Sprintf("%.0fp", v) result.Quality = v
case int: case float64:
result.Quality = fmt.Sprintf("%dp", v) // Для 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 result.Voice = torrent.Info.Voices
@@ -127,10 +199,10 @@ func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models
result.Seasons = torrent.Info.Seasons result.Seasons = torrent.Info.Seasons
} }
// Если качество все еще не определено, извлекаем из названия
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)
} }