diff --git a/tvapp2/index.js b/tvapp2/index.js index 2578da2a..d10d5509 100755 --- a/tvapp2/index.js +++ b/tvapp2/index.js @@ -51,16 +51,44 @@ chalk.level = 3; let URLS_FILE; let FORMATTED_FILE; let EPG_FILE; -const externalURL = `${process.env.URL_REPO}/tvapp2-externals/raw/branch/main/urls.txt`; -const externalEPG = `${process.env.URL_REPO}//XMLTV-EPG/raw/branch/main/xmltv.1.xml`; -const externalFORMATTED_1 = `${process.env.URL_REPO}/tvapp2-externals/raw/branch/main/formatted.dat`; -const externalFORMATTED_2 = ''; -const externalFORMATTED_3 = ''; + +/* + Define > Environment Variables || Defaults +*/ + const envUrlRepo = process.env.URL_REPO || `https://git.binaryninja.net/binaryninja`; const envStreamQuality = process.env.STREAM_QUALITY || `hd`; const envFilePlaylist = process.env.FILE_PLAYLIST || `playlist.m3u8`; const envFileEPG = process.env.FILE_EPG || `xmltv.xml`; const LOG_LEVEL = process.env.LOG_LEVEL || 8; + +/* + Define > Externals +*/ + +const extURL = `${envUrlRepo}/tvapp2-externals/raw/branch/main/urls.txt`; +const extEPG = `${envUrlRepo}/XMLTV-EPG/raw/branch/main/xmltv.1.xml`; +const extFormatted = `${envUrlRepo}/tvapp2-externals/raw/branch/main/formatted.dat`; +const extEvents = ''; + +/* + Define > Defaults +*/ + +let urls = []; +let tokenData = { + subdomain: null, + token: null, + url: null, + validationUrl: null, + cookies: null, +}; + +let lastTokenFetchTime = 0; + +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'; + /* Define > Logs @@ -191,7 +219,6 @@ if (process.pkg) { 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 { @@ -201,6 +228,12 @@ if (process.pkg) { EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml'); } +/* + Semaphore > Declare + + allows multiple threads to work with the same shared resources +*/ + class Semaphore { constructor(max) { this.max = max; @@ -224,22 +257,21 @@ class Semaphore { } } +/* + Semaphore > Initialize + + @arg int threads_max +*/ + const semaphore = new Semaphore(5); -let urls = []; -let tokenData = { - subdomain: null, - token: null, - url: null, - validationUrl: null, - cookies: null, -}; -let lastTokenFetchTime = 0; +/* + Func > Download File -const log = (message) => { - const now = new Date(); - console.log(`[${now.toLocaleTimeString()}] ${message}`); -}; + @arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt + @arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt + @return Promise<> +*/ async function downloadFile(url, filePath) { Log.info(`Fetching ${url}`) @@ -248,7 +280,6 @@ async function downloadFile(url, filePath) { const isHttps = new URL(url).protocol === 'https:'; const httpModule = isHttps ? https : http; const file = fs.createWriteStream(filePath); - httpModule .get(url, (response) => { if (response.statusCode !== 200) { @@ -276,6 +307,19 @@ async function downloadFile(url, filePath) { }); } +/* + Func > Ensure File Exists + + if file exists; start download from external website utilizing url and file path arguments; or + throw error to user that file does not exist via the URL. + + If file cannot be obtained from external url; use local copy if available + + @arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt + @arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt + @return none +*/ + async function ensureFileExists(url, filePath) { try { await downloadFile(url, filePath); @@ -406,9 +450,6 @@ async function serveKey(req, res) { } } -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) => { @@ -587,6 +628,10 @@ async function serveChannelPlaylist(req, res) { } } +/* + Rewrites the URLs +*/ + async function rewritePlaylist(originalUrl, req) { const rawData = await fetchRemote(originalUrl); const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http'); @@ -608,6 +653,10 @@ async function rewritePlaylist(originalUrl, req) { }); } +/* + Serves IPTV .m3u playlist +*/ + async function servePlaylist(response, req) { try { @@ -634,17 +683,19 @@ async function servePlaylist(response, req) { } catch (error) { Log.error(`Error in servePlaylist:`, chalk.white(` → `), chalk.grey(`${error.message}`)); - console.error('Error in servePlaylist:', error.message); response.writeHead(500, { 'Content-Type': 'text/plain' }); - response.end(`Error serving playlist: ${error.message}`); response.end(`Error serving playlist: ${error.message}`); } } +/* + Serves IPTV .xml guide data +*/ + async function serveXmltv(response, req) { try { @@ -670,65 +721,10 @@ async function serveXmltv(response, req) { }); 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, { @@ -756,8 +752,9 @@ async function initialize() { try { Log.info(`Initializing server...`); - await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE); - await ensureFileExists(externalEPG, EPG_FILE); + await ensureFileExists(extURL, URLS_FILE); + await ensureFileExists(extFormatted, FORMATTED_FILE); + await ensureFileExists(extEPG, EPG_FILE); urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean); if (urls.length === 0) { @@ -779,82 +776,352 @@ const server = http.createServer((req, res) => { const htmlContent = ` - - - Playlist Details + + + TVApp2 - File Browser + + + + + -
-
-

Playlist Details

-
-

- Playlist URL: - -

-

- EPG URL: - -

+
+ +
+ +
+
+
+
This page displays your most recent copies of the .m3u8 playlist and .xml EPG guide data. Right-click each file, select Copy Link and paste the URLs within an IPTV app such as Jellyfin. The .m3u8 and .m3u8.gz are identical guide lists, but the .xml.gz is compressed and will import into your IPTV application much faster.
-
-
-
+ + +
+ + + + + + + + + + + + + + + + + + + + +
+ File Name + + Description +
+ + + Playlist data file which contains a list of all channels, their associated group, and logo URL.
+ + + XML / EPG guide data which contains a list of all programs which are scheduled to play on a specific channel.
+
+ +
+
+
+
+ + + + + `; res.writeHead(200, { @@ -939,11 +1212,6 @@ const server = http.createServer((req, res) => { 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, { @@ -978,6 +1246,5 @@ const server = http.createServer((req, res) => { await initialize(); server.listen(envWebPort, envWebIP, () => { Log.info(`Server is running on ${envWebIP}:${envWebPort}`) - log(`Server is running on port ${PORT}`); }); })();