const fs = require('fs'); const https = require('https'); const path = require('path'); const UserAgent = require('user-agents'); const http = require('http'); const os = require('os'); const zlib = require('zlib'); const { randomUUID } = require('crypto'); const { channel } = require('diagnostics_channel'); const cache = new Map(); let URLS_FILE; let FORMATTED_FILE; let EPG_FILE; const xmltv_epg = 'https://git.binaryninja.net/BinaryNinja/XMLTV-EPG/raw/branch/main/xmltv.1.xml'; const externalURL = 'https://git.binaryninja.net/BinaryNinja/tvapp2-externals/raw/branch/main/urls.txt'; const externalEPG = 'https://git.binaryninja.net/BinaryNinja/XMLTV-EPG/raw/branch/main/xmltv.1.xml'; const externalFORMATTED_1 = 'https://git.binaryninja.net/BinaryNinja/tvapp2-externals/raw/branch/main/formatted.dat'; const externalFORMATTED_2 = ''; const externalFORMATTED_3 = ''; const externalEvents = ''; if (process.pkg) { console.log('Process package'); const basePath = path.dirname(process.execPath); URLS_FILE = path.join(basePath, 'urls.txt'); FORMATTED_FILE = path.join(basePath, 'formatted.dat'); //EPG_FILE = path.join(basePath, 'epg.xml'); EPG_FILE = path.join(basePath, 'xmltv.1.xml'); EPG_FILE.length; } else { console.log('Process locals'); URLS_FILE = path.resolve(__dirname, 'urls.txt'); FORMATTED_FILE = path.resolve(__dirname, 'formatted.dat'); EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml'); } class Semaphore { constructor(max) { this.max = max; this.queue = []; this.active = 0; } async acquire() { if (this.active < this.max) { this.active++; return; } return new Promise((resolve) => this.queue.push(resolve)); } release() { this.active--; if (this.queue.length > 0) { const resolve = this.queue.shift(); this.active++; resolve(); } } } const semaphore = new Semaphore(5); let urls = []; let tokenData = { subdomain: null, token: null, url: null, validationUrl: null, cookies: null, }; let lastTokenFetchTime = 0; const log = (message) => { const now = new Date(); console.log(`[${now.toLocaleTimeString()}] ${message}`); }; async function downloadFile(url, filePath) { console.log(`Fetching ${url}`); return new Promise((resolve, reject) => { const isHttps = new URL(url).protocol === 'https:'; const httpModule = isHttps ? require('https') : require('http'); const file = fs.createWriteStream(filePath); httpModule .get(url, (response) => { if (response.statusCode !== 200) { console.error(`Failed to download file: ${url}. Status code: ${response.statusCode}`); return reject(new Error(`Failed to download file: ${url}. Status code: ${response.statusCode}`)); } response.pipe(file); file.on('finish', () => { log(`Sucess: ${filePath}`); file.close(() => resolve(true)); }); }) .on('error', (err) => { console.error(`Error downloading file: ${url}. Error: ${err.message}`); fs.unlink(filePath, () => reject(err)); }); }); } async function ensureFileExists(url, filePath) { try { await downloadFile(url, filePath); } catch (error) { if (fs.existsSync(filePath)) { console.warn(`Using existing file for ${filePath} due to download failure.`); } else { console.error(`Critical: Failed to download ${url}, and no local file exists.`); throw error; } } } // REMOVED REFERENCE CALLS TO THIS FUNCTION // TODO: UPDATES TO HANDLER FOR SPORT EVENTS async function fetchSportsData() { return new Promise((resolve, reject) => { const isHttps = new URL(externalEvents).protocol === 'https:'; const httpModule = isHttps ? require('https') : require('http'); httpModule .get(url, (response) => { if (response.statusCode !== 200) { console.error(`Failed to fetch sports data. Status code: ${response.statusCode}`); return reject(new Error(`Failed to fetch sports data. Status code: ${response.statusCode}`)); } let data = ''; response.on('data', (chunk) => (data += chunk)); response.on('end', () => { log('Fetched sports data successfully.'); resolve(data); }); }) .on('error', (err) => { console.error(`Error fetching sports data: ${err.message}`); reject(err); }); }); } async function fetchRemote(url) { return new Promise((resolve, reject) => { const mod = url.startsWith('https') ? https : http; mod .get(url, { headers: { 'Accept-Encoding': 'gzip, deflate, br' } }, (resp) => { if (resp.statusCode !== 200) { return reject(new Error(`HTTP ${resp.statusCode} for ${url}`)); } const chunks = []; resp.on('data', (chunk) => chunks.push(chunk)); resp.on('end', () => { const buffer = Buffer.concat(chunks); const encoding = resp.headers['content-encoding']; if (encoding === 'gzip') { zlib.gunzip(buffer, (err, decoded) => { if (err) return reject(err); resolve(decoded); }); } else if (encoding === 'deflate') { zlib.inflate(buffer, (err, decoded) => { if (err) return reject(err); resolve(decoded); }); } else if (encoding === 'br') { zlib.brotliDecompress(buffer, (err, decoded) => { if (err) return reject(err); resolve(decoded); }); } else { resolve(buffer); } }); }) .on('error', reject); }); } async function serveKey(req, res) { try { const uriParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('uri'); if (!uriParam) { res.writeHead(400, { 'Content-Type': 'text/plain' }); return res.end('Error: Missing "uri" parameter for key download.'); } const keyData = await fetchRemote(uriParam); res.writeHead(200, { 'Content-Type': 'application/octet-stream' }); res.end(keyData); } catch (err) { console.error('Error in serveKey:', err.message); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error fetching key.'); } } let gCookies = {}; const USERAGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'; function parseSetCookieHeaders(setCookieValues) { if (!Array.isArray(setCookieValues)) return; setCookieValues.forEach((line) => { const [cookiePair] = line.split(';'); if (cookiePair) { const [key, val] = cookiePair.split('='); if (key && val) { gCookies[key.trim()] = val.trim(); } } }); } function buildCookieHeader() { const pairs = []; for (const [k, v] of Object.entries(gCookies)) { pairs.push(`${k}=${v}`); } return pairs.join('; '); } function fetchPage(url) { return new Promise((resolve, reject) => { const opts = { method: 'GET', headers: { 'User-Agent': USERAGENT, Accept: '*/*', Cookie: buildCookieHeader(), }, }; https .get(url, opts, (res) => { if (res.statusCode !== 200) { return reject(new Error(`Non-200 status ${res.statusCode} => ${url}`)); } if (res.headers['set-cookie']) { parseSetCookieHeaders(res.headers['set-cookie']); } let data = ''; res.on('data', (chunk) => (data += chunk)); res.on('end', () => resolve(data)); }) .on('error', reject); }); } async function getTokenizedUrl(channelUrl) { try { const html = await fetchPage(channelUrl); let streamName; let streamHost; if (channelUrl.includes('espn-')) { streamName = 'ESPN'; } else if (channelUrl.includes('espn2-')) { streamName = 'ESPN2'; } else { const streamNameMatch = html.match(/id="stream_name" name="([^"]+)"/); if (!streamNameMatch) { log('No "stream_name" found'); return null; } streamName = streamNameMatch[1]; } if (channelUrl.match('tvpass\.org')) { streamHost = 'tvpass.org'; }; if (channelUrl.match('thetvapp\.to')) { streamHost = 'thetvapp.to'; }; const tokenUrl = `https://${streamHost}/token/${streamName}?quality=hd`; const tokenResponse = await fetchPage(tokenUrl); let finalUrl; try { const json = JSON.parse(tokenResponse); finalUrl = json.url; } catch (err) { log('Failed to parse token JSON'); return null; } if (!finalUrl) { log('No URL found in the token JSON'); return null; } log(`Tokenized URL: ${finalUrl}`); return finalUrl; } catch (err) { log(`Fatal error fetching token: ${err.message}`); return null; } } async function serveChannelPlaylist(req, res) { await semaphore.acquire(); try { const urlParam = new URL(req.url, `http://${req.headers.host}`).searchParams.get('url'); if (!urlParam) { log('Error: Missing URL parameter'); res.writeHead(400, { 'Content-Type': 'text/plain' }); res.end('Error: Missing URL parameter.'); return; } const decodedUrl = decodeURIComponent(urlParam); if (decodedUrl.endsWith('.ts')) { res.writeHead(302, { Location: decodedUrl }); res.end(); return; } const cachedUrl = getCache(decodedUrl); if (cachedUrl) { const rewrittenPlaylist = await rewritePlaylist(cachedUrl, req); res.writeHead(200, { 'Content-Type': 'application/vnd.apple.mpegurl', 'Content-Disposition': 'inline; filename="playlist.m3u8"', }); res.end(rewrittenPlaylist); return; } log(`Fetching stream: ${urlParam}`); const finalUrl = await getTokenizedUrl(decodedUrl); if (!finalUrl) { log('Error: Failed to retrieve tokenized URL'); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error: Failed to retrieve tokenized URL.'); return; } setCache(decodedUrl, finalUrl, 4 * 60 * 60 * 1000); const hdUrl = finalUrl.replace('tracks-v2a1', 'tracks-v1a1'); const rewrittenPlaylist = await rewritePlaylist(hdUrl, req); res.writeHead(200, { 'Content-Type': 'application/vnd.apple.mpegurl', 'Content-Disposition': 'inline; filename="playlist.m3u8"', }); res.end(rewrittenPlaylist); log('Served playlist'); } catch (error) { log(`Error processing request: ${error.message}`); if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Error processing request.'); } } finally { semaphore.release(); } } async function rewritePlaylist(originalUrl, req) { const rawData = await fetchRemote(originalUrl); const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; const playlistContent = rawData.toString('utf8'); return playlistContent .replace(/URI="([^"]+)"/g, (match, uri) => { const resolvedUri = new URL(uri, originalUrl).href; return `URI="${baseUrl}/key?uri=${encodeURIComponent(resolvedUri)}"`; }) .replace(/^([^#].*\.m3u8)(\?.*)?$/gm, (match, uri) => { const resolvedUri = new URL(uri, originalUrl).href; return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`; }) .replace(/^([^#].*\.ts)(\?.*)?$/gm, (match, uri) => { const resolvedUri = new URL(uri, originalUrl).href; return `${baseUrl}/channel?url=${encodeURIComponent(resolvedUri)}`; }); } async function servePlaylist(response, req) { try { const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8'); const updatedContent = formattedContent .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; }) .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => { return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; }); response.writeHead(200, { 'Content-Type': 'application/x-mpegURL', 'Content-Disposition': 'inline; filename="playlist.m3u8"', }); response.end(updatedContent); } catch (error) { console.error('Error in servePlaylist:', error.message); response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(`Error serving playlist: ${error.message}`); } } async function serveXmltv(response, req) { try { const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8'); response.writeHead(200, { 'Content-Type': 'application/xml', 'Content-Disposition': 'inline; filename="xmltv.1.xml"', }); response.end(formattedContent); } catch (error) { console.error('Error in servePlaylist:', error.message); response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(`Error serving playlist: ${error.message}`); } }; /* ORIGINAL ASYNC HANDLER - HOPE ALL IS WELL DTANK - JOB WELL DONE async function serveXmltv(response, req) { try { const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; //const sportsData = await fetchSportsData(); const formattedContent = fs.readFileSync(EPG_FILE, 'utf-8'); //const updatedContent = formattedContent //.replace(/#\[SPORTS\]/g, sportsData || '') //.replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { //return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; //}); response.writeHead(200, { 'Content-Type': 'application/x-mpegURL', 'Content-Disposition': 'inline; filename="playlist.m3u8"', }); response.end(updatedContent); } catch (error) { console.error('Error in servePlaylist:', error.message); response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(`Error serving playlist: ${error.message}`); } } async function servePlaylist(response, req) { try { const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; //const sportsData = await fetchSportsData(); const formattedContent = fs.readFileSync(FORMATTED_FILE, 'utf-8'); const updatedContent = formattedContent //.replace(/#\[SPORTS\]/g, sportsData || '') .replace(/(https?:\/\/[^\s]*thetvapp[^\s]*)/g, (fullUrl) => { return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; }) .replace(/(https?:\/\/[^\s]*tvpass[^\s]*)/g, (fullUrl) => { return `${baseUrl}/channel?url=${encodeURIComponent(fullUrl)}`; }); response.writeHead(200, { 'Content-Type': 'application/x-mpegURL', 'Content-Disposition': 'inline; filename="playlist.m3u8"', }); response.end(updatedContent); } catch (error) { console.error('Error in servePlaylist:', error.message); response.writeHead(500, { 'Content-Type': 'text/plain' }); response.end(`Error serving playlist: ${error.message}`); } } */ function setCache(key, value, ttl) { const expiry = Date.now() + ttl; cache.set(key, { value, expiry }); log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`); } function getCache(key) { const cached = cache.get(key); if (cached && cached.expiry > Date.now()) { return cached.value; } else { if (cached) log(`Cache expired for key: ${key}`); cache.delete(key); return null; } } async function initialize() { try { log('Initializing server...'); await ensureFileExists(externalURL, URLS_FILE); await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE); await ensureFileExists(externalEPG, EPG_FILE); urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean); if (urls.length === 0) { throw new Error(`No valid URLs found in ${URLS_FILE}`); } log('Initialization complete.'); } catch (error) { console.error(`Initialization error: ${error.message}`); } } const server = http.createServer((req, res) => { const handleRequest = async () => { const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); const host = req.headers.host; const baseUrl = `${protocol}://${host}`; if (req.url === '/' && req.method === 'GET') { const htmlContent = ` Playlist Details

Playlist Details

Playlist URL:

EPG URL:

`; res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(htmlContent); return; } if (req.url === '/playlist' && req.method === 'GET') { log('Playlist request received'); await servePlaylist(res, req); return; } if (req.url.startsWith('/channel') && req.method === 'GET') { await serveChannelPlaylist(req, res); return; } if (req.url.startsWith('/key') && req.method === 'GET') { await serveKey(req, res); return; } if (req.url === '/epg' && req.method === 'GET') { log('Epg request received'); await serveXmltv(res, req); return; /*res.writeHead(302, { Location: 'https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml', }); res.end(); return;*/ } res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); }; handleRequest().catch((error) => { console.error('Error handling request:', error); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); }); }); (async () => { await initialize(); const PORT = 4124; server.listen(PORT, '0.0.0.0', () => { log(`Server is running on port ${PORT}`); }); })();