UNPKG

torapi

Version:

Unofficial API (backend) for RuTracker, Kinozal, RuTor and NoNameClub for receiving torrent files and detailed information about distribution by movie title, TV series or id, and also provides RSS news feed for all providers.

1,217 lines (1,192 loc) 117 kB
const express = require('express') const swaggerJsdoc = require('swagger-jsdoc') const swaggerUi = require('swagger-ui-express') const yargs = require('yargs') const axios = require('axios') const cheerio = require('cheerio') const proxy = require('https-proxy-agent') const iconv = require('iconv-lite') const xml2js = require('xml2js') const path = require('path') const fs = require('fs') // Прочитать содержимое файла с категориями const categoryList = JSON.parse(fs.readFileSync(path.join(__dirname, 'category.json'), 'utf-8')) // Параметры запуска const { hideBin } = require('yargs/helpers') const argv = yargs(hideBin(process.argv)) .option('port', { alias: 'p', type: 'number', default: 8443, description: 'Express server port' }) .option('test', { alias: 't', type: 'boolean', default: false, description: 'Test endpoints and stop server' }) .option('query', { alias: 'q', type: 'string', default: 'The+Rookie', description: 'Title for test' }) .option('proxyAddress', { type: 'string', description: 'Address proxy server' }) .option('proxyPort', { type: 'number', description: 'Port proxy server' }) .option('username', { type: 'string', description: 'Username for proxy server' }) .option('password', { type: 'string', description: 'Password for proxy server' }) .argv // Использовать Puppeteer для получения списка файлов // const puppeteer = require('puppeteer') const RuTrackerPuppeteer = false const RuTorPuppeteer = false // Создание экземпляра Axios с использованием конфигурации Proxy const createAxiosProxy = () => { const config = {} if (argv.proxyAddress && argv.proxyPort) { if (argv.username && argv.password) { config.httpsAgent = new proxy.HttpsProxyAgent(`http://${argv.username}:${argv.password}@${argv.proxyAddress}:${argv.proxyPort}`) } else { config.httpsAgent = new proxy.HttpsProxyAgent(`http://${argv.proxyAddress}:${argv.proxyPort}`) } } return axios.create(config) } const axiosProxy = createAxiosProxy() // Имя агента в заголовке запросов (вместо axios) const headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0 Win64 x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36' } // Cookie для автроризации на сайте RuTracker (требуется для получения info hash списка файлов) const headers_RuTracker = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0 Win64 x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Cookie': 'bb_session=0-44590272-Sp8wQfjonpx37QjDuZUD' } // Cookie для автроризации на сайте Kinozal (требуется для получения info hash списка файлов) const headers_Kinozal = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0 Win64 x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', 'Cookie': 'uid=20631917; pass=KOJ4DJf1VS' } // Функция получения текущего времени для логирования function getCurrentTime() { const now = new Date() const hours = now.getHours().toString().padStart(2, '0') const minutes = now.getMinutes().toString().padStart(2, '0') const seconds = now.getSeconds().toString().padStart(2, '0') return `${hours}:${minutes}:${seconds}` } // Функция преобразования номера страницы (для RuTracker и NoNameClub) function getPage(page) { const pages = { '0': '0', '1': '50', '2': '100', '3': '150', '4': '200', '5': '250', '6': '300', '7': '350', '8': '400', '9': '450', '10': '500', '11': '550', '12': '600', '13': '650', '14': '700', '15': '750', '16': '800', '17': '850', '18': '900', '19': '950', '20': '1000' } return pages[page] } // Функция преобразования времени в формат 'dd.mm.yyyy' (для RuTracker и RuTor) function formatDate(dateString, type) { const months = { 'Янв': '01', 'Фев': '02', 'Мар': '03', 'Апр': '04', 'Май': '05', 'Июн': '06', 'Июл': '07', 'Авг': '08', 'Сен': '09', 'Окт': '10', 'Ноя': '11', 'Дек': '12' } const parts = dateString.split(`${type}`) let day = parts[0].trim() const month = months[parts[1].trim()] const year = '20' + parts[2].trim() // Добавляем ведущий ноль к дню if (day.length === 1) { day = '0' + day } return `${day}.${month}.${year}` } // Функция преобразования времени из Unix Timestamp в 'dd.mm.yyyy HH:MM' (для NoNameClub) function unixTimestamp(timestamp) { const date = new Date(timestamp * 1000) const day = String(date.getDate()).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0') const year = date.getFullYear() const hours = String(date.getHours()).padStart(2, '0') const minutes = String(date.getMinutes()).padStart(2, '0') return `${day}.${month}.${year} ${hours}:${minutes}` } // Функция для добавления списка торрент трекеров в магнитную ссылку function addTrackerList(infoHash, tracker) { let magnetLink = `magnet:?xt=urn:btih:${infoHash}` let trackers = [] if (tracker === "RuTracker") { trackers = [ "http://retracker.local/announce", "http://bt.t-ru.org/ann", "http://bt2.t-ru.org/ann", "http://bt3.t-ru.org/ann", "http://bt4.t-ru.org/ann" ] } else if (tracker === "Kinozal") { trackers = [ "http://retracker.local/announce", "http://tr0.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr1.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr2.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr3.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr4.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr5.torrent4me.com/ann?uk=kCm7WcIM00", "http://tr0.tor4me.info/ann?uk=kCm7WcIM00", "http://tr1.tor4me.info/ann?uk=kCm7WcIM00", "http://tr2.tor4me.info/ann?uk=kCm7WcIM00", "http://tr3.tor4me.info/ann?uk=kCm7WcIM00", "http://tr4.tor4me.info/ann?uk=kCm7WcIM00", "http://tr5.tor4me.info/ann?uk=kCm7WcIM00", "http://tr0.tor2me.info/ann?uk=kCm7WcIM00", "http://tr1.tor2me.info/ann?uk=kCm7WcIM00", "http://tr2.tor2me.info/ann?uk=kCm7WcIM00", "http://tr3.tor2me.info/ann?uk=kCm7WcIM00", "http://tr4.tor2me.info/ann?uk=kCm7WcIM00", "http://tr5.tor2me.info/ann?uk=kCm7WcIM00" ] } else if (tracker === "RuTor") { trackers = [ "http://retracker.local/announce", "udp://opentor.net:6969", "udp://open.stealth.si:80/announce", "udp://exodus.desync.com:6969/announce", "http://tracker.grepler.com:6969/announce", "udp://tracker.dler.com:6969/announce", "udp://tracker.bitsearch.to:1337/announce", "http://h1.trakx.nibba.trade:80/announce", "http://h2.trakx.nibba.trade:80/announce", "http://h3.trakx.nibba.trade:80/announce", "http://h4.trakx.nibba.trade:80/announce", "http://h5.trakx.nibba.trade:80/announce" ] } else if (tracker === "NoNameClub") { trackers = [ "http://retracker.local/announce", "http://bt01.nnm-club.info:2710/announce", "http://bt02.nnm-club.info:2710/announce", "http://bt01.nnm-club.cc:2710/announce", "http://bt02.nnm-club.cc:2710/announce" ] } for (var i = 0; i < trackers.length; i++) { magnetLink += "&tr=" + encodeURIComponent(trackers[i]) } return magnetLink } // RuTracker async function RuTracker(query, categoryId, page) { if (query === 'undefined') { query = '' } // Получаем кастомный номер страницы через функцию (кратный 50) const p = getPage(page) // Список все зеркальных URL провайдера для перебора в цикле в случае недоступности одного const urls = [ 'https://rutracker.org', 'https://rutracker.net', 'https://rutracker.nl' ] // Переменная для отслеживания успешного выполнения запроса let checkUrl = false const torrents = [] let html let url for (let i = 0; i < urls.length; i++) { url = urls[i] urlQuery = `${urls[i]}/forum/tracker.php?nm=${query}&f=${categoryId}&start=${p}` try { const response = await axiosProxy.get(urlQuery, { timeout: 3000, responseType: 'arraybuffer', headers: headers_RuTracker }) // Декодируем HTML-страницу в кодировку win-1251 html = iconv.decode(response.data, 'win1251') // Если удалось получить данные, фиксируем успух, логируем и выходим из цикла checkUrl = true console.log(`${getCurrentTime()} [Request] ${urlQuery}`) break } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) } } if (!checkUrl) { return { 'Result': `Server is not available` } } const data = cheerio.load(html) data('table .forumline tbody tr').each((_, element) => { const checkData = data(element).find('.row4 .wbr .med').text().trim() if (checkData.length > 0) { const torrent = { 'Name': data(element).find('.row4 .wbr .med').text().trim(), 'Id': data(element).find('.row4 .wbr .med').attr('href').replace(/.+t=/g, ''), 'Url': `${url}/forum/` + data(element).find('.row4 .wbr .med').attr('href'), 'Torrent': `${url}/forum/dl.php?t=` + data(element).find('.row4 .wbr .med').attr('href').replace(/.+t=/g, ''), // Забираем первые два значения (размер и тип данных) /// 'Size': data(element).find('.row4.small:eq(0)').text().trim().split(' ').slice(0,1).join(' '), 'Size': data(element).find('a.small.tr-dl.dl-stub').text().trim().split(' ').slice(0, 1).join(' '), 'Download_Count': data(element).find('td.row4.small.number-format').text().trim(), // Проверяем проверенный ли торрент и изменяем формат вывода 'Checked': data(element).find('td.row1.t-ico').text().trim() === '√' ? 'True' : 'False', // 'Type_Link': `${url}/forum/` + data(element).find('.row1 .f-name .gen').attr('href'), 'Category': data(element).find('.row1 .f-name .gen').text().trim(), 'Seeds': data(element).find('b.seedmed').text().trim(), 'Peers': data(element).find('td.row4.leechmed.bold').text().trim(), // Заменяем все символы пробела на обычные пробелы и форматируем дату (передаем пробел вторым параметром разделителя) 'Date': formatDate( data(element).find('td.row4 p').text().trim().replace(/(\d{1,2}-[А-Яа-я]{3}-\d{2}).*/, '$1'), "-" ) } torrents.push(torrent) } }) if (torrents.length === 0) { return { 'Result': 'No matches were found for your title' } } else { return torrents } } // RuTracker All Page async function RuTrackerAllPage(query, categoryId) { let result = [] page = 0 while (true) { let currentResult = await RuTracker(query, categoryId, page) if (Array.isArray(currentResult)) { currentResult.forEach(element => { result.push(element) }) } else { result = [{ 'Result': 'No matches were found for your title' }] break } // Максимум 10 страниц if (currentResult.length === 50 && page < 9) { page++ } else { break } } return result } // RuTracker ID async function RuTrackerID(query) { const url = `https://rutracker.org/forum/viewtopic.php?t=${query}` let html try { const response = await axiosProxy.get(url, { responseType: 'arraybuffer', headers: headers_RuTracker }) html = iconv.decode(response.data, 'win1251') console.log(`${getCurrentTime()} [Request] ${url}`) } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `The ${error.hostname} server is not available` } } const data = cheerio.load(html) let Name = data('a#topic-title').text().trim() // Hash let Hash = data('a[href*="magnet:?xt=urn:btih:"]').attr('href').replace(/.+btih:|&.+/g, '') // Получение ссылки на загрузку торрент файла (по поиску части содержимого атрибута и по классу необходимы Cookie) // let Torrent = data('a[href*="dl.php?t="]').attr('href') // let Torrent = data('a.dl-stub.dl-link.dl-topic').attr('href') let Torrent = `https://rutracker.org/forum/dl.php?t=${query}` // IMDb let imdb data('a[href*="imdb.com"]').each((index, element) => { const href = data(element).attr('href') if (href.includes('imdb.com')) { imdb = href return false } }) if (!imdb) { imdb = "" } // Kinopoisk let kp data('a[href*="kinopoisk.ru"]').each((index, element) => { const href = data(element).attr('href') if (href.includes('kinopoisk.ru')) { kp = href return false } }) if (!kp) { kp = "" } // Год выпуска const Year = (() => { const element = data('span.post-b:contains("Год")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Страна let Release = (() => { const element = data('span.post-b:contains("Страна")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Жанр const Type = (() => { const element = data('span.post-b:contains("Жанр")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Продолжительность const Duration = (() => { const element = data('span.post-b:contains("Продолжительность")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Перевод const Audio = (() => { const element = data('span.post-b:contains("Перевод")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Режиссёр const Directer = (() => { const element = data('span.post-b:contains("Режиссёр")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // В ролях const Actors = (() => { const element = data('span.post-b:contains("В ролях")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Описание const Description = (() => { const element = data('span.post-b:contains("Описание")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Качество const videoQuality = (() => { const element = data('span.post-b:contains("Качество")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Видео const Video = (() => { const element = data('span.post-b:contains("Видео")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })() // Постер let Poster = '' const posterElement = data('.postImg.postImgAligned.img-right').attr('title') if (posterElement && posterElement.length) { Poster = posterElement } // Получаем список файлов let torrents = [] // Puppeteer if (RuTrackerPuppeteer == true) { torrents = await RuTrackerFilesPuppetter(query) } else { const urlFiles = 'https://rutracker.org/forum/viewtorrent.php' const postData = `t=${query}` try { const response = await axiosProxy.post(urlFiles, postData, { responseType: 'arraybuffer', headers: headers_RuTracker }) html = iconv.decode(response.data, 'win1251') console.log(`${getCurrentTime()} [Request] ${urlFiles} (RuTracker Files)`) } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `The ${error.hostname} server is not available` } } const dataFiles = cheerio.load(html) dataFiles('ul.ftree > li.dir > ul > li').each((index, element) => { const fileName = dataFiles(element).find('b').text().trim() const fileSize = dataFiles(element).find('i').text().trim() torrents.push({ Name: fileName, Size: fileSize }) }) } return [ { Name: Name, Url: url, Hash: Hash, Magnet: addTrackerList(Hash,"RuTracker"), Torrent: Torrent, IMDb_link: imdb, Kinopoisk_link: kp, IMDb_id: imdb.replace(/[^0-9]/g, ''), Kinopoisk_id: kp.replace(/[^0-9]/g, ''), Year: Year.replace(/:\s/g, ''), Release: Release.replace(/:\s/g, ''), Type: Type.replace(/:\s/g, ''), Duration: Duration.replace(/:\s/g, '').replace(/~ |~/g, ''), Audio: Audio.replace(/:\s/g, ''), Directer: Directer.replace(/:\s/g, ''), Actors: Actors.replace(/:\s/g, ''), Description: Description.replace(/:\s/g, ''), Quality: videoQuality.replace(/:\s/g, ''), Video: Video.replace(/:\s/g, ''), Poster: Poster, Files: torrents } ] } async function RuTrackerFilesPuppetter(query) { const torrents = [] const url = `https://rutracker.org/forum/viewtopic.php?t=${query}` const launchOptions = { // Скрыть отображение браузера (по умолчанию) headless: true, // Опции запуска браузера без песочницы, которая изолирует процессы от операционной системы (для работы через Docker) args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-quic' ] } // Добавляем Proxy в конфигурацию запуска браузера if (argv.proxyAddress && argv.proxyPort) { launchOptions.args.push(`--proxy-server=http://${argv.proxyAddress}:${argv.proxyPort}`) } // Запускаем браузер const browser = await puppeteer.launch(launchOptions) // Открываем новую пустую страницу const page = await browser.newPage() // Авторизация в Proxy if (argv.username && argv.password) { await page.authenticate({ username: argv.username, password: argv.password }) } // Устанавливаем Cookie const cookies = [ { name: 'bb_session', value: '0-44590272-Sp8wQfjonpx37QjDuZUD', domain: '.rutracker.org', path: '/' } ] for (const cookie of cookies) { await page.setCookie(cookie) } // Открываем страницу // await page.goto(`https://rutracker.org/forum/viewtopic.php?t=6489937`, {timeout: 60000, waitUntil: 'domcontentloaded'}) await page.goto(url, { // Ожиданием загрузку страницы 60 секунд timeout: 60000, // Ожидать только полной загрузки DOM (не ждать загрузки внешних ресурсов, таких как изображения, стили и скрипты) waitUntil: 'domcontentloaded' }) // Ожидаем загрузку кнопки на странице await page.waitForSelector('.lite') // Метод выполнения JavaScript в контексте страницы браузера await page.evaluate(() => { // Находим кнопку по пути JavaScript (по id) и нажимаем на нее document.querySelector("#tor-filelist-btn").click() // Находим все кпноки (по классу или id) // const buttons = document.querySelectorAll('.lite') // const buttons = document.querySelectorAll('#tor-filelist-btn') // Проходимся по найденным кнопкам // buttons.forEach(button => { // // Проверяем, содержит ли кнопка текст "Список файлов" и нажимаем на нее // if (button.textContent.includes('Список файлов')) { // button.click() // } // }) }) // Дожидаемся загрузки нового элемента // const elementHandle = await page.waitForSelector('#tor-filelist') // Находим элемент с идентификатором #tor-filelist или по классу .med.ftree-windowed await page.waitForFunction(() => { // Первый аргумент функция с условием, которая должна вернуть true const element = document.querySelector('#tor-filelist') // Проверяем, что элемент существует и его содержимое не содержит текст загрузки return element && !element.textContent.includes("загружается...") }, // Опции { // Ожидать результат 30 секунд (по умолчанию) timeout: 30000, // Проверка каждые 50мс (по умолчанию 100мс) polling: 50 }) // После успешной проверки возвращаем результат, используя метод textContent, innerText (массив) или innerHTML (включая HTML-разметку внутри элемента) или null const elementTable = await page.evaluate(() => { const element = document.querySelector('#tor-filelist') return element ? element.innerHTML : null }) // Закрываем браузер await browser.close() // Заполняем массив const dataFiles = cheerio.load(elementTable) dataFiles('li.file').each((index, element) => { const fileName = dataFiles(element).find('b').text().trim() const fileSize = dataFiles(element).find('s').text().trim() torrents.push({ Name: fileName, Size: fileSize }) }) return torrents } // RuTracker RSS Native async function RuTrackerRSS(typeData, categoryId) { const url = `https://feed.rutracker.cc/atom/f/${categoryId}.atom` console.log(`${getCurrentTime()} [Request] ${url}`) try { const response = await axiosProxy.get(url, { headers: headers }) if (typeData === "json") { const parser = new xml2js.Parser({ mergeAttrs: true, explicitArray: false }) let json = await parser.parseStringPromise(response.data) // Вытаскиваем только item (entry) для json json = json.feed.entry.map(item => ({ id: item.id, link: item.link.href, updated: item.updated, title: item.title, author: item.author.name, author: item.author.name, category: item.category.term, categoryLable: item.category.label })) return json } else { return response.data } } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `Server is not available` } } } // Kinozal async function Kinozal(query, categoryId, page, year, format) { if (query === 'undefined') { query = '' } const urls = [ 'https://kinozal.tv', 'https://kinozal.me', 'https://kinozal.guru' ] let checkUrl = false const torrents = [] let html let url for (const u of urls) { url = u.replace('https://','') const urlQuery = `${u}/browse.php?s=${query}&page=${page}&c=${categoryId}&d=${year}&v=${format}` try { const response = await axiosProxy.get(urlQuery, { timeout: 3000, responseType: 'arraybuffer', headers: headers }) html = iconv.decode(response.data, 'win1251') checkUrl = true console.log(`${getCurrentTime()} [Request] ${urlQuery}`) break } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) } } if (!checkUrl) { return { 'Result': `Server is not available` } } // Загружаем HTML-страницу с помощью Cheerio const data = cheerio.load(html) // Поиск таблицы с классом (.) t_peer, его дочернего элемента tbody и вложенных tr для перебора строк таблицы и извлечения данных из каждой строки data('.t_peer tbody tr').each((_, element) => { // Проверяем, что элемент с названием не пустой (пропустить первый элемент наименование столбцов) const checkData = data(element).find('.nam a') if (checkData.length > 0) { // Ищем дочерний элемент с классом 'nam' и его вложенным элементом 'a' torrentName = data(element).find('.nam a') // Забираем текст заголовка и разбиваем его на массив const Title = torrentName.text().trim() const arrTitle = Title.split(" / ") // Получаем количество элементов в заголовке // const count = arrTitle.length // +++ Анализ заголовка // Забираем все элементы 's' const s = data(element).find('.s') // Забираем дату из 3-его элемента 's' const sDate = s.eq(2).text().trim() // сейчас || сегодня в 15:17 || вчера в 23:51 || 06.10.2024 в 19:47 // Разбиваем дату на массив const dateArray = sDate.split(" ") let date let time // Получаем текущую дату и время const today = new Date() let currentDay = String(today.getDate()).padStart(2, '0') let currentMonth = String(today.getMonth() + 1).padStart(2, '0') // Месяцы начинаются с 0 let currentYear = today.getFullYear() // Проверяем и обновляем дату и время до формата dd.mm.yyyy и hh:mm if (dateArray.includes('сейчас')) { date = `${currentDay}.${currentMonth}.${currentYear}` time = `${today.getHours()}:${today.getMinutes()}` } // Получаем текущую дату и вытаскиваем время из массива else if (dateArray.includes('сегодня')) { date = `${currentDay}.${currentMonth}.${currentYear}` time = dateArray[2] } // Вычитаем один день else if (dateArray.includes('вчера')) { today.setDate(today.getDate() - 1) currentDay = String(today.getDate()).padStart(2, '0') currentMonth = String(today.getMonth() + 1).padStart(2, '0') currentYear = today.getFullYear() date = `${currentDay}.${currentMonth}.${currentYear}` time = dateArray[2] } else { date = dateArray[0] time = dateArray[2] } // Получем жанр по type id const categoryGetId = data(element).find("td.bt img")?.attr("onclick")?.match(/\d+/)[0] // Получаем название жанра по id через индекс массива const category = categoryList.Kinozal[categoryGetId] // Заполняем новый временный массив const torrent = { // Заполняем параметры из заголовка 'Name': Title.trim(), 'Title': arrTitle[0].trim(), 'Id': torrentName.attr('href').replace(/.+id=/, ''), 'Original_Name': arrTitle[1]?.trim() || '', 'Year': arrTitle[2]?.trim() || '', 'Language': arrTitle[3]?.trim() || '', 'Format': arrTitle[4]?.trim() || '', 'Url': `https://${url}` + torrentName.attr('href'), 'Torrent': `https://dl.${url}` + data(element).find('.nam a').attr('href').replace(/details/, 'download'), // Обновить наименования едениц измерений на англ. 'Size': s.eq(1).text().trim().replace(/КБ/g, 'KB').replace(/ГБ/g, 'GB').replace(/МБ/g, 'MB'), 'Comments': s.eq(0).text().trim(), 'Category': category, 'Seeds': data(element).find('.sl_s').text().trim(), 'Peers': data(element).find('.sl_p').text().trim(), 'Time': time, 'Date': date } torrents.push(torrent) } }) if (torrents.length === 0) { return { 'Result': 'No matches were found for your title' } } else { return torrents } } // Kinozal All Page async function KinozalAllPage(query, categoryId, year, format) { let result = [] page = 0 while (true) { let currentResult = await Kinozal(query, categoryId, page, year, format) if (Array.isArray(currentResult)) { currentResult.forEach(element => { result.push(element) }) } else { result = [{ 'Result': 'No matches were found for your title' }] break } // Максимум 100 страниц if (currentResult.length === 50 && page < 99) { page++ } else { break } } return result } // Kinozal ID async function KinozalID(query) { const url = `https://kinozal.tv/details.php?id=${query}` const torrents = [] let html try { const response = await axiosProxy.get(url, { responseType: 'arraybuffer', headers: headers }) html = iconv.decode(response.data, 'win1251') console.log(`${getCurrentTime()} [Request] ${url}`) } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `The ${error.hostname} server is not available` } } const data = cheerio.load(html) // Hash and files const url_get_srv_details = `https://kinozal.tv/get_srv_details.php?id=${query}&action=2` let html2 try { const response = await axiosProxy.get(url_get_srv_details, { responseType: 'arraybuffer', headers: headers_Kinozal }) html2 = iconv.decode(response.data, 'utf8') console.log(`${getCurrentTime()} [Request] ${url}`) } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `The ${error.hostname} server is not available` } } dataFiles = cheerio.load(html2) // Files const torrentFiles = [] dataFiles('div.treeview ul li').each((index, element) => { const fileName = dataFiles(element).text().trim() // Получаем текст из дочернего элемента <i> const fileSize = dataFiles(element).find('i').text().trim() torrentFiles.push({ // Удаляем размер из названия (разбиваем на массив, удаляем последние 3 элемента и объединяем обратно) Name: fileName.split(' ').slice(0, -3).join(' '), // Удаляем байты Size: fileSize.replace(/ \(.+/, '') }) }) // Проверяем количество элементов в массиве if (torrentFiles.length == 0) { dataFiles('div.b.ing').each((index, element) => { const fileName = dataFiles(element).text().trim() const fileSize = dataFiles(element).find('i').text().trim() torrentFiles.push({ Name: fileName.split(' ').slice(0, -3).join(' '), Size: fileSize.replace(/ \(.+/, '') }) }) } // IMDb let imdb data('a[href*="imdb.com"]').each((index, element) => { const href = data(element).attr('href') if (href.includes('imdb.com')) { imdb = href return false } }) if (!imdb) { imdb = "" } // Kinopoisk let kp data('a[href*="kinopoisk.ru"]').each((index, element) => { const href = data(element).attr('href') if (href.includes('kinopoisk.ru')) { kp = href return false } }) if (!kp) { kp = "" } let Hash = dataFiles('li').eq(0).text().replace(/.+:/, '').trim() // Постер let Poster = '' const posterElement = data('div.content > div.mn_wrap > div.mn1_menu > ul > li > a > img').attr('src') if (posterElement && posterElement.length) { // Проверка на внешний или внутренний источник постера if (posterElement.startsWith('http')) { Poster = posterElement } else { Poster = 'https://kinozal.tv' + posterElement } } // Массив из внешних постеров const url_posters = `https://kinozal.tv/get_srv_details.php?id=${query}&pagesd=2` let Posters = [] let html3 try { const response = await axiosProxy.get(url_posters, { responseType: 'arraybuffer', headers: headers_Kinozal }) html3 = iconv.decode(response.data, 'utf8') console.log(`${getCurrentTime()} [Request] ${url}`) } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `The ${error.hostname} server is not available` } } let dataPosters = cheerio.load(html3) // dataPosters('a').attr('href') // Перебрать все элементы с тэгом 'a' для получения значения их атрибута 'href' dataPosters('a').each((index, element) => { const href = dataPosters(element).attr('href') if (href) { Posters.push(href) } }) // Заполняем массив const torrent = { 'Name': (() => { const element = data('div.mn1_content .bx1 b:contains("Название:")')[0] if (element) { const nextNode = element.nextSibling return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' } else { return '' } })(), 'Original_Name': (() => { // Обращаемся к элементу b по наименованию контейнера const element = data('div.mn1_content .bx1 b:contains("Оригинальное название:")')[0] // Проверяем наличие контейнера (что оно не является null или undefined) if (element) { // Свойство DOM, которое возвращает следующий узел после элемента <b> const nextNode = element.nextSibling // Используем тернарный оператор, проверяем, что nextNode не является null или undefined и тип узла равен текстовому значению DOM return nextNode && nextNode.nodeType === 3 ? nextNode.nodeValue.trim() : '' // Возвращаем текстовое содержимое узла или пустое значение } else { return '' } })(), 'Url': url, 'Hash': Hash, 'Magnet': addTrackerList(Hash,"Kinozal"), 'Torrent': `https://dl.kinozal.tv/download.php?id=${query}`, 'IMDb_link': imdb, 'Kinopoisk_link': kp, 'IMDB_id': imdb.replace(/[^0-9]/g, ''), 'Kinopoisk_id': kp.replace(/[^0-9]/g, ''), // Находим нужный контейнер который содержит год выпуска и забираем текстовое значение следующего узла 'Year': data('div.mn1_content .bx1 b:contains("Год выпуска:")')[0]?.nextSibling?.nodeValue?.trim() || '', 'Type': data('div.mn1_content').find('.lnks_tobrs').eq(0)?.text()?.trim() || '', 'Release': data('div.mn1_content').find('.lnks_tobrs').eq(1)?.text()?.trim() || '', 'Directer': data('div.mn1_content').find('.lnks_toprs').eq(0)?.text()?.trim() || '', 'Actors': data('div.mn1_content').find('.lnks_toprs').eq(1)?.text()?.trim() || '', // 'Description': data('div.mn1_content').find('.bx1.justify:eq(2) p b').eq(0)[0].nextSibling.nodeValue.trim(), 'Description': data('div#main div.content div.mn_wrap div.mn1_content div.bx1.justify p') ?.clone() // Клонируем элемент, чтобы не модифицировать исходный ?.children('b') // Выбираем все дочерние элементы 'b' ?.remove() // Удаляем их ?.end() // Возвращаемся к исходному элементу ?.text().trim() || '', 'Quality': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(0)[0]?.nextSibling?.nodeValue?.trim() || '', 'Video': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(1)[0]?.nextSibling?.nodeValue?.trim() || '', 'Audio': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(2)[0]?.nextSibling?.nodeValue?.trim() || '', 'Size': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(3)[0]?.nextSibling?.nodeValue?.trim() || '', // 'Size': data('div.mn1_menu').find('span.floatright.green.n').eq(0).text().replace(/\(.+/, '').trim(), 'Duration': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(4)[0]?.nextSibling?.nodeValue?.trim() || '', 'Transcript': data('div.mn1_content').find('.justify.mn2.pad5x5 b').eq(5)[0]?.nextSibling?.nodeValue?.trim() || '', 'Seeds': data('div.mn1_menu').find('span.floatright').eq(0)?.text()?.trim() || '', 'Peers': data('div.mn1_menu').find('span.floatright').eq(1)?.text()?.trim() || '', 'Download_Count': data('div.mn1_menu').find('span.floatright').eq(2)?.text()?.trim() || '', 'Files_Count': data('div.mn1_menu').find('span.floatright').eq(3)?.text()?.trim() || '', 'Comments': data('div.mn1_menu').find('span.floatright').eq(4)?.text()?.trim() || '', 'IMDb_Rating': data('div.mn1_menu').find('span.floatright').eq(5)?.text()?.trim() || '', 'Kinopoisk_Rating': data('div.mn1_menu').find('span.floatright').eq(6)?.text()?.trim() || '', 'Kinozal_Rating': data('div.mn1_menu').find('span.floatright').eq(7)?.text()?.trim().replace(/\s.+/, '') || '', 'Votes': data('div.mn1_menu').find('span.floatright').eq(8)?.text()?.trim() || '', 'Added_Date': data('div.mn1_menu').find('span.floatright.green.n').eq(1)?.text()?.trim() || '', 'Update_Date': data('div.mn1_menu').find('span.floatright.green.n').eq(2)?.text()?.trim() || '', 'Poster': Poster, 'Posters': Posters, 'Files': torrentFiles } torrents.push(torrent) if (torrents.length === 0) { return { 'Result': 'No matches were found for your title' } } else { return torrents } } // Kinozal RSS Custom (через основную функцию) async function KinozalRssCustom(typeData, categoryId, year, format) { const dataKinozal = await Kinozal('', categoryId, 0, year, format) const torrents = [] if (dataKinozal.length === 50) { dataKinozal.forEach(element => { let dateArray = element.Date.split('.') let time = element.Time // Получаем формат: YYYY-MM-DDTHH:MM:SS+00:00 let updateDate = `${dateArray[2]}-${dateArray[1]}-${dateArray[0]}T${time}:00+00:00` const torrent = { 'date': updateDate, 'title': element.Name, 'category': element.Category, 'link': element.Url , 'downloadLink': element.Torrent, 'size': element.Size, 'comments': element.Comments, 'seeds': element.Seeds, 'peers': element.Peers } torrents.push(torrent) }) } // Получаем параметр описания по категории let description = 'Раздачи на главной торрент трекера' if ( (categoryId >= 0 && categoryId <= 24) || // 0-24 (categoryId === 32) || (categoryId === 35) || (categoryId === 37) || (categoryId === 38) || (categoryId === 39) || (categoryId >= 40 && categoryId <= 50) || // 40-50 (categoryId >= 1001 && categoryId <= 1006) // 1001-1006 ) { description = categoryList.Kinozal[categoryId] } if (torrents.length === 0) { return { 'Result': `Server is not available` } } else { if (typeData === "json") { return torrents } else { const builder = new xml2js.Builder() const rss = { rss: { $: { version: '2.0' }, channel: { title: 'Торрент трекер Кинозал', link: 'https://kinozal.tv', description: description, language: 'ru-ru', pubDate: new Date(Date.now() + 3 * 60 * 60 * 1000).toUTCString().replace('GMT', '+0300'), lastBuildDate: new Date(Date.now() + 3 * 60 * 60 * 1000).toUTCString().replace('GMT', '+0300'), item: torrents.map(torrent => ({ title: torrent.title, category: torrent.category, link: torrent.link, pubDate: torrent.date, size: torrent.size, comments: torrent.comments, seeds: torrent.seeds, peers: torrent.peers, enclosure: { $: { url: torrent.downloadLink, type: 'application/x-bittorrent' } } })) } } } return builder.buildObject(rss) } } } // Kinozal RSS Native async function KinozalRSS(typeData) { const url = "https://kinozal.tv/rss.xml" console.log(`${getCurrentTime()} [Request] ${url}`) try { const response = await axiosProxy.get(url, { headers: headers }) if (typeData === "json") { const parser = new xml2js.Parser({ mergeAttrs: true, // Атрибуты элемента XML включаются в список дочерних элементов вместо добавления в отдельный объект с ключом $ explicitArray: false // Элементы, которые встречаются только один раз, преобразуются в объект, а не в массив }) let json = await parser.parseStringPromise(response.data) // Вытаскиваем только item для json json = json.rss.channel.item.map(item => ({ title: item.title, link: item.link, category: item.category, guid: item.guid, pubDate: item.pubDate })) return json } else { return response.data } } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) return { 'Result': `Server is not available` } } } // RuTor async function RuTor(query, categoryId, page) { let urls = [] if (query === 'undefined' || !query || query === '' || query.length === 0) { urls = [ `https://rutor.info/browse/${page}/${categoryId}/0/0`, `https://rutor.is/browse/${page}/${categoryId}/0/0` ] } else { urls = [ `https://rutor.info/search/${page}/${categoryId}/300/0/${query}`, `https://rutor.is/search/${page}/${categoryId}/300/0/${query}` ] } let checkUrl = false const torrents = [] let html let url for (const urlQuery of urls) { url = urlQuery.replace(/^(https:\/\/rutor\.[a-z]{2,4}).+/, "$1") // подстановочный параметр для рабочего url try { const response = await axiosProxy.get(urlQuery, { timeout: 3000, responseType: 'arraybuffer', headers: headers }) // Декодируем HTML-страницу в кодировку UTF-8 html = iconv.decode(response.data, 'utf8') checkUrl = true console.log(`${getCurrentTime()} [Request] ${urlQuery}`) break } catch (error) { console.error(`${getCurrentTime()} [ERROR] ${error.hostname} server is not available (Code: ${error.code})`) } } if (!checkUrl) { return { 'Result': `Server is not available` } } const data = cheerio.load(html) data('table:eq(2) tbody tr').each((_, element) => { const checkData = data(element).find('a:eq(2)') if (checkData.length > 0) { // Проверяем количетсов элементов 'td' const count = data(element).find('td').length // Если 5 элементов, то 3-й индекс содержит размер, если 4, то 2-й индекс const sizeIndex = count === 5 ? 3 : count === 4 ? 2 : 1 // Если 5 элементов, то есть комментарии и забираем их количество из 2 индекса const comments = count === 5 ? data(element).find('td:eq(2)').text().trim() : count === 5 ? 0 : "0" const torrent = { 'Name': data(element).find('a:eq(2)').text().trim(), 'Id': data(element).find('a:eq(2)').attr('href').replace(/\/torrent\//g, "").replace(/\/.+/g, ""), 'Url': url + data(element).find('a:eq(2)').attr('href'), 'Torrent': "https:" + data(element).find('a:eq(0)').attr('href'), 'Hash': data(element).find('a:eq(1)').attr('href').replace(/.+btih:|&.+/g, ''), 'Size': data(element).find(`td:eq(${sizeIndex})`).text().trim(), 'Comments': comments, 'Seeds': data(element).find('.green').text().trim(), 'Peers': data(element).find('.red').text().trim(), // 'Date': data(element).find('td:eq(0)').text().trim(), // Заменяем все символы пробела на обычные пробелы и форматируем дату (передаем пробел вторым параметром разделителя) 'Date': formatDate( data(element).find('td:eq(0)').text().trim().replace(/\s+/g, ' '), " " ) } torrents.push(torrent) } }) if (torrents.length === 0) { return { 'Result': 'No matches were found for your title' } } else { return torrents } } // RuTor All Page async function RuTorAllPage(query, categoryId) { let result = [] page = 0 while (true) { let currentResult = await RuTor(query, categoryId, page) if (Array.isArray(currentResult)) { currentResult.forEach(element => { result.push(element) }) } else { result = [{ 'Result': 'No matches were found for your title' }] break } // Максимум 20 страниц (20 по 100 = 2000 результатов) if (currentResult.length === 100 && page < 9) { page++ } else { break