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
JavaScript
;
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);
}