UNPKG

xdl-node

Version:

A library for retrieving audio streams and other data from X Spaces, built on Node.js and TypeScript.

260 lines (259 loc) 10 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const index_1 = require("../index"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); // Конфигурация const COOKIES_PATH = './cookies.txt'; const SPACE_URL = 'https://x.com/i/spaces/1kvJpyPkWjgxE'; const CHECK_INTERVAL = 10000; // 10 секунд const LOGS_DIR = './logs'; // Извлекаем ID из URL для имени файла const SPACE_ID = SPACE_URL.split('/').pop() || 'unknown'; const MERGED_PLAYLIST_FILE = path_1.default.join(LOGS_DIR, `${SPACE_ID}.m3u8`); const SEGMENTS_JSON_FILE = path_1.default.join(LOGS_DIR, `${SPACE_ID}_segments.json`); /** * Инициализирует директории */ function initDirectories() { if (!fs_1.default.existsSync(LOGS_DIR)) { fs_1.default.mkdirSync(LOGS_DIR, { recursive: true }); } } /** * Загружает существующие сегменты или создаёт новый Map */ function loadSegments() { const segments = new Map(); if (fs_1.default.existsSync(SEGMENTS_JSON_FILE)) { try { const data = JSON.parse(fs_1.default.readFileSync(SEGMENTS_JSON_FILE, 'utf8')); if (data.segments && Array.isArray(data.segments)) { data.segments.forEach((segment) => { const key = getSegmentKey(segment); segments.set(key, segment); }); } } catch (error) { // Тихая обработка ошибки } } return segments; } /** * Сохраняет информацию о сегментах и формирует объединённый m3u8 файл */ function saveSegments(segments, headers) { fs_1.default.writeFileSync(SEGMENTS_JSON_FILE, JSON.stringify({ lastUpdated: new Date().toISOString(), headers, segments: Array.from(segments.values()), }, null, 2)); let m3u8Content = ''; // Добавляем глобальные заголовки headers.forEach((header) => { m3u8Content += header + '\n'; }); // Сортируем сегменты по индексу const sortedSegments = Array.from(segments.values()).sort((a, b) => a.index - b.index); // Добавляем сегменты с их заголовками sortedSegments.forEach((segment) => { if (segment.headers) { segment.headers.forEach((header) => { m3u8Content += header + '\n'; }); } m3u8Content += segment.content + '\n'; }); fs_1.default.writeFileSync(MERGED_PLAYLIST_FILE, m3u8Content); } /** * Извлекает информацию о сегментах из плейлиста */ function parsePlaylist(playlistContent) { const lines = playlistContent.split('\n'); const segments = []; const discontinuities = []; const globalHeaders = []; let currentProgramDate = ''; let currentDuration = 0; let segmentIndex = 0; let currentHeaders = []; // Парсим глобальные заголовки до первого сегмента let i = 0; for (; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; if (line.startsWith('#')) { if (line.startsWith('#EXTINF:') || line.startsWith('#EXT-X-PROGRAM-DATE-TIME:') || line === '#EXT-X-DISCONTINUITY') { break; } globalHeaders.push(line); } } // Парсим сегменты for (; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; if (line.startsWith('#')) { if (line.startsWith('#EXT-X-PROGRAM-DATE-TIME:')) { currentProgramDate = line.substring(line.indexOf(':') + 1).trim(); currentHeaders.push(line); } else if (line.startsWith('#EXTINF:')) { const durationMatch = line.match(/#EXTINF:([\d.]+)/); if (durationMatch) { currentDuration = parseFloat(durationMatch[1]); } currentHeaders.push(line); } else if (line === '#EXT-X-DISCONTINUITY') { discontinuities.push(segmentIndex); currentHeaders.push(line); } else { currentHeaders.push(line); } } else if (line.endsWith('.aac') || line.endsWith('.ts') || line.includes('chunk_')) { let segmentIndexFromUrl = -1; const indexMatch = line.match(/chunk_[\d]+_([\d]+)_/); if (indexMatch && indexMatch[1]) { segmentIndexFromUrl = parseInt(indexMatch[1], 10); } segments.push({ url: line, index: segmentIndexFromUrl >= 0 ? segmentIndexFromUrl : segmentIndex, programDate: currentProgramDate, duration: currentDuration, content: line, headers: [...currentHeaders], }); currentHeaders = []; currentDuration = 0; currentProgramDate = ''; segmentIndex++; } } return { segments, discontinuities, headers: globalHeaders }; } /** * Генерирует ключ для сегмента */ function getSegmentKey(segment) { const match = segment.url.match(/chunk_[\d]+_([\d]+)_/); if (match && match[1]) { return match[1]; } return segment.url; } /** * Объединяет новые сегменты с существующими */ function mergeSegments(existingSegments, newSegments) { let added = 0; newSegments.forEach((segment) => { const key = getSegmentKey(segment); if (!existingSegments.has(key)) { existingSegments.set(key, segment); added++; } }); return { segments: existingSegments, added }; } /** * Обрабатывает и объединяет плейлист */ function processPlaylist(playlistContent) { const { segments, discontinuities, headers } = parsePlaylist(playlistContent); const existingSegments = loadSegments(); const { segments: mergedSegments, added } = mergeSegments(existingSegments, segments); saveSegments(mergedSegments, headers); const allSegments = Array.from(mergedSegments.values()); const sortedSegments = allSegments.sort((a, b) => a.index - b.index); return { totalSegments: sortedSegments.length, firstIndex: sortedSegments.length > 0 ? sortedSegments[0].index : -1, lastIndex: sortedSegments.length > 0 ? sortedSegments[sortedSegments.length - 1].index : -1, addedSegments: added, discontinuities: discontinuities.length, }; } /** * Мониторит и объединяет m3u8 поток */ async function monitorStream(xdl) { initDirectories(); console.log(`Мониторинг Space ID: ${SPACE_ID}`); console.log(`Интервал проверки: ${CHECK_INTERVAL / 1000} секунд`); console.log(`Объединённый плейлист: ${MERGED_PLAYLIST_FILE}`); let intervalId; let consecutiveNoNewCount = 0; let prevTotalSegments = 0; async function checkOnce() { try { const startTime = Date.now(); const m3u8Stream = await xdl.getM3u8Stream(SPACE_URL); const downloadTime = Date.now() - startTime; const stats = processPlaylist(m3u8Stream); const processTime = Date.now() - startTime - downloadTime; console.log(`[${new Date().toISOString().split('.')[0]}Z] ` + `Байт: ${m3u8Stream.length.toLocaleString()}, Добавлено: ${stats.addedSegments}, Всего: ${stats.totalSegments}, Время: ${downloadTime + processTime}ms`); if (stats.discontinuities > 0) { console.log(`⚠️ Разрывов: ${stats.discontinuities}`); } // Если общее число сегментов увеличилось - сбрасываем счётчик if (stats.totalSegments > prevTotalSegments) { consecutiveNoNewCount = 0; prevTotalSegments = stats.totalSegments; } else { consecutiveNoNewCount++; console.log(`Нет новых сегментов. ${consecutiveNoNewCount} запрос(ов) подряд без обновления.`); } if (consecutiveNoNewCount >= 3) { console.log(`Нет новых уникальных сегментов в течение 3 последовательных запросов. Стрим считается завершённым.`); clearInterval(intervalId); process.exit(0); } } catch (error) { console.error(`❌ ОШИБКА: ${error.message}`); } } await checkOnce(); intervalId = setInterval(checkOnce, CHECK_INTERVAL); process.on('SIGINT', () => { clearInterval(intervalId); console.log(`\nМониторинг завершён. Плейлист: ${MERGED_PLAYLIST_FILE}`); process.exit(0); }); } /** * Главная функция */ async function main() { try { const xdl = index_1.XDL.init(COOKIES_PATH); await monitorStream(xdl); } catch (error) { console.error(`Критическая ошибка: ${error.message}`); process.exit(1); } } if (require.main === module) { main().catch(console.error); }