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,7 +1,6 @@
package config package config
import ( import (
"log"
"os" "os"
) )
@@ -28,6 +27,10 @@ type Config struct {
KPAPIKey string KPAPIKey string
HDVBToken string HDVBToken string
KPAPIBaseURL string KPAPIBaseURL string
CollapsAPIHost string
CollapsToken string
VeoVeoHost string
VeoVeoToken string
} }
func New() *Config { func New() *Config {
@@ -52,21 +55,22 @@ func New() *Config {
GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""), GoogleRedirectURL: getEnv(EnvGoogleRedirectURL, ""),
FrontendURL: getEnv(EnvFrontendURL, ""), FrontendURL: getEnv(EnvFrontendURL, ""),
VibixHost: getEnv(EnvVibixHost, DefaultVibixHost), VibixHost: getEnv(EnvVibixHost, DefaultVibixHost),
VibixToken: getEnv(EnvVibixToken, ""),
KPAPIKey: getEnv(EnvKPAPIKey, ""), KPAPIKey: getEnv(EnvKPAPIKey, ""),
HDVBToken: getEnv(EnvHDVBToken, ""), HDVBToken: getEnv(EnvHDVBToken, ""),
KPAPIBaseURL: getEnv("KPAPI_BASE_URL", DefaultKPAPIBase), 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 { func getMongoURI() string {
for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} { for _, envVar := range []string{"MONGO_URI", "MONGODB_URI", "DATABASE_URL", "MONGO_URL"} {
if value := os.Getenv(envVar); value != "" { if value := os.Getenv(envVar); value != "" {
log.Printf("DEBUG: Using %s for MongoDB connection", envVar)
return value return value
} }
} }
log.Printf("DEBUG: No MongoDB URI environment variable found")
return "" return ""
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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)
} }