Refactored server/db.js and all routes to use asynchronous fs operations, refactored m3uParser and epgParser to use streaming (via readline and sax) to prevent errors with large files, updated server/routes/proxy.js to use native stream piping for images and video, reducing latency and memory overhead
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
/**
|
||||
* M3U Playlist Parser
|
||||
* Parses EXTM3U format playlists and extracts channel information
|
||||
* M3U Playlist Parser (Streaming)
|
||||
* Parses EXTM3U format playlists and extracts channel information line-by-line
|
||||
*/
|
||||
|
||||
const readline = require('readline');
|
||||
const { Readable } = require('stream');
|
||||
|
||||
/**
|
||||
* Generate a simple stable ID from name and group
|
||||
* @param {string} name - Channel name
|
||||
@@ -21,69 +24,6 @@ function generateStableId(name, group) {
|
||||
return `m3u_${Math.abs(hash).toString(36)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse M3U playlist content
|
||||
* @param {string} content - Raw M3U playlist content
|
||||
* @returns {{ channels: Array, groups: Array }}
|
||||
*/
|
||||
function parse(content) {
|
||||
const lines = content.split('\n').map(line => line.trim());
|
||||
const channels = [];
|
||||
const groupsSet = new Set();
|
||||
|
||||
// Verify it's a valid M3U file
|
||||
if (!lines[0] || !lines[0].startsWith('#EXTM3U')) {
|
||||
throw new Error('Invalid M3U format: missing #EXTM3U header');
|
||||
}
|
||||
|
||||
let currentInfo = null;
|
||||
let currentGroup = null;
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('#EXTINF:')) {
|
||||
// Parse EXTINF line
|
||||
currentInfo = parseExtinf(line);
|
||||
if (currentInfo.groupTitle) {
|
||||
groupsSet.add(currentInfo.groupTitle);
|
||||
currentGroup = currentInfo.groupTitle;
|
||||
}
|
||||
} else if (line.startsWith('#EXTGRP:')) {
|
||||
// Parse EXTGRP line (alternative group specification)
|
||||
currentGroup = line.substring(8).trim();
|
||||
groupsSet.add(currentGroup);
|
||||
if (currentInfo) {
|
||||
currentInfo.groupTitle = currentGroup;
|
||||
}
|
||||
} else if (line && !line.startsWith('#')) {
|
||||
// This is a stream URL
|
||||
if (currentInfo) {
|
||||
const groupTitle = currentInfo.groupTitle || currentGroup || 'Uncategorized';
|
||||
// Generate a stable ID: use tvgId if present, otherwise hash name+group
|
||||
const stableId = currentInfo.tvgId || generateStableId(currentInfo.name, groupTitle);
|
||||
|
||||
channels.push({
|
||||
...currentInfo,
|
||||
id: stableId,
|
||||
url: line,
|
||||
groupTitle: groupTitle
|
||||
});
|
||||
currentInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert groups to array of objects
|
||||
const groups = Array.from(groupsSet).map((name, index) => ({
|
||||
id: `group_${index}`,
|
||||
name,
|
||||
channelCount: channels.filter(c => c.groupTitle === name).length
|
||||
}));
|
||||
|
||||
return { channels, groups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse EXTINF line and extract attributes
|
||||
* @param {string} line - EXTINF line
|
||||
@@ -138,6 +78,75 @@ function parseExtinf(line) {
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse M3U content (Stream or String)
|
||||
* @param {Readable|string} input - M3U content as Stream or String
|
||||
* @returns {Promise<{ channels: Array, groups: Array }>}
|
||||
*/
|
||||
async function parse(input) {
|
||||
const channels = [];
|
||||
const groupsSet = new Set();
|
||||
let currentInfo = null;
|
||||
let currentGroup = null;
|
||||
|
||||
let inputStream;
|
||||
if (typeof input === 'string') {
|
||||
inputStream = Readable.from([input]);
|
||||
} else {
|
||||
inputStream = input;
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: inputStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
for await (const line of rl) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.startsWith('#EXTINF:')) {
|
||||
// Parse EXTINF line
|
||||
currentInfo = parseExtinf(trimmed);
|
||||
if (currentInfo.groupTitle) {
|
||||
groupsSet.add(currentInfo.groupTitle);
|
||||
currentGroup = currentInfo.groupTitle;
|
||||
}
|
||||
} else if (trimmed.startsWith('#EXTGRP:')) {
|
||||
// Parse EXTGRP line (alternative group specification)
|
||||
currentGroup = trimmed.substring(8).trim();
|
||||
groupsSet.add(currentGroup);
|
||||
if (currentInfo) {
|
||||
currentInfo.groupTitle = currentGroup;
|
||||
}
|
||||
} else if (!trimmed.startsWith('#')) {
|
||||
// This is a stream URL
|
||||
if (currentInfo) {
|
||||
const groupTitle = currentInfo.groupTitle || currentGroup || 'Uncategorized';
|
||||
// Generate a stable ID: use tvgId if present, otherwise hash name+group
|
||||
const stableId = currentInfo.tvgId || generateStableId(currentInfo.name, groupTitle);
|
||||
|
||||
channels.push({
|
||||
...currentInfo,
|
||||
id: stableId,
|
||||
url: trimmed,
|
||||
groupTitle: groupTitle
|
||||
});
|
||||
currentInfo = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert groups to array of objects
|
||||
const groups = Array.from(groupsSet).map((name, index) => ({
|
||||
id: `group_${index}`,
|
||||
name,
|
||||
channelCount: channels.filter(c => c.groupTitle === name).length
|
||||
}));
|
||||
|
||||
return { channels, groups };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and parse M3U from URL
|
||||
* @param {string} url - M3U playlist URL
|
||||
@@ -148,8 +157,20 @@ async function fetchAndParse(url) {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch M3U: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const content = await response.text();
|
||||
return parse(content);
|
||||
|
||||
// Check if body is a Node.js stream (undici/node-fetch) or web stream
|
||||
let stream;
|
||||
if (response.body && typeof response.body.pipe === 'function') {
|
||||
stream = response.body;
|
||||
} else if (response.body) {
|
||||
// Convert Web Stream to Node Readable for readline
|
||||
stream = Readable.fromWeb(response.body);
|
||||
} else {
|
||||
// Fallback for empty body
|
||||
stream = Readable.from([]);
|
||||
}
|
||||
|
||||
return parse(stream);
|
||||
}
|
||||
|
||||
module.exports = { parse, parseExtinf, fetchAndParse };
|
||||
|
||||
Reference in New Issue
Block a user