mirror of
https://github.com/TheBinaryNinja/tvapp2.git
synced 2026-06-04 11:35:42 -04:00
feat: new dark theme for webserver page
This commit is contained in:
575
tvapp2/index.js
575
tvapp2/index.js
@@ -51,16 +51,44 @@ chalk.level = 3;
|
|||||||
let URLS_FILE;
|
let URLS_FILE;
|
||||||
let FORMATTED_FILE;
|
let FORMATTED_FILE;
|
||||||
let EPG_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`;
|
Define > Environment Variables || Defaults
|
||||||
const externalFORMATTED_2 = '';
|
*/
|
||||||
const externalFORMATTED_3 = '';
|
|
||||||
const envUrlRepo = process.env.URL_REPO || `https://git.binaryninja.net/binaryninja`;
|
const envUrlRepo = process.env.URL_REPO || `https://git.binaryninja.net/binaryninja`;
|
||||||
const envStreamQuality = process.env.STREAM_QUALITY || `hd`;
|
const envStreamQuality = process.env.STREAM_QUALITY || `hd`;
|
||||||
const envFilePlaylist = process.env.FILE_PLAYLIST || `playlist.m3u8`;
|
const envFilePlaylist = process.env.FILE_PLAYLIST || `playlist.m3u8`;
|
||||||
const envFileEPG = process.env.FILE_EPG || `xmltv.xml`;
|
const envFileEPG = process.env.FILE_EPG || `xmltv.xml`;
|
||||||
const LOG_LEVEL = process.env.LOG_LEVEL || 8;
|
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
|
Define > Logs
|
||||||
|
|
||||||
@@ -191,7 +219,6 @@ if (process.pkg) {
|
|||||||
const basePath = path.dirname(process.execPath);
|
const basePath = path.dirname(process.execPath);
|
||||||
URLS_FILE = path.join(basePath, 'urls.txt');
|
URLS_FILE = path.join(basePath, 'urls.txt');
|
||||||
FORMATTED_FILE = path.join(basePath, 'formatted.dat');
|
FORMATTED_FILE = path.join(basePath, 'formatted.dat');
|
||||||
//EPG_FILE = path.join(basePath, 'epg.xml');
|
|
||||||
EPG_FILE = path.join(basePath, 'xmltv.1.xml');
|
EPG_FILE = path.join(basePath, 'xmltv.1.xml');
|
||||||
EPG_FILE.length;
|
EPG_FILE.length;
|
||||||
} else {
|
} else {
|
||||||
@@ -201,6 +228,12 @@ if (process.pkg) {
|
|||||||
EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml');
|
EPG_FILE = path.resolve(__dirname, 'xmltv.1.xml');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Semaphore > Declare
|
||||||
|
|
||||||
|
allows multiple threads to work with the same shared resources
|
||||||
|
*/
|
||||||
|
|
||||||
class Semaphore {
|
class Semaphore {
|
||||||
constructor(max) {
|
constructor(max) {
|
||||||
this.max = max;
|
this.max = max;
|
||||||
@@ -224,22 +257,21 @@ class Semaphore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Semaphore > Initialize
|
||||||
|
|
||||||
|
@arg int threads_max
|
||||||
|
*/
|
||||||
|
|
||||||
const semaphore = new Semaphore(5);
|
const semaphore = new Semaphore(5);
|
||||||
|
|
||||||
let urls = [];
|
/*
|
||||||
let tokenData = {
|
Func > Download File
|
||||||
subdomain: null,
|
|
||||||
token: null,
|
|
||||||
url: null,
|
|
||||||
validationUrl: null,
|
|
||||||
cookies: null,
|
|
||||||
};
|
|
||||||
let lastTokenFetchTime = 0;
|
|
||||||
|
|
||||||
const log = (message) => {
|
@arg str url https://git.binaryninja.net/binaryninja/tvapp2-externals/raw/branch/main/urls.txt
|
||||||
const now = new Date();
|
@arg str filePath H:\Repos\github\BinaryNinja\tvapp2\tvapp2\urls.txt
|
||||||
console.log(`[${now.toLocaleTimeString()}] ${message}`);
|
@return Promise<>
|
||||||
};
|
*/
|
||||||
|
|
||||||
async function downloadFile(url, filePath) {
|
async function downloadFile(url, filePath) {
|
||||||
Log.info(`Fetching ${url}`)
|
Log.info(`Fetching ${url}`)
|
||||||
@@ -248,7 +280,6 @@ async function downloadFile(url, filePath) {
|
|||||||
const isHttps = new URL(url).protocol === 'https:';
|
const isHttps = new URL(url).protocol === 'https:';
|
||||||
const httpModule = isHttps ? https : http;
|
const httpModule = isHttps ? https : http;
|
||||||
const file = fs.createWriteStream(filePath);
|
const file = fs.createWriteStream(filePath);
|
||||||
|
|
||||||
httpModule
|
httpModule
|
||||||
.get(url, (response) => {
|
.get(url, (response) => {
|
||||||
if (response.statusCode !== 200) {
|
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) {
|
async function ensureFileExists(url, filePath) {
|
||||||
try {
|
try {
|
||||||
await downloadFile(url, filePath);
|
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) {
|
function parseSetCookieHeaders(setCookieValues) {
|
||||||
if (!Array.isArray(setCookieValues)) return;
|
if (!Array.isArray(setCookieValues)) return;
|
||||||
setCookieValues.forEach((line) => {
|
setCookieValues.forEach((line) => {
|
||||||
@@ -587,6 +628,10 @@ async function serveChannelPlaylist(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rewrites the URLs
|
||||||
|
*/
|
||||||
|
|
||||||
async function rewritePlaylist(originalUrl, req) {
|
async function rewritePlaylist(originalUrl, req) {
|
||||||
const rawData = await fetchRemote(originalUrl);
|
const rawData = await fetchRemote(originalUrl);
|
||||||
const protocol = req.headers['x-forwarded-proto']?.split(',')[0] || (req.socket.encrypted ? 'https' : 'http');
|
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) {
|
async function servePlaylist(response, req) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -634,17 +683,19 @@ async function servePlaylist(response, req) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
Log.error(`Error in servePlaylist:`, chalk.white(` → `), chalk.grey(`${error.message}`));
|
Log.error(`Error in servePlaylist:`, chalk.white(` → `), chalk.grey(`${error.message}`));
|
||||||
|
|
||||||
console.error('Error in servePlaylist:', error.message);
|
|
||||||
response.writeHead(500, {
|
response.writeHead(500, {
|
||||||
'Content-Type': 'text/plain'
|
'Content-Type': 'text/plain'
|
||||||
});
|
});
|
||||||
response.end(`Error serving playlist: ${error.message}`);
|
|
||||||
|
|
||||||
response.end(`Error serving playlist: ${error.message}`);
|
response.end(`Error serving playlist: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Serves IPTV .xml guide data
|
||||||
|
*/
|
||||||
|
|
||||||
async function serveXmltv(response, req) {
|
async function serveXmltv(response, req) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -670,65 +721,10 @@ async function serveXmltv(response, req) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
response.end(`Error serving playlist: ${error.message}`);
|
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) {
|
function setCache(key, value, ttl) {
|
||||||
const expiry = Date.now() + ttl;
|
const expiry = Date.now() + ttl;
|
||||||
cache.set(key, {
|
cache.set(key, {
|
||||||
@@ -756,8 +752,9 @@ async function initialize() {
|
|||||||
try {
|
try {
|
||||||
Log.info(`Initializing server...`);
|
Log.info(`Initializing server...`);
|
||||||
|
|
||||||
await ensureFileExists(externalFORMATTED_1, FORMATTED_FILE);
|
await ensureFileExists(extURL, URLS_FILE);
|
||||||
await ensureFileExists(externalEPG, EPG_FILE);
|
await ensureFileExists(extFormatted, FORMATTED_FILE);
|
||||||
|
await ensureFileExists(extEPG, EPG_FILE);
|
||||||
|
|
||||||
urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean);
|
urls = fs.readFileSync(URLS_FILE, 'utf-8').split('\n').filter(Boolean);
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
@@ -779,82 +776,352 @@ const server = http.createServer((req, res) => {
|
|||||||
const htmlContent = `<!DOCTYPE html>
|
const htmlContent = `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
<title>Playlist Details</title>
|
<title>TVApp2 - File Browser</title>
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/lightbox2/2.10.0/css/lightbox.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/TheBinaryNinja/tvapp2@main/tvapp2/web/css/tvapp2.min.css">
|
||||||
<style>
|
<style>
|
||||||
body {
|
body
|
||||||
font-family: Arial, sans-serif;
|
{
|
||||||
margin: 0;
|
background-color: #f8f9fa;
|
||||||
background-color: #fff;
|
padding-bottom: 20px;
|
||||||
padding: 20px;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
@media (prefers-color-scheme: dark)
|
||||||
|
{
|
||||||
|
body
|
||||||
|
{
|
||||||
|
background-color: #262626;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container
|
||||||
|
{
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container nav
|
||||||
|
{
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container .about
|
||||||
|
{
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-bottom: 4vh;
|
||||||
|
font-size: 1.6vmin;
|
||||||
|
line-height: 2.5vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb
|
||||||
|
{
|
||||||
|
background-color: transparent;
|
||||||
|
padding: 0rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb .breadcrumb-item a
|
||||||
|
{
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
html
|
||||||
|
{
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p
|
||||||
|
{
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 470px;
|
margin-bottom: 0;
|
||||||
margin: 0 auto;
|
padding-bottom: 20px;
|
||||||
|
padding-top: 20px;
|
||||||
|
background-color: #151515;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.footer a,
|
||||||
color: #333;
|
.footer a:focus,
|
||||||
margin-bottom: 20px;
|
.footer a:hover
|
||||||
|
{
|
||||||
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
.footer a
|
||||||
color: #007bff;
|
{
|
||||||
text-decoration: none;
|
color: #f7296c;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
.navbar
|
||||||
text-decoration: underline;
|
{
|
||||||
|
padding: 15px 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details p {
|
.header
|
||||||
margin: 10px 0;
|
{
|
||||||
color: #555;
|
background-color: #a82147;
|
||||||
|
color: #fff;
|
||||||
|
height: 55px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warning {
|
.header .navbar-brand
|
||||||
color: #ff4e4e;
|
{
|
||||||
|
padding: 0 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header a
|
||||||
|
{
|
||||||
|
color: #FFF;
|
||||||
|
text-decoration: none;
|
||||||
|
padding-left: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#breadcrumbs::before
|
||||||
|
{
|
||||||
|
margin-top: 4px;
|
||||||
|
padding-right: 15px;
|
||||||
|
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 576 512' fill='white' width='19px' height='19px'%3E%3Cdefs%3E%3Cstyle%3E.fa-secondary%7Bopacity:.4%7D%3C/style%3E%3C/defs%3E%3Cpath class='fa-primary' d='M160 384H512c35.3 0 64-28.7 64-64V160c0-35.3-28.7-64-64-64H394.5c-17 0-33.3-6.7-45.3-18.7L322.7 50.7c-12-12-28.3-18.7-45.3-18.7H160c-35.3 0-64 28.7-64 64V320c0 35.3 28.7 64 64 64z'%3E%3C/path%3E%3Cpath class='fa-secondary' d='M24 96c13.3 0 24 10.7 24 24V344c0 48.6 39.4 88 88 88H456c13.3 0 24 10.7 24 24s-10.7 24-24 24H136C60.9 480 0 419.1 0 344V120c0-13.3 10.7-24 24-24z'%3E%3C/path%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active
|
||||||
|
{
|
||||||
|
color: #6c757d;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb
|
||||||
|
{
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container
|
||||||
|
{
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list a,
|
||||||
|
#list a:focus
|
||||||
|
{
|
||||||
|
color: #FFF !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list a:hover
|
||||||
|
{
|
||||||
|
color: #ff275d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list colgroup
|
||||||
|
{
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#list .filename
|
||||||
|
{
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
table
|
||||||
|
{
|
||||||
|
margin-bottom: 10vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th a
|
||||||
|
{
|
||||||
|
color: #9b9b9b !important;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td, .table th
|
||||||
|
{
|
||||||
|
padding: .75rem;
|
||||||
|
vertical-align: top;
|
||||||
|
border-top: 0px solid #dee2e6;
|
||||||
|
font-size: 1.6vmin;
|
||||||
|
line-height: 2.5vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead tr
|
||||||
|
{
|
||||||
|
border-bottom: 2px solid #575757;
|
||||||
|
background-color: #181818;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th
|
||||||
|
{
|
||||||
|
vertical-align: bottom;
|
||||||
|
border-bottom: 0px solid #575757;
|
||||||
|
font-size: 1.6vmin;
|
||||||
|
line-height: 2.5vmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-hover tbody tr:hover
|
||||||
|
{
|
||||||
|
background-color: rgba(155, 155, 155, 0.075);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-accent
|
||||||
|
{
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 20px;
|
color: #d0c273;
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#firewall-warning {
|
#warning-firewall
|
||||||
margin-top: 20px;
|
{
|
||||||
|
background-color: #0F0F0F57;
|
||||||
|
padding: 2vh;
|
||||||
|
margin: 2vh;
|
||||||
|
font-size: 1.6vmin;
|
||||||
|
border: 1px dashed #FF6C00;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
line-height: 25px;
|
||||||
color: #555;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#warning-container p {
|
#warning-localhost
|
||||||
margin: 10px 0;
|
{
|
||||||
|
background-color: #0F0F0F57;
|
||||||
|
padding: 2vh;
|
||||||
|
margin: 2vh;
|
||||||
|
font-size: 1.6vmin;
|
||||||
|
border: 1px dashed #FF0048;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.notice
|
||||||
|
{
|
||||||
|
color: #FFF;
|
||||||
|
background-color: #97950A;
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.warning
|
||||||
|
{
|
||||||
|
color: #FFF;
|
||||||
|
background-color: #aa102d;
|
||||||
|
padding-left: 7px;
|
||||||
|
padding-right: 7px;
|
||||||
|
padding-top: 2px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
font-family: SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code
|
||||||
|
{
|
||||||
|
font-size: 96%;
|
||||||
|
color: #ff4985;
|
||||||
|
word-break: break-word;
|
||||||
|
padding-right: 5px;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark)
|
||||||
|
{
|
||||||
|
#list a,
|
||||||
|
#list a:focus,
|
||||||
|
#list a:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<center>
|
<div class="header">
|
||||||
<div class="container main-container">
|
<nav class="navbar sticky-top container">
|
||||||
<h1>Playlist Details</h1>
|
<div class="navbar-brand">
|
||||||
<div class="details">
|
<i class="fa-sharp-duotone fa-regular fa-tv" style="--fa-primary-color: rgb(255, 255, 255); --fa-secondary-color: rgb(255, 255, 255);" aria-hidden="true"></i>
|
||||||
<p>
|
<a href="https://github.com/Aetherinox/thetvapp-docker">TVApp2 for Docker</a>
|
||||||
<strong>Playlist URL:</strong>
|
</div>
|
||||||
<a id="playlist-url" target="_blank"></a>
|
</nav>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
|
||||||
<strong>EPG URL:</strong>
|
<div class="container header-container">
|
||||||
<a id="epg-url" target="_blank"></a>
|
<div class="row">
|
||||||
</p>
|
<div class="col">
|
||||||
|
<div class="about">This page displays your most recent copies of the <code>.m3u8</code> playlist and <code>.xml</code> EPG guide data. Right-click each file, select <span class="text-accent">Copy Link</span> and paste the URLs within an IPTV app such as Jellyfin. The <code>.m3u8</code> and <code>.m3u8.gz</code> are identical guide lists, but the <code>.xml.gz</code> is compressed and will import into your IPTV application much faster.</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="firewall-warning"></div>
|
|
||||||
<div id="warning-container" class="container"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</center>
|
</div>
|
||||||
|
|
||||||
|
<div class="container main-container">
|
||||||
|
<table id="list" class="table table-sm table-hover text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr class="d-none d-md-table-row">
|
||||||
|
<td class="col-auto"></td>
|
||||||
|
<th style="width:55%" class="col filename">
|
||||||
|
File Name
|
||||||
|
</th>
|
||||||
|
<th style="width:25%" class="col-auto d-none d-md-table-cell">
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td class="col-auto">
|
||||||
|
<svg class="svg-inline--fa fa-file-lines fa-fw" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-lines" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-fa-i2svg="">
|
||||||
|
<path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM112 256H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"></path>
|
||||||
|
</svg>
|
||||||
|
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
|
||||||
|
</td>
|
||||||
|
<td class="link col filename">
|
||||||
|
<a id="playlist-url" target="_blank"></a>
|
||||||
|
</td>
|
||||||
|
<td class="date col-auto d-none d-md-table-cell">Playlist data file which contains a list of all channels, their associated group, and logo URL.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="col-auto">
|
||||||
|
<svg class="svg-inline--fa fa-file-lines fa-fw" aria-hidden="true" focusable="false" data-prefix="fas" data-icon="file-lines" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" data-fa-i2svg="">
|
||||||
|
<path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V160H256c-17.7 0-32-14.3-32-32V0H64zM256 0V128H384L256 0zM112 256H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16zm0 64H272c8.8 0 16 7.2 16 16s-7.2 16-16 16H112c-8.8 0-16-7.2-16-16s7.2-16 16-16z"></path>
|
||||||
|
</svg>
|
||||||
|
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
|
||||||
|
</td>
|
||||||
|
<td class="link col filename">
|
||||||
|
<a id="epg-url" target="_blank"></a>
|
||||||
|
</td>
|
||||||
|
<td class="date col-auto d-none d-md-table-cell">XML / EPG guide data which contains a list of all programs which are scheduled to play on a specific channel.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div id="warning-firewall"></div>
|
||||||
|
<div id="warning-localhost" class="container"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer navbar">
|
||||||
|
<div class="container">
|
||||||
|
<div class="col text-center text-muted text-small text-nowrap">
|
||||||
|
<small>Developed by BinaryNinja - <a href="https://github.com/thebinaryninja/tvapp2">TVApp2</a></small><br />
|
||||||
|
<small>This utility is for educational purposes only</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const baseURL = window.location.origin;
|
const baseURL = window.location.origin;
|
||||||
const playlistURL = baseURL + "/playlist";
|
const playlistURL = baseURL + "/playlist";
|
||||||
@@ -864,30 +1131,36 @@ const server = http.createServer((req, res) => {
|
|||||||
document.getElementById("epg-url").textContent = "${envFileEPG}";
|
document.getElementById("epg-url").textContent = "${envFileEPG}";
|
||||||
document.getElementById("epg-url").href = epgURL;
|
document.getElementById("epg-url").href = epgURL;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function()
|
||||||
|
{
|
||||||
const host = window.location.hostname;
|
const host = window.location.hostname;
|
||||||
if (host === "localhost" || host === "127.0.0.1") {
|
if (host === "localhost" || host === "127.0.0.1")
|
||||||
const warning = document.createElement("div");
|
{
|
||||||
warning.style.color = "#ff4e4e";
|
const warning = document.createElement("div");
|
||||||
warning.style.fontSize = "14px";
|
warning.innerHTML = "<p><span class='warning'>Warning</span> If you are accessing this page via 127.0.0.1 or localhost, proxying will not work on other devices.Please load \
|
||||||
warning.style.textAlign = "center";
|
this page using your computer's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other devices on your network.</p> \
|
||||||
warning.style.fontWeight = "bold";
|
<p> How to locate IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' \
|
||||||
warning.innerHTML = " < p > Warning: If you are accessing this page via 127.0 .0 .1 or localhost, proxying will not work on other devices.Please load this page using your computer 's IP address (e.g., 192.168.x.x) and port in order to access the playlist from other devices on your network.</p>" +
|
target = '_blank' > Windows</a> or <a href='https://youtube.com/watch?v=gaIYP4TZfHI' \
|
||||||
" < p > How to locate IP address on < a href = 'https://www.youtube.com/watch?v=UAhDHXN2c6E'
|
target = '_blank' > Linux</a>.</p>";
|
||||||
target = '_blank' > Windows < /a> or < a href = 'https://www.youtube.com/watch?v=gaIYP4TZfHI'
|
|
||||||
target = '_blank' > Linux < /a>. < /p>";
|
document.getElementById("warning-localhost").appendChild(warning);
|
||||||
document.getElementById("warning-container").appendChild(warning);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
|
document.addEventListener("DOMContentLoaded", function()
|
||||||
const warningMessage = " < p > Ensure that port < strong > " + port + " < /strong> is open and allowed through your Windows ( < a href = 'https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18'
|
{
|
||||||
target = '_blank' > how to < /a>) or Linux ( < a href = 'https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12'
|
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
|
||||||
target = '_blank' > how to < /a>) firewall settings. This will enable other devices, such as Firestick, Android, and others, to connect to the server and request the playlist through the proxy. < /p>";
|
const warningMessage = "<p><span class='notice'>Notice</span> Port <strong> " + port + " </strong> must be open and allowed through your OS firewall settings \
|
||||||
document.getElementById("firewall-warning").innerHTML = warningMessage;
|
(<a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> | \
|
||||||
|
<a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a>). \
|
||||||
|
This action enables devices such as Firestick or Android to connect to the server and request the playlist through the proxy.</p>";
|
||||||
|
|
||||||
|
document.getElementById("warning-firewall").innerHTML = warningMessage;
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/gh/TheBinaryNinja/tvapp2@main/tvapp2/web/js/tvapp2.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>`;
|
</html>`;
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
@@ -939,11 +1212,6 @@ const server = http.createServer((req, res) => {
|
|||||||
|
|
||||||
await serveXmltv(res, req);
|
await serveXmltv(res, req);
|
||||||
return;
|
return;
|
||||||
/*res.writeHead(302, {
|
|
||||||
Location: 'https://raw.githubusercontent.com/dtankdempse/thetvapp-m3u/refs/heads/main/guide/epg.xml',
|
|
||||||
});
|
|
||||||
res.end();
|
|
||||||
return;*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.writeHead(404, {
|
res.writeHead(404, {
|
||||||
@@ -978,6 +1246,5 @@ const server = http.createServer((req, res) => {
|
|||||||
await initialize();
|
await initialize();
|
||||||
server.listen(envWebPort, envWebIP, () => {
|
server.listen(envWebPort, envWebIP, () => {
|
||||||
Log.info(`Server is running on ${envWebIP}:${envWebPort}`)
|
Log.info(`Server is running on ${envWebIP}:${envWebPort}`)
|
||||||
log(`Server is running on port ${PORT}`);
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user