Files
neomovies-api/src/services/redapi.service.js

759 lines
29 KiB
JavaScript
Raw Normal View History

2025-07-17 20:35:20 +03:00
const axios = require('axios');
/**
* Клиент для работы с RedAPI (Lampac)
* Основан на коде Lampac ApiController.cs и RedApi.cs
*/
class RedApiClient {
constructor(baseUrl = 'http://redapi.cfhttp.top', apikey = '') {
this.baseUrl = baseUrl;
this.apikey = apikey;
}
/**
* Поиск торрентов через RedAPI с поддержкой сезонов
* @param {Object} params - параметры поиска
* @returns {Promise<Array>}
*/
async searchTorrents(params) {
const {
query, // поисковый запрос
title, // название фильма/сериала
title_original, // оригинальное название
year, // год выпуска
is_serial, // тип контента: 1-фильм, 2-сериал, 5-аниме
category, // категория
imdb, // IMDB ID
season // номер сезона для сериалов
} = params;
const searchParams = new URLSearchParams();
if (query) searchParams.append('query', query);
if (title) searchParams.append('title', title);
if (title_original) searchParams.append('title_original', title_original);
if (year) searchParams.append('year', year);
if (is_serial) searchParams.append('is_serial', is_serial);
if (category) searchParams.append('category[]', category);
if (imdb) searchParams.append('imdb', imdb);
if (season) searchParams.append('season', season);
if (this.apikey) searchParams.append('apikey', this.apikey);
try {
console.log('RedAPI search params:', params);
console.log('Search URL:', `${this.baseUrl}/api/v2.0/indexers/all/results?${searchParams}`);
const response = await axios.get(
`${this.baseUrl}/api/v2.0/indexers/all/results?${searchParams}`,
{ timeout: 8000 }
);
return this.parseResults(response.data);
} catch (error) {
console.error('RedAPI search error:', error.message);
return [];
}
}
/**
* Парсинг результатов поиска с поддержкой сезонов
* @param {Object} data - ответ от RedAPI
* @returns {Array}
*/
parseResults(data) {
if (!data.Results || !Array.isArray(data.Results)) {
console.log('RedAPI: No results found or invalid response format');
return [];
}
console.log(`RedAPI: Found ${data.Results.length} results`);
return data.Results.map(torrent => ({
title: torrent.Title,
tracker: torrent.Tracker,
size: torrent.Size,
seeders: torrent.Seeders,
peers: torrent.Peers,
magnet: torrent.MagnetUri,
publishDate: torrent.PublishDate,
category: torrent.CategoryDesc,
quality: torrent.Info?.quality,
voice: torrent.Info?.voices,
details: torrent.Details,
types: torrent.Info?.types,
seasons: torrent.Info?.seasons, // Добавляем информацию о сезонах
source: 'RedAPI'
}));
}
/**
* Фильтрация результатов по типу контента на клиенте
* Решает проблему смешанных результатов от API
* @param {Array} results - результаты поиска
* @param {string} contentType - тип контента (movie/serial/anime)
* @returns {Array}
*/
filterByContentType(results, contentType) {
return results.filter(torrent => {
// Фильтрация по полю types, если оно есть
if (torrent.types && Array.isArray(torrent.types)) {
switch (contentType) {
case 'movie':
return torrent.types.some(type =>
['movie', 'multfilm', 'documovie'].includes(type)
);
case 'serial':
return torrent.types.some(type =>
['serial', 'multserial', 'docuserial', 'tvshow'].includes(type)
);
case 'anime':
return torrent.types.includes('anime');
}
}
// Фильтрация по названию, если types недоступно
const title = torrent.title.toLowerCase();
switch (contentType) {
case 'movie':
return !/(сезон|серии|series|season|эпизод)/i.test(title);
case 'serial':
return /(сезон|серии|series|season|эпизод)/i.test(title);
case 'anime':
return torrent.category === 'TV/Anime' || /anime/i.test(title);
default:
return true;
}
});
}
/**
* Поиск фильмов с дополнительной фильтрацией
* @param {string} title - название на русском
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @returns {Promise<Array>}
*/
async searchMovies(title, originalTitle, year) {
const results = await this.searchTorrents({
title,
title_original: originalTitle,
year,
is_serial: 1,
category: '2000'
});
return this.filterByContentType(results, 'movie');
}
/**
* Поиск сериалов с дополнительной фильтрацией
* @param {string} title - название на русском
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @returns {Promise<Array>}
*/
async searchSeries(title, originalTitle, year) {
const results = await this.searchTorrents({
title,
title_original: originalTitle,
year,
is_serial: 2,
category: '5000'
});
return this.filterByContentType(results, 'serial');
}
/**
* Поиск аниме
* @param {string} title - название на русском
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @returns {Promise<Array>}
*/
async searchAnime(title, originalTitle, year) {
const results = await this.searchTorrents({
title,
title_original: originalTitle,
year,
is_serial: 5,
category: '5070'
});
return this.filterByContentType(results, 'anime');
}
/**
* Поиск по общему запросу с фильтрацией качества
* @param {string} query - поисковый запрос
* @param {string} type - тип контента (movie/serial/anime)
* @param {number} year - год выпуска
* @returns {Promise<Array>}
*/
async searchByQuery(query, type = 'movie', year = null) {
const params = { query };
if (year) params.year = year;
switch (type) {
case 'movie':
params.is_serial = 1;
params.category = '2000';
break;
case 'serial':
params.is_serial = 2;
params.category = '5000';
break;
case 'anime':
params.is_serial = 5;
params.category = '5070';
break;
}
const results = await this.searchTorrents(params);
return this.filterByContentType(results, type);
}
/**
* Получение информации о фильме по IMDB ID через Alloha API
* @param {string} imdbId - IMDB ID
* @returns {Promise<Object|null>}
*/
async getMovieInfoByImdb(imdbId) {
try {
const response = await axios.get(
`https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1&imdb=${imdbId}`,
{ timeout: 10000 }
);
const data = response.data?.data;
return data ? {
name: data.name,
original_name: data.original_name
} : null;
} catch (error) {
console.error('Ошибка получения информации по IMDB:', error.message);
return null;
}
}
/**
* Получение информации по Kinopoisk ID
* @param {string} kpId - Kinopoisk ID
* @returns {Promise<Object|null>}
*/
async getMovieInfoByKinopoisk(kpId) {
try {
const response = await axios.get(
`https://api.alloha.tv/?token=04941a9a3ca3ac16e2b4327347bbc1&kp=${kpId}`,
{ timeout: 10000 }
);
const data = response.data?.data;
return data ? {
name: data.name,
original_name: data.original_name
} : null;
} catch (error) {
console.error('Ошибка получения информации по Kinopoisk ID:', error.message);
return null;
}
}
/**
* Поиск по IMDB ID
* @param {string} imdbId - IMDB ID (например, 'tt1234567')
* @param {string} type - 'movie', 'serial' или 'anime'
* @returns {Promise<Array>}
*/
async searchByImdb(imdbId, type = 'movie') {
if (!imdbId || !imdbId.match(/^tt\d+$/)) {
throw new Error('Неверный формат IMDB ID. Должен быть в формате tt1234567');
}
console.log(`RedAPI search by IMDB ID: ${imdbId}`);
// Сначала получаем информацию о фильме
const movieInfo = await this.getMovieInfoByImdb(imdbId);
const params = { imdb: imdbId };
// Устанавливаем категорию и тип контента
switch (type) {
case 'movie':
params.is_serial = 1;
params.category = '2000';
break;
case 'serial':
params.is_serial = 2;
params.category = '5000';
break;
case 'anime':
params.is_serial = 5;
params.category = '5070';
break;
default:
params.is_serial = 1;
params.category = '2000';
}
// Если получили информацию о фильме, добавляем названия
if (movieInfo) {
params.title = movieInfo.name;
params.title_original = movieInfo.original_name;
}
const results = await this.searchTorrents(params);
return this.filterByContentType(results, type);
}
/**
* Поиск по Kinopoisk ID
* @param {string} kpId - Kinopoisk ID
* @param {string} type - 'movie', 'serial' или 'anime'
* @returns {Promise<Array>}
*/
async searchByKinopoisk(kpId, type = 'movie') {
if (!kpId || !kpId.toString().match(/^\d+$/)) {
throw new Error('Неверный формат Kinopoisk ID');
}
console.log(`RedAPI search by Kinopoisk ID: ${kpId}`);
const movieInfo = await this.getMovieInfoByKinopoisk(kpId);
const params = { query: `kp${kpId}` };
switch (type) {
case 'movie':
params.is_serial = 1;
params.category = '2000';
break;
case 'serial':
params.is_serial = 2;
params.category = '5000';
break;
case 'anime':
params.is_serial = 5;
params.category = '5070';
break;
}
if (movieInfo) {
params.title = movieInfo.name;
params.title_original = movieInfo.original_name;
}
const results = await this.searchTorrents(params);
return this.filterByContentType(results, type);
}
/**
* Расширенная фильтрация по качеству
* @param {Array} results - результаты поиска
* @param {Object} qualityFilter - объект с параметрами фильтрации качества
* @returns {Array}
*/
filterByQuality(results, qualityFilter = {}) {
if (!qualityFilter || Object.keys(qualityFilter).length === 0) {
return results;
}
const {
qualities = [], // ['1080p', '720p', '4K', '2160p']
minQuality = null, // минимальное качество
maxQuality = null, // максимальное качество
excludeQualities = [], // исключить качества
hdr = null, // true/false для HDR
hevc = null // true/false для HEVC/H.265
} = qualityFilter;
// Порядок качества от низкого к высокому
const qualityOrder = ['360p', '480p', '720p', '1080p', '1440p', '2160p', '4K'];
return results.filter(torrent => {
const title = torrent.title.toLowerCase();
// Определяем качество из названия
const detectedQuality = this.detectQuality(title);
// Фильтрация по конкретным качествам
if (qualities.length > 0) {
const hasQuality = qualities.some(q =>
title.includes(q.toLowerCase()) ||
(q === '4K' && title.includes('2160p'))
);
if (!hasQuality) return false;
}
// Фильтрация по минимальному качеству
if (minQuality && detectedQuality) {
const minIndex = qualityOrder.indexOf(minQuality);
const currentIndex = qualityOrder.indexOf(detectedQuality);
if (currentIndex !== -1 && minIndex !== -1 && currentIndex < minIndex) {
return false;
}
}
// Фильтрация по максимальному качеству
if (maxQuality && detectedQuality) {
const maxIndex = qualityOrder.indexOf(maxQuality);
const currentIndex = qualityOrder.indexOf(detectedQuality);
if (currentIndex !== -1 && maxIndex !== -1 && currentIndex > maxIndex) {
return false;
}
}
// Исключение определенных качеств
if (excludeQualities.length > 0) {
const hasExcluded = excludeQualities.some(q =>
title.includes(q.toLowerCase())
);
if (hasExcluded) return false;
}
// Фильтрация по HDR
if (hdr !== null) {
const hasHDR = /hdr|dolby.vision|dv/i.test(title);
if (hdr && !hasHDR) return false;
if (!hdr && hasHDR) return false;
}
// Фильтрация по HEVC
if (hevc !== null) {
const hasHEVC = /hevc|h\.265|x265/i.test(title);
if (hevc && !hasHEVC) return false;
if (!hevc && hasHEVC) return false;
}
return true;
});
}
/**
* Определение качества из названия торрента
* @param {string} title - название торрента
* @returns {string|null}
*/
detectQuality(title) {
const qualityPatterns = [
{ pattern: /2160p|4k/i, quality: '2160p' },
{ pattern: /1440p/i, quality: '1440p' },
{ pattern: /1080p/i, quality: '1080p' },
{ pattern: /720p/i, quality: '720p' },
{ pattern: /480p/i, quality: '480p' },
{ pattern: /360p/i, quality: '360p' }
];
for (const { pattern, quality } of qualityPatterns) {
if (pattern.test(title)) {
return quality;
}
}
return null;
}
/**
* Получение статистики по качеству
* @param {Array} results - результаты поиска
* @returns {Object}
*/
getQualityStats(results) {
const stats = {};
results.forEach(torrent => {
const quality = this.detectQuality(torrent.title.toLowerCase());
if (quality) {
stats[quality] = (stats[quality] || 0) + 1;
}
});
return stats;
}
/**
* Группировка результатов по качеству
* @param {Array} results - результаты поиска
* @returns {Object} - объект с группами качества
*/
groupByQuality(results) {
const groups = {
'4K': [],
'2160p': [],
'1440p': [],
'1080p': [],
'720p': [],
'480p': [],
'360p': [],
'unknown': []
};
results.forEach(torrent => {
const quality = this.detectQuality(torrent.title.toLowerCase());
if (quality) {
// Объединяем 4K и 2160p в одну группу
if (quality === '2160p') {
groups['4K'].push(torrent);
} else {
groups[quality].push(torrent);
}
} else {
groups['unknown'].push(torrent);
}
});
// Удаляем пустые группы и сортируем по качеству (от высокого к низкому)
const sortedGroups = {};
const qualityOrder = ['4K', '1440p', '1080p', '720p', '480p', '360p', 'unknown'];
qualityOrder.forEach(quality => {
if (groups[quality].length > 0) {
// Сортируем торренты внутри группы по сидам
groups[quality].sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
sortedGroups[quality] = groups[quality];
}
});
return sortedGroups;
}
/**
* Расширенный поиск с поддержкой сезонов
* @param {Object} searchParams - параметры поиска
* @param {Object} qualityFilter - фильтр качества
* @returns {Promise<Array>}
*/
async searchWithQualityFilter(searchParams, qualityFilter = {}) {
const results = await this.searchTorrents(searchParams);
// Применяем фильтрацию по типу контента
let filteredResults = results;
if (searchParams.contentType) {
filteredResults = this.filterByContentType(results, searchParams.contentType);
}
// Применяем фильтрацию по сезону (дополнительная на клиенте)
if (searchParams.season && !searchParams.seasonFromAPI) {
filteredResults = this.filterBySeason(filteredResults, searchParams.season);
}
// Применяем фильтрацию по качеству
filteredResults = this.filterByQuality(filteredResults, qualityFilter);
// Сортируем результаты
if (qualityFilter.sortBy) {
filteredResults = this.sortTorrents(filteredResults, qualityFilter.sortBy, qualityFilter.sortOrder);
}
return filteredResults;
}
/**
* Сортировка результатов
* @param {Array} results - результаты поиска
* @param {string} sortBy - поле для сортировки (seeders/size/date)
* @param {string} order - порядок сортировки (asc/desc)
* @returns {Array}
*/
sortTorrents(results, sortBy = 'seeders', order = 'desc') {
return results.sort((a, b) => {
let valueA, valueB;
switch (sortBy) {
case 'seeders':
valueA = a.seeders || 0;
valueB = b.seeders || 0;
break;
case 'size':
valueA = a.size || 0;
valueB = b.size || 0;
break;
case 'date':
valueA = new Date(a.publishDate || 0);
valueB = new Date(b.publishDate || 0);
break;
default:
return 0;
}
if (order === 'asc') {
return valueA - valueB;
} else {
return valueB - valueA;
}
});
}
/**
* Поиск сериалов с поддержкой выбора сезона
* @param {string} title - название на русском
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @param {number} season - номер сезона (опционально)
* @param {Object} qualityFilter - фильтр качества
* @returns {Promise<Array>}
*/
async searchSeries(title, originalTitle, year, season = null, qualityFilter = {}) {
const params = {
title,
title_original: originalTitle,
year,
is_serial: 2,
category: '5000',
contentType: 'serial'
};
if (season) {
params.season = season;
}
return this.searchWithQualityFilter(params, qualityFilter);
}
/**
* Получение доступных сезонов для сериала
* @param {string} title - название сериала
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @returns {Promise<Array>} - массив номеров сезонов
*/
async getAvailableSeasons(title, originalTitle, year) {
const results = await this.searchSeries(title, originalTitle, year);
const seasons = new Set();
results.forEach(torrent => {
// Extract from the dedicated field
if (torrent.seasons && Array.isArray(torrent.seasons)) {
torrent.seasons.forEach(s => seasons.add(parseInt(s)));
}
// Extract from title
const title = torrent.title;
const seasonRegex = /(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон/gi;
for (const match of title.matchAll(seasonRegex)) {
const seasonNumber = parseInt(match[1] || match[2]);
if (!isNaN(seasonNumber)) {
seasons.add(seasonNumber);
}
}
});
return Array.from(seasons).sort((a, b) => a - b);
}
/**
* Фильтрация результатов по сезону на клиенте
* @param {Array} results - результаты поиска
* @param {number} season - номер сезона
* @returns {Array}
*/
filterBySeason(results, season) {
if (!season) return results;
return results.filter(torrent => {
// Проверяем поле seasons
if (torrent.seasons && Array.isArray(torrent.seasons)) {
return torrent.seasons.includes(season);
}
// Проверяем название торрента
const title = torrent.title.toLowerCase();
const seasonPatterns = [
new RegExp(`сезон[\\s:]*${season}`, 'i'),
new RegExp(`season[\\s:]*${season}`, 'i'),
new RegExp(`s${season}(?![0-9])`, 'i'),
new RegExp(`${season}[\\s]*сезон`, 'i')
];
return seasonPatterns.some(pattern => pattern.test(title));
});
}
/**
* Поиск конкретного сезона сериала
* @param {string} title - название сериала
* @param {string} originalTitle - оригинальное название
* @param {number} year - год выпуска
* @param {number} season - номер сезона
* @param {Object} qualityFilter - фильтр качества
* @returns {Promise<Array>}
*/
async searchSeriesSeason(title, originalTitle, year, season, qualityFilter = {}) {
// Сначала пробуем поиск с параметром season
let results = await this.searchSeries(title, originalTitle, year, season, qualityFilter);
// Если результатов мало, делаем общий поиск и фильтруем на клиенте
if (results.length < 5) {
const allResults = await this.searchSeries(title, originalTitle, year, null, qualityFilter);
const filteredResults = this.filterBySeason(allResults, season);
// Объединяем результаты и убираем дубликаты
const combined = [...results, ...filteredResults];
const unique = combined.filter((torrent, index, self) =>
index === self.findIndex(t => t.magnet === torrent.magnet)
);
results = unique;
}
return results;
}
/**
* Группировка результатов по сезону
* @param {Array} results - результаты поиска
* @returns {Object} - объект с группами по сезонам
*/
groupBySeason(results) {
const grouped = {};
results.forEach(torrent => {
const seasons = new Set();
// Extract seasons from the dedicated field
if (torrent.seasons && Array.isArray(torrent.seasons)) {
torrent.seasons.forEach(s => seasons.add(parseInt(s)));
}
// Extract from title as a fallback or supplement
const title = torrent.title;
const seasonRegex = /(?:s|сезон)[\s:]*(\d+)|(\d+)\s*сезон/gi;
for (const match of title.matchAll(seasonRegex)) {
const seasonNumber = parseInt(match[1] || match[2]);
if (!isNaN(seasonNumber)) {
seasons.add(seasonNumber);
}
}
const seasonsArray = Array.from(seasons);
// If no season is found, group as 'unknown'
if (seasonsArray.length === 0) {
seasonsArray.push('unknown');
}
// Add torrent to all relevant season groups
seasonsArray.forEach(season => {
const seasonKey = season === 'unknown' ? 'Неизвестно' : `Сезон ${season}`;
if (!grouped[seasonKey]) {
grouped[seasonKey] = [];
}
// Ensure torrent is not added to the same group twice
if (!grouped[seasonKey].find(t => t.magnet === torrent.magnet)) {
grouped[seasonKey].push(torrent);
}
});
});
// Sort torrents within each group by seeders
Object.keys(grouped).forEach(season => {
grouped[season].sort((a, b) => (b.seeders || 0) - (a.seeders || 0));
});
return grouped;
}
}
module.exports = RedApiClient;