diff --git a/README.md b/README.md index 9aca71cb..1030276c 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ For the [environment variables](#environment-variables), you may specify these i | `WEB_IP` | `0.0.0.0` | IP to use for webserver | | `WEB_PORT` | `4124` | Port to use for webserver | | `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. | -| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.

⚠️ This should not be used unless you know what you're doing | +| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.

⚠️ This should not be used unless you know what you're doing | | `DIR_RUN` | `/usr/bin/app` | Path inside container where TVApp2 will be placed after it is built

⚠️ This should not be used unless you know what you're doing |
@@ -750,7 +750,7 @@ This docker container contains the following env variables: | `WEB_IP` | `0.0.0.0` | IP to use for webserver | | `WEB_PORT` | `4124` | Port to use for webserver | | `URL_REPO` | `https://git.binaryninja.net/BinaryNinja/` | Determines where the data files will be downloaded from. Do not change this or you will be unable to get M3U and EPG data. | -| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.

⚠️ This should not be used unless you know what you're doing | +| `DIR_BUILD` | `/usr/src/app` | Path inside container where TVApp2 will be built.

⚠️ This should not be used unless you know what you're doing | | `DIR_RUN` | `/usr/bin/app` | Path inside container where TVApp2 will be placed after it is built

⚠️ This should not be used unless you know what you're doing |
diff --git a/tvapp2/index.js b/tvapp2/index.js index a7e8bdd7..b4b7a66d 100755 --- a/tvapp2/index.js +++ b/tvapp2/index.js @@ -37,389 +37,411 @@ if (process.pkg) { } class Semaphore { - constructor(max) { - this.max = max; - this.queue = []; - this.active = 0; - } - async acquire() { - if (this.active < this.max) { - this.active++; - return; + constructor(max) { + this.max = max; + this.queue = []; + this.active = 0; } - return new Promise((resolve) => this.queue.push(resolve)); - } - release() { - this.active--; - if (this.queue.length > 0) { - const resolve = this.queue.shift(); - this.active++; - resolve(); + 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, + 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}`); + 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); + 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)); - }); - }); + 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(`Success: ${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; + 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); - }); - }); + 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); - }); + 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.'); + 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.'); } - 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(); - } - } - }); + 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('; '); + 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); - }); + 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; + 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('Failed to parse token JSON'); - return null; + log(`Fatal error fetching token: ${err.message}`); + 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; + 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(); } - 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)}`; - }); + 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 { + 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)}`; + 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); - response.writeHead(200, { - 'Content-Type': 'application/x-mpegURL', - 'Content-Disposition': 'inline; filename="playlist.m3u8"', - }); - response.end(updatedContent); + } catch (error) { - } catch (error) { + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { + 'Content-Type': 'text/plain' + }); + response.end(`Error serving playlist: ${error.message}`); - 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 { + 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'); + 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); + response.writeHead(200, { + 'Content-Type': 'application/xml', + 'Content-Disposition': 'inline; filename="xmltv.1.xml"', + }); + response.end(formattedContent); - } catch (error) { + } catch (error) { - console.error('Error in servePlaylist:', error.message); - response.writeHead(500, { 'Content-Type': 'text/plain' }); - response.end(`Error serving playlist: ${error.message}`); + console.error('Error in servePlaylist:', error.message); + response.writeHead(500, { + 'Content-Type': 'text/plain' + }); + response.end(`Error serving playlist: ${error.message}`); - } + } }; @@ -478,176 +500,202 @@ async function servePlaylist(response, req) { */ function setCache(key, value, ttl) { - const expiry = Date.now() + ttl; - cache.set(key, { value, expiry }); - log(`Cache set: ${key}, expires in ${ttl / 1000} seconds`); + 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; - } + 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}`); + 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}`); } - 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 = ` + 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:

-
-
-
-
-
- - - + + + + 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'); - }); + 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 () => {