/
marazmdev
/
russtube
Обзор
Документация
Войти
/
marazmdev
/
russtube
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
master
src/analyzer.js
219 строк
8 KB
TwixoffStudio
Create: index.html, analyzer.js, server.js, package.json, soon.soon, README.md
14 июн 2026, 14:38
Верифицирован
14 июн 2026, 14:38
93e525d
Код
Авторство
О чём код?
import axios from 'axios'; import * as cheerio from 'cheerio'; 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', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 'Accept-Language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7', 'Accept-Encoding': 'gzip, deflate, br', 'DNT': '1', 'Connection': 'keep-alive', 'Upgrade-Insecure-Requests': '1', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Cache-Control': 'max-age=0', }; /** * Основной анализатор — получает страницу russtube и извлекает прямую ссылку * @param {string} url - URL страницы russtube.ru * @returns {Promise<{title, thumbnail, videoUrl, type, sources}>} */ export async function analyzeUrl(url) { const response = await axios.get(url, { headers: HEADERS, timeout: 20000, maxRedirects: 5, }); const html = response.data; const $ = cheerio.load(html); const title = extractTitle($); const thumbnail = extractThumbnail($, url); const type = detectType(url); const sources = extractSources($, html, url); if (!sources || sources.length === 0) { throw new Error( 'Прямая ссылка на видео не найдена. Возможно, видео недоступно или страница изменила структуру.' ); } // Лучший источник — выбираем по качеству const best = pickBest(sources); return { title, thumbnail, videoUrl: best.url, type, quality: best.quality || 'auto', sources, }; } // ─── Извлечение заголовка ────────────────────────────────────────────────── function extractTitle($) { return ( $('meta[property="og:title"]').attr('content') || $('meta[name="twitter:title"]').attr('content') || $('h1.video-title').first().text().trim() || $('h1').first().text().trim() || $('title').text().replace(' - RussTube', '').trim() || 'Без названия' ); } // ─── Извлечение превью ──────────────────────────────────────────────────── function extractThumbnail($, pageUrl) { return ( $('meta[property="og:image"]').attr('content') || $('meta[name="twitter:image"]').attr('content') || $('video').attr('poster') || null ); } // ─── Определение типа (видео или шортс) ────────────────────────────────── function detectType(url) { if (url.includes('/shorts/') || url.includes('?shorts') || url.includes('shorts=')) { return 'shorts'; } return 'video'; } // ─── Главный экстрактор ссылок ───────────────────────────────────────────── function extractSources($, html, pageUrl) { const sources = []; // 1. <video> тег — src или дочерние <source> $('video').each((_, el) => { const src = $(el).attr('src'); if (src && isVideoUrl(src)) { sources.push({ url: resolveUrl(src, pageUrl), quality: 'auto', method: 'video-tag' }); } $(el).find('source').each((_, s) => { const ssrc = $(s).attr('src'); const label = $(s).attr('label') || $(s).attr('res') || 'auto'; if (ssrc && isVideoUrl(ssrc)) { sources.push({ url: resolveUrl(ssrc, pageUrl), quality: label, method: 'source-tag' }); } }); }); // 2. og:video meta-теги ['og:video', 'og:video:url', 'og:video:secure_url'].forEach(prop => { const content = $(`meta[property="${prop}"]`).attr('content'); if (content && isVideoUrl(content)) { sources.push({ url: content, quality: 'auto', method: 'og-meta' }); } }); // 3. JSON-LD schema.org $('script[type="application/ld+json"]').each((_, el) => { try { const data = JSON.parse($(el).html()); const found = extractFromSchema(data, pageUrl); sources.push(...found); } catch {} }); // 4. Inline JS — ищем паттерны file:, src:, url:, videoUrl, mp4 const jsPatterns = [ /(?:file|src|url|videoUrl|video_url|source|contentUrl)\s*[:=]\s*["']([^"']+\.(?:mp4|webm|m3u8|ogg)[^"']*)/gi, /["'](https?:\/\/[^"']+\.(?:mp4|webm|m3u8|ogg)[^"']*)/gi, /(?:hls|dash|stream)\s*[:=]\s*["']([^"']+)/gi, ]; for (const pattern of jsPatterns) { let match; while ((match = pattern.exec(html)) !== null) { const candidate = match[1]; if (isVideoUrl(candidate)) { sources.push({ url: resolveUrl(candidate, pageUrl), quality: 'auto', method: 'js-inline' }); } } } // 5. Ищем iframe с embed-видео (YouTube / VK / другие) $('iframe[src]').each((_, el) => { const src = $(el).attr('src') || $(el).attr('data-src'); if (src && (src.includes('youtube') || src.includes('vk.com/video_ext') || src.includes('rutube'))) { sources.push({ url: src, quality: 'embed', method: 'iframe' }); } }); // Дедупликация const seen = new Set(); return sources.filter(s => { if (seen.has(s.url)) return false; seen.add(s.url); return true; }); } // ─── JSON-LD парсер ──────────────────────────────────────────────────────── function extractFromSchema(data, pageUrl) { const results = []; if (!data) return results; const objs = Array.isArray(data) ? data : [data]; for (const obj of objs) { if (obj.contentUrl && isVideoUrl(obj.contentUrl)) { results.push({ url: resolveUrl(obj.contentUrl, pageUrl), quality: 'auto', method: 'schema-contentUrl' }); } if (obj.embedUrl) { results.push({ url: obj.embedUrl, quality: 'embed', method: 'schema-embedUrl' }); } // Рекурсивно по вложенным объектам for (const val of Object.values(obj)) { if (typeof val === 'object' && val !== null) { results.push(...extractFromSchema(val, pageUrl)); } } } return results; } // ─── Хелперы ────────────────────────────────────────────────────────────── function isVideoUrl(url) { if (!url || typeof url !== 'string') return false; if (url.startsWith('data:')) return false; return /\.(mp4|webm|m3u8|ogg|mov|avi|mkv|ts)(\?|$|#)/i.test(url) || url.includes('/hls/') || url.includes('/stream/') || url.includes('manifest') || url.includes('playlist.m3u8'); } function resolveUrl(url, base) { if (url.startsWith('http')) return url; try { return new URL(url, base).href; } catch { return url; } } const QUALITY_ORDER = ['1080', '720', '480', '360', '240', 'auto', 'embed']; function pickBest(sources) { // Предпочитаем mp4 напрямую, потом m3u8, потом embed const direct = sources.filter(s => /\.mp4/i.test(s.url)); if (direct.length) { return direct.sort((a, b) => { return QUALITY_ORDER.indexOf(a.quality) - QUALITY_ORDER.indexOf(b.quality); })[0]; } const hls = sources.filter(s => /\.m3u8/i.test(s.url)); if (hls.length) return hls[0]; return sources[0]; }