mirror of
https://gitlab.com/foxixus/neomovies-api.git
synced 2025-12-15 20:16:10 +05:00
v3.0.1
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
18
main.go
@@ -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
1988
neomovies-api.log
Normal file
File diff suppressed because one or more lines are too long
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 структуры для получения информации о фильмах
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user