Files
neomovies-api/pkg/services/torrent.go

936 lines
26 KiB
Go
Raw Permalink Normal View History

2025-08-07 13:47:42 +00:00
package services
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"sort"
"strconv"
"strings"
"time"
"neomovies-api/pkg/models"
)
type TorrentService struct {
client *http.Client
baseURL string
apiKey string
}
2025-08-08 16:47:02 +00:00
func NewTorrentServiceWithConfig(baseURL, apiKey string) *TorrentService {
return &TorrentService{
client: &http.Client{Timeout: 8 * time.Second},
baseURL: baseURL,
apiKey: apiKey,
}
}
2025-08-07 13:47:42 +00:00
func NewTorrentService() *TorrentService {
return &TorrentService{
client: &http.Client{Timeout: 8 * time.Second},
baseURL: "http://redapi.cfhttp.top",
2025-08-08 16:47:02 +00:00
apiKey: "",
2025-08-07 13:47:42 +00:00
}
}
// SearchTorrents - основной метод поиска торрентов через RedAPI
func (s *TorrentService) SearchTorrents(params map[string]string) (*models.TorrentSearchResponse, error) {
searchParams := url.Values{}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for key, value := range params {
if value != "" {
if key == "category" {
searchParams.Add("category[]", value)
} else {
searchParams.Add(key, value)
}
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
if s.apiKey != "" {
searchParams.Add("apikey", s.apiKey)
}
searchURL := fmt.Sprintf("%s/api/v2.0/indexers/all/results?%s", s.baseURL, searchParams.Encode())
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
resp, err := s.client.Get(searchURL)
if err != nil {
return nil, fmt.Errorf("failed to search torrents: %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)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return &models.TorrentSearchResponse{
Query: params["query"],
Results: results,
Total: len(results),
}, nil
}
// parseRedAPIResults преобразует результаты RedAPI в наш формат
func (s *TorrentService) parseRedAPIResults(data models.RedAPIResponse) []models.TorrentResult {
var results []models.TorrentResult
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for _, torrent := range data.Results {
var sizeStr string
switch v := torrent.Size.(type) {
case string:
sizeStr = v
case float64:
sizeStr = fmt.Sprintf("%.0f", v)
case int:
sizeStr = fmt.Sprintf("%d", v)
default:
sizeStr = ""
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
result := models.TorrentResult{
Title: torrent.Title,
Tracker: torrent.Tracker,
Size: sizeStr,
Seeders: torrent.Seeders,
Peers: torrent.Peers,
MagnetLink: torrent.MagnetUri,
PublishDate: torrent.PublishDate,
Category: torrent.CategoryDesc,
Details: torrent.Details,
Source: "RedAPI",
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
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)
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
result.Voice = torrent.Info.Voices
result.Types = torrent.Info.Types
result.Seasons = torrent.Info.Seasons
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
if result.Quality == "" {
result.Quality = s.ExtractQuality(result.Title)
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
results = append(results, result)
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return results
}
// SearchTorrentsByIMDbID - поиск по IMDB ID с поддержкой всех функций
func (s *TorrentService) SearchTorrentsByIMDbID(tmdbService *TMDBService, imdbID, mediaType string, options *models.TorrentSearchOptions) (*models.TorrentSearchResponse, error) {
title, originalTitle, year, err := s.getTitleFromTMDB(tmdbService, imdbID, mediaType)
if err != nil {
return nil, fmt.Errorf("failed to get title from TMDB: %w", err)
}
2025-08-07 18:25:43 +00:00
params := map[string]string{
"imdb": imdbID,
"query": title,
"title_original": originalTitle,
"year": year,
}
2025-08-07 13:47:42 +00:00
switch mediaType {
case "movie":
params["is_serial"] = "1"
params["category"] = "2000"
2025-08-07 18:25:43 +00:00
case "serial", "series", "tv":
2025-08-07 13:47:42 +00:00
params["is_serial"] = "2"
params["category"] = "5000"
case "anime":
params["is_serial"] = "5"
params["category"] = "5070"
}
2025-08-07 18:25:43 +00:00
if options != nil && options.Season != nil && *options.Season > 0 {
2025-08-07 13:47:42 +00:00
params["season"] = strconv.Itoa(*options.Season)
}
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
if options != nil {
response.Results = s.FilterByContentType(response.Results, options.ContentType)
response.Results = s.FilterTorrents(response.Results, options)
response.Results = s.sortTorrents(response.Results, options.SortBy, options.SortOrder)
2025-08-07 18:25:43 +00:00
}
response.Total = len(response.Results)
if len(response.Results) < 5 && (mediaType == "serial" || mediaType == "series" || mediaType == "tv") && options != nil && options.Season != nil {
paramsNoSeason := map[string]string{
"imdb": imdbID,
"query": title,
"title_original": originalTitle,
"year": year,
"is_serial": "2",
"category": "5000",
}
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
if err == nil {
filtered := s.filterBySeason(fallbackResp.Results, *options.Season)
all := append(response.Results, filtered...)
unique := make([]models.TorrentResult, 0, len(all))
seen := make(map[string]bool)
for _, t := range all {
if !seen[t.MagnetLink] {
unique = append(unique, t)
seen[t.MagnetLink] = true
}
}
response.Results = unique
}
2025-08-07 13:47:42 +00:00
}
return response, nil
}
// SearchMovies - поиск фильмов с дополнительной фильтрацией
func (s *TorrentService) SearchMovies(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
params := map[string]string{
"title": title,
"title_original": originalTitle,
"year": year,
"is_serial": "1",
"category": "2000",
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
response.Results = s.FilterByContentType(response.Results, "movie")
response.Total = len(response.Results)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return response, nil
}
// SearchSeries - поиск сериалов с поддержкой fallback и фильтрации по сезону
func (s *TorrentService) SearchSeries(title, originalTitle, year string, season *int) (*models.TorrentSearchResponse, error) {
params := map[string]string{
"title": title,
"title_original": originalTitle,
"year": year,
"is_serial": "2",
"category": "5000",
}
if season != nil {
params["season"] = strconv.Itoa(*season)
}
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
// Если указан сезон и результатов мало, делаем fallback-поиск без сезона и фильтруем на клиенте
if season != nil && len(response.Results) < 5 {
paramsNoSeason := map[string]string{
"title": title,
"title_original": originalTitle,
"year": year,
"is_serial": "2",
"category": "5000",
}
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
if err == nil {
filtered := s.filterBySeason(fallbackResp.Results, *season)
// Объединяем и убираем дубликаты по MagnetLink
all := append(response.Results, filtered...)
unique := make([]models.TorrentResult, 0, len(all))
seen := make(map[string]bool)
for _, t := range all {
if !seen[t.MagnetLink] {
unique = append(unique, t)
seen[t.MagnetLink] = true
}
}
response.Results = unique
}
}
response.Results = s.FilterByContentType(response.Results, "serial")
response.Total = len(response.Results)
return response, nil
}
// filterBySeason - фильтрация результатов по сезону (аналогично JS)
func (s *TorrentService) filterBySeason(results []models.TorrentResult, season int) []models.TorrentResult {
if season == 0 {
return results
}
filtered := make([]models.TorrentResult, 0, len(results))
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
for _, torrent := range results {
found := false
// Проверяем поле seasons
for _, s := range torrent.Seasons {
if s == season {
found = true
break
}
}
if found {
filtered = append(filtered, torrent)
continue
}
// Проверяем в названии
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
for _, match := range matches {
seasonNumber := 0
if match[1] != "" {
seasonNumber, _ = strconv.Atoi(match[1])
} else if match[2] != "" {
seasonNumber, _ = strconv.Atoi(match[2])
}
if seasonNumber == season {
filtered = append(filtered, torrent)
break
}
}
}
return filtered
}
// SearchAnime - поиск аниме
func (s *TorrentService) SearchAnime(title, originalTitle, year string) (*models.TorrentSearchResponse, error) {
params := map[string]string{
"title": title,
"title_original": originalTitle,
"year": year,
"is_serial": "5",
"category": "5070",
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
response.Results = s.FilterByContentType(response.Results, "anime")
response.Total = len(response.Results)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return response, nil
}
// AllohaResponse - структура ответа от Alloha API
type AllohaResponse struct {
Status string `json:"status"`
Data struct {
Name string `json:"name"`
OriginalName string `json:"original_name"`
Year int `json:"year"`
Category int `json:"category"` // 1-фильм, 2-сериал
} `json:"data"`
}
// getMovieInfoByIMDB - получение информации через Alloha API (как в JavaScript версии)
func (s *TorrentService) getMovieInfoByIMDB(imdbID string) (string, string, string, error) {
// Используем тот же токен что и в JavaScript версии
endpoint := fmt.Sprintf("https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1&imdb=%s", imdbID)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return "", "", "", err
}
resp, err := s.client.Do(req)
if err != nil {
return "", "", "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", err
}
var allohaResponse AllohaResponse
if err := json.Unmarshal(body, &allohaResponse); err != nil {
return "", "", "", err
}
if allohaResponse.Status != "success" {
return "", "", "", fmt.Errorf("no results found for IMDB ID: %s", imdbID)
}
title := allohaResponse.Data.Name
originalTitle := allohaResponse.Data.OriginalName
year := ""
if allohaResponse.Data.Year > 0 {
year = strconv.Itoa(allohaResponse.Data.Year)
}
return title, originalTitle, year, nil
}
// getTitleFromTMDB - получение информации из TMDB (с fallback на Alloha API)
func (s *TorrentService) getTitleFromTMDB(tmdbService *TMDBService, imdbID, mediaType string) (string, string, string, error) {
// Сначала пробуем Alloha API (как в JavaScript версии)
title, originalTitle, year, err := s.getMovieInfoByIMDB(imdbID)
if err == nil {
return title, originalTitle, year, nil
}
// Если Alloha API не работает, пробуем TMDB API
endpoint := fmt.Sprintf("https://api.themoviedb.org/3/find/%s", imdbID)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
req, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return "", "", "", err
}
params := url.Values{}
params.Set("external_source", "imdb_id")
params.Set("language", "ru-RU")
req.URL.RawQuery = params.Encode()
req.Header.Set("Authorization", "Bearer "+tmdbService.accessToken)
req.Header.Set("Accept", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return "", "", "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", "", err
}
var findResponse struct {
MovieResults []struct {
Title string `json:"title"`
OriginalTitle string `json:"original_title"`
ReleaseDate string `json:"release_date"`
} `json:"movie_results"`
TVResults []struct {
Name string `json:"name"`
OriginalName string `json:"original_name"`
FirstAirDate string `json:"first_air_date"`
} `json:"tv_results"`
}
if err := json.Unmarshal(body, &findResponse); err != nil {
return "", "", "", err
}
if mediaType == "movie" && len(findResponse.MovieResults) > 0 {
movie := findResponse.MovieResults[0]
title := movie.Title
originalTitle := movie.OriginalTitle
year := ""
if movie.ReleaseDate != "" {
year = movie.ReleaseDate[:4]
}
return title, originalTitle, year, nil
}
if (mediaType == "tv" || mediaType == "series") && len(findResponse.TVResults) > 0 {
tv := findResponse.TVResults[0]
title := tv.Name
originalTitle := tv.OriginalName
year := ""
if tv.FirstAirDate != "" {
year = tv.FirstAirDate[:4]
}
return title, originalTitle, year, nil
}
return "", "", "", fmt.Errorf("no results found for IMDB ID: %s", imdbID)
}
2025-08-07 18:25:43 +00:00
// FilterByContentType - фильтрация по типу контента (как в JS)
2025-08-07 13:47:42 +00:00
func (s *TorrentService) FilterByContentType(results []models.TorrentResult, contentType string) []models.TorrentResult {
if contentType == "" {
return results
}
var filtered []models.TorrentResult
for _, torrent := range results {
// Фильтрация по полю types, если оно есть
if len(torrent.Types) > 0 {
switch contentType {
case "movie":
if s.containsAny(torrent.Types, []string{"movie", "multfilm", "documovie"}) {
filtered = append(filtered, torrent)
}
2025-08-07 18:25:43 +00:00
case "serial", "series", "tv":
2025-08-07 13:47:42 +00:00
if s.containsAny(torrent.Types, []string{"serial", "multserial", "docuserial", "tvshow"}) {
filtered = append(filtered, torrent)
}
case "anime":
if s.contains(torrent.Types, "anime") {
filtered = append(filtered, torrent)
}
}
continue
}
// Фильтрация по названию, если types недоступно
title := strings.ToLower(torrent.Title)
switch contentType {
case "movie":
if !regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
filtered = append(filtered, torrent)
}
2025-08-07 18:25:43 +00:00
case "serial", "series", "tv":
2025-08-07 13:47:42 +00:00
if regexp.MustCompile(`(?i)(сезон|серии|series|season|эпизод)`).MatchString(title) {
filtered = append(filtered, torrent)
}
case "anime":
if torrent.Category == "TV/Anime" || regexp.MustCompile(`(?i)anime`).MatchString(title) {
filtered = append(filtered, torrent)
}
default:
filtered = append(filtered, torrent)
}
}
return filtered
}
// FilterTorrents - фильтрация торрентов по опциям
func (s *TorrentService) FilterTorrents(torrents []models.TorrentResult, options *models.TorrentSearchOptions) []models.TorrentResult {
if options == nil {
return torrents
}
var filtered []models.TorrentResult
for _, torrent := range torrents {
// Фильтрация по качеству
if len(options.Quality) > 0 {
found := false
for _, quality := range options.Quality {
if strings.EqualFold(torrent.Quality, quality) {
found = true
break
}
}
if !found {
continue
}
}
// Фильтрация по минимальному качеству
if options.MinQuality != "" && !s.qualityMeetsMinimum(torrent.Quality, options.MinQuality) {
continue
}
// Фильтрация по максимальному качеству
if options.MaxQuality != "" && !s.qualityMeetsMaximum(torrent.Quality, options.MaxQuality) {
continue
}
// Исключение качеств
if len(options.ExcludeQualities) > 0 {
excluded := false
for _, excludeQuality := range options.ExcludeQualities {
if strings.EqualFold(torrent.Quality, excludeQuality) {
excluded = true
break
}
}
if excluded {
continue
}
}
// Фильтрация по HDR
if options.HDR != nil {
hasHDR := regexp.MustCompile(`(?i)(hdr|dolby.vision|dv)`).MatchString(torrent.Title)
if *options.HDR != hasHDR {
continue
}
}
// Фильтрация по HEVC
if options.HEVC != nil {
hasHEVC := regexp.MustCompile(`(?i)(hevc|h\.265|x265)`).MatchString(torrent.Title)
if *options.HEVC != hasHEVC {
continue
}
}
// Фильтрация по сезону (дополнительная на клиенте)
if options.Season != nil {
if !s.matchesSeason(torrent, *options.Season) {
continue
}
}
filtered = append(filtered, torrent)
}
return filtered
}
// matchesSeason - проверка соответствия сезону
func (s *TorrentService) matchesSeason(torrent models.TorrentResult, season int) bool {
// Проверяем в поле seasons
for _, s := range torrent.Seasons {
if s == season {
return true
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Проверяем в названии
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
for _, match := range matches {
seasonNumber := 0
if match[1] != "" {
seasonNumber, _ = strconv.Atoi(match[1])
} else if match[2] != "" {
seasonNumber, _ = strconv.Atoi(match[2])
}
if seasonNumber == season {
return true
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return false
}
// ExtractQuality - извлечение качества из названия
func (s *TorrentService) ExtractQuality(title string) string {
title = strings.ToUpper(title)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
qualityPatterns := []struct {
pattern string
quality string
}{
{`2160P|4K`, "2160p"},
{`1440P`, "1440p"},
{`1080P`, "1080p"},
{`720P`, "720p"},
{`480P`, "480p"},
{`360P`, "360p"},
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for _, qp := range qualityPatterns {
if matched, _ := regexp.MatchString(qp.pattern, title); matched {
if qp.quality == "2160p" {
return "4K"
}
return qp.quality
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return "Unknown"
}
// sortTorrents - сортировка результатов
func (s *TorrentService) sortTorrents(torrents []models.TorrentResult, sortBy, sortOrder string) []models.TorrentResult {
if sortBy == "" {
sortBy = "seeders"
}
if sortOrder == "" {
sortOrder = "desc"
}
sort.Slice(torrents, func(i, j int) bool {
var less bool
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
switch sortBy {
case "seeders":
less = torrents[i].Seeders < torrents[j].Seeders
case "size":
less = s.compareSizes(torrents[i].Size, torrents[j].Size)
case "date":
2025-08-07 18:25:43 +00:00
t1, _ := time.Parse(time.RFC3339, torrents[i].PublishDate)
t2, _ := time.Parse(time.RFC3339, torrents[j].PublishDate)
less = t1.Before(t2)
2025-08-07 13:47:42 +00:00
default:
less = torrents[i].Seeders < torrents[j].Seeders
}
if sortOrder == "asc" {
return less
}
return !less
})
return torrents
}
// GroupByQuality - группировка по качеству
func (s *TorrentService) GroupByQuality(results []models.TorrentResult) map[string][]models.TorrentResult {
groups := make(map[string][]models.TorrentResult)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for _, torrent := range results {
quality := torrent.Quality
if quality == "" {
quality = "unknown"
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Объединяем 4K и 2160p в одну группу
if quality == "2160p" {
quality = "4K"
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
groups[quality] = append(groups[quality], torrent)
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Сортируем торренты внутри каждой группы по сидам
for quality := range groups {
sort.Slice(groups[quality], func(i, j int) bool {
return groups[quality][i].Seeders > groups[quality][j].Seeders
})
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return groups
}
// GroupBySeason - группировка по сезонам
func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[string][]models.TorrentResult {
groups := make(map[string][]models.TorrentResult)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for _, torrent := range results {
seasons := make(map[int]bool)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Извлекаем сезоны из поля seasons
for _, season := range torrent.Seasons {
seasons[season] = true
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Извлекаем сезоны из названия
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
for _, match := range matches {
seasonNumber := 0
if match[1] != "" {
seasonNumber, _ = strconv.Atoi(match[1])
} else if match[2] != "" {
seasonNumber, _ = strconv.Atoi(match[2])
}
if seasonNumber > 0 {
seasons[seasonNumber] = true
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Если сезоны не найдены, добавляем в группу "unknown"
if len(seasons) == 0 {
groups["Неизвестно"] = append(groups["Неизвестно"], torrent)
} else {
// Добавляем торрент во все соответствующие группы сезонов
for season := range seasons {
seasonKey := fmt.Sprintf("Сезон %d", season)
// Проверяем дубликаты
found := false
for _, existing := range groups[seasonKey] {
if existing.MagnetLink == torrent.MagnetLink {
found = true
break
}
}
if !found {
groups[seasonKey] = append(groups[seasonKey], torrent)
}
}
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Сортируем торренты внутри каждой группы по сидам
for season := range groups {
sort.Slice(groups[season], func(i, j int) bool {
return groups[season][i].Seeders > groups[season][j].Seeders
})
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
return groups
}
// GetAvailableSeasons - получение доступных сезонов для сериала
func (s *TorrentService) GetAvailableSeasons(title, originalTitle, year string) ([]int, error) {
response, err := s.SearchSeries(title, originalTitle, year, nil)
if err != nil {
return nil, err
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
seasonsSet := make(map[int]bool)
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
for _, torrent := range response.Results {
// Извлекаем из поля seasons
for _, season := range torrent.Seasons {
seasonsSet[season] = true
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
// Извлекаем из названия
seasonRegex := regexp.MustCompile(`(?i)(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон`)
matches := seasonRegex.FindAllStringSubmatch(torrent.Title, -1)
for _, match := range matches {
seasonNumber := 0
if match[1] != "" {
seasonNumber, _ = strconv.Atoi(match[1])
} else if match[2] != "" {
seasonNumber, _ = strconv.Atoi(match[2])
}
if seasonNumber > 0 {
seasonsSet[seasonNumber] = true
}
}
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
var seasons []int
for season := range seasonsSet {
seasons = append(seasons, season)
}
2025-08-07 18:25:43 +00:00
2025-08-07 13:47:42 +00:00
sort.Ints(seasons)
return seasons, nil
}
2025-08-07 18:25:43 +00:00
// SearchByImdb - поиск по IMDB ID (movie/serial/anime).
func (s *TorrentService) SearchByImdb(imdbID, contentType string, season *int) ([]models.TorrentResult, error) {
if imdbID == "" || !strings.HasPrefix(imdbID, "tt") {
return nil, fmt.Errorf("Неверный формат IMDB ID. Должен быть в формате tt1234567")
}
// НЕ добавляем title, originalTitle, year, чтобы запрос не был слишком строгим.
params := map[string]string{
"imdb": imdbID,
}
// Определяем тип контента для API
switch contentType {
case "movie":
params["is_serial"] = "1"
params["category"] = "2000"
case "serial", "series", "tv":
params["is_serial"] = "2"
params["category"] = "5000"
case "anime":
params["is_serial"] = "5"
params["category"] = "5070"
default:
// Значение по умолчанию на случай неизвестного типа
params["is_serial"] = "1"
params["category"] = "2000"
}
// Параметр season можно оставить, он полезен
if season != nil && *season > 0 {
params["season"] = strconv.Itoa(*season)
}
resp, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
results := resp.Results
// Fallback для сериалов: если указан сезон и результатов мало, ищем без сезона и фильтруем на клиенте
if s.contains([]string{"serial", "series", "tv"}, contentType) && season != nil && len(results) < 5 {
paramsNoSeason := map[string]string{
"imdb": imdbID,
"is_serial": "2",
"category": "5000",
}
2025-08-07 18:25:43 +00:00
fallbackResp, err := s.SearchTorrents(paramsNoSeason)
if err == nil {
filtered := s.filterBySeason(fallbackResp.Results, *season)
// Объединяем и убираем дубликаты по MagnetLink
all := append(results, filtered...)
unique := make([]models.TorrentResult, 0, len(all))
seen := make(map[string]bool)
for _, t := range all {
if !seen[t.MagnetLink] {
unique = append(unique, t)
seen[t.MagnetLink] = true
}
}
results = unique
}
}
// Финальная фильтрация по типу контента на стороне клиента для надежности
results = s.FilterByContentType(results, contentType)
return results, nil
}
// ############# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ #############
2025-08-07 13:47:42 +00:00
func (s *TorrentService) qualityMeetsMinimum(quality, minQuality string) bool {
qualityOrder := map[string]int{
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
}
2025-08-07 18:25:43 +00:00
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
minLevel, ok2 := qualityOrder[strings.ToLower(minQuality)]
if !ok1 || !ok2 {
return true // Если качество не определено, не фильтруем
}
2025-08-07 13:47:42 +00:00
return currentLevel >= minLevel
}
func (s *TorrentService) qualityMeetsMaximum(quality, maxQuality string) bool {
qualityOrder := map[string]int{
"360p": 1, "480p": 2, "720p": 3, "1080p": 4, "1440p": 5, "4K": 6, "2160p": 6,
}
2025-08-07 18:25:43 +00:00
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
maxLevel, ok2 := qualityOrder[strings.ToLower(maxQuality)]
if !ok1 || !ok2 {
return true // Если качество не определено, не фильтруем
}
2025-08-07 13:47:42 +00:00
return currentLevel <= maxLevel
}
2025-08-07 18:25:43 +00:00
func (s *TorrentService) parseSize(sizeStr string) int64 {
val, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return 0
}
return val
}
2025-08-07 13:47:42 +00:00
func (s *TorrentService) compareSizes(size1, size2 string) bool {
2025-08-07 18:25:43 +00:00
return s.parseSize(size1) < s.parseSize(size2)
2025-08-07 13:47:42 +00:00
}
func (s *TorrentService) contains(slice []string, item string) bool {
for _, s := range slice {
2025-08-07 18:25:43 +00:00
if strings.EqualFold(s, item) {
2025-08-07 13:47:42 +00:00
return true
}
}
return false
}
func (s *TorrentService) containsAny(slice []string, items []string) bool {
for _, item := range items {
if s.contains(slice, item) {
return true
}
}
return false
2025-08-07 18:25:43 +00:00
}