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

936 lines
26 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}
func NewTorrentServiceWithConfig(baseURL, apiKey string) *TorrentService {
return &TorrentService{
client: &http.Client{Timeout: 8 * time.Second},
baseURL: baseURL,
apiKey: apiKey,
}
}
func NewTorrentService() *TorrentService {
return &TorrentService{
client: &http.Client{Timeout: 8 * time.Second},
baseURL: "http://redapi.cfhttp.top",
apiKey: "",
}
}
// SearchTorrents - основной метод поиска торрентов через RedAPI
func (s *TorrentService) SearchTorrents(params map[string]string) (*models.TorrentSearchResponse, error) {
searchParams := url.Values{}
for key, value := range params {
if value != "" {
if key == "category" {
searchParams.Add("category[]", value)
} else {
searchParams.Add(key, value)
}
}
}
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: %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: params["query"],
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 {
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 = ""
}
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",
}
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)
}
result.Voice = torrent.Info.Voices
result.Types = torrent.Info.Types
result.Seasons = torrent.Info.Seasons
}
if result.Quality == "" {
result.Quality = s.ExtractQuality(result.Title)
}
results = append(results, result)
}
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)
}
params := map[string]string{
"imdb": imdbID,
"query": title,
"title_original": originalTitle,
"year": year,
}
switch mediaType {
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"
}
if options != nil && options.Season != nil && *options.Season > 0 {
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)
}
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
}
}
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",
}
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
response.Results = s.FilterByContentType(response.Results, "movie")
response.Total = len(response.Results)
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",
}
response, err := s.SearchTorrents(params)
if err != nil {
return nil, err
}
response.Results = s.FilterByContentType(response.Results, "anime")
response.Total = len(response.Results)
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)
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)
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)
}
// FilterByContentType - фильтрация по типу контента (как в JS)
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)
}
case "serial", "series", "tv":
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)
}
case "serial", "series", "tv":
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
}
}
// Проверяем в названии
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
}
}
return false
}
// ExtractQuality - извлечение качества из названия
func (s *TorrentService) ExtractQuality(title string) string {
title = strings.ToUpper(title)
qualityPatterns := []struct {
pattern string
quality string
}{
{`2160P|4K`, "2160p"},
{`1440P`, "1440p"},
{`1080P`, "1080p"},
{`720P`, "720p"},
{`480P`, "480p"},
{`360P`, "360p"},
}
for _, qp := range qualityPatterns {
if matched, _ := regexp.MatchString(qp.pattern, title); matched {
if qp.quality == "2160p" {
return "4K"
}
return qp.quality
}
}
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
switch sortBy {
case "seeders":
less = torrents[i].Seeders < torrents[j].Seeders
case "size":
less = s.compareSizes(torrents[i].Size, torrents[j].Size)
case "date":
t1, _ := time.Parse(time.RFC3339, torrents[i].PublishDate)
t2, _ := time.Parse(time.RFC3339, torrents[j].PublishDate)
less = t1.Before(t2)
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)
for _, torrent := range results {
quality := torrent.Quality
if quality == "" {
quality = "unknown"
}
// Объединяем 4K и 2160p в одну группу
if quality == "2160p" {
quality = "4K"
}
groups[quality] = append(groups[quality], torrent)
}
// Сортируем торренты внутри каждой группы по сидам
for quality := range groups {
sort.Slice(groups[quality], func(i, j int) bool {
return groups[quality][i].Seeders > groups[quality][j].Seeders
})
}
return groups
}
// GroupBySeason - группировка по сезонам
func (s *TorrentService) GroupBySeason(results []models.TorrentResult) map[string][]models.TorrentResult {
groups := make(map[string][]models.TorrentResult)
for _, torrent := range results {
seasons := make(map[int]bool)
// Извлекаем сезоны из поля seasons
for _, season := range torrent.Seasons {
seasons[season] = true
}
// Извлекаем сезоны из названия
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
}
}
// Если сезоны не найдены, добавляем в группу "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)
}
}
}
}
// Сортируем торренты внутри каждой группы по сидам
for season := range groups {
sort.Slice(groups[season], func(i, j int) bool {
return groups[season][i].Seeders > groups[season][j].Seeders
})
}
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
}
seasonsSet := make(map[int]bool)
for _, torrent := range response.Results {
// Извлекаем из поля seasons
for _, season := range torrent.Seasons {
seasonsSet[season] = true
}
// Извлекаем из названия
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
}
}
}
var seasons []int
for season := range seasonsSet {
seasons = append(seasons, season)
}
sort.Ints(seasons)
return seasons, nil
}
// 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",
}
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
}
// ############# ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ #############
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,
}
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
minLevel, ok2 := qualityOrder[strings.ToLower(minQuality)]
if !ok1 || !ok2 {
return true // Если качество не определено, не фильтруем
}
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,
}
currentLevel, ok1 := qualityOrder[strings.ToLower(quality)]
maxLevel, ok2 := qualityOrder[strings.ToLower(maxQuality)]
if !ok1 || !ok2 {
return true // Если качество не определено, не фильтруем
}
return currentLevel <= maxLevel
}
func (s *TorrentService) parseSize(sizeStr string) int64 {
val, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
return 0
}
return val
}
func (s *TorrentService) compareSizes(size1, size2 string) bool {
return s.parseSize(size1) < s.parseSize(size2)
}
func (s *TorrentService) contains(slice []string, item string) bool {
for _, s := range slice {
if strings.EqualFold(s, item) {
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
}