feat: add ability to restart / resync m3u playlist and epg guide data

This commit is contained in:
2025-04-08 09:52:30 -07:00
parent 68c4778ed8
commit ec18ceb6db
3 changed files with 313 additions and 81 deletions

View File

@@ -1073,7 +1073,7 @@ const server = http.createServer( ( request, response ) =>
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}); });
response.end( `Restart triggered` ); response.end( `{ "status": "ok" }` );
return; return;
} }

View File

@@ -1,3 +1,19 @@
.table
{
--bs-table-bg: #1b1b1b;
--bs-table-border-color: #313131;
}
.table-striped > tbody > tr:nth-of-type(2n+1) > *
{
--bs-table-bg-type: #242424;
}
.table > :not(caption) > * > *
{
}
body body
{ {
background-color: #f8f9fa; background-color: #f8f9fa;
@@ -32,13 +48,35 @@ body
to { opacity: 1; } to { opacity: 1; }
} }
@keyframes fade-in-dimmer
{
from { opacity: 0; }
to { opacity: 0.90; }
}
@keyframes fade-in-dimmer-repeat
{
from { opacity: 0.90; }
to { opacity: 0.7; }
}
@keyframes scale-in @keyframes scale-in
{ {
from { from {
transform: scale(1, 1); transform: scale(1, 1);
} }
to { to {
transform: scale(1.1, 1.1); transform: scale(1.5, 1.5);
}
}
@keyframes spin-scale
{
to {
transform: rotate(0deg) scale(1, 1);
}
from {
transform: rotate(360deg) scale(1.5, 1.5);
} }
} }
@@ -115,30 +153,6 @@ p
margin-left: auto; margin-left: auto;
} }
.header .logo
{
animation-name: fade-in, scale-in;
animation-duration: 1s, 0.5s;
animation-timing-function: ease-in, linear;
animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1;
transition: all 0.3s;
opacity: 0.5;
transform: scale(1.1);
}
.header .restart
{
animation-name: fade-in, scale-in;
animation-duration: 1s, 0.5s;
animation-timing-function: ease-in, linear;
animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1;
transition: all 0.3s;
opacity: 0.5;
transform: scale(1.1);
}
.footer .footer
{ {
position: absolute; position: absolute;
@@ -151,8 +165,8 @@ p
.footer-inner .footer-inner
{ {
margin: 0; margin: 0;
padding-bottom: 20px; padding-bottom: 10px;
padding-top: 20px; padding-top: 10px;
background-color: #151515; background-color: #151515;
} }
@@ -177,7 +191,6 @@ p
{ {
background-color: #a82147; background-color: #a82147;
color: #fff; color: #fff;
height: 55px;
} }
.header .navbar-brand .header .navbar-brand
@@ -298,11 +311,10 @@ p
.navbar-social svg .navbar-social svg
{ {
font-size: clamp(0.7em, 2vw, 1.1em); font-size: clamp(0.7em, 2vw, 1.1em);
padding-top: 2px;
margin-left: 10px; margin-left: 10px;
} }
.navbar-social svg:hover .logo
{ {
animation-name: fade-in, scale-in; animation-name: fade-in, scale-in;
animation-duration: 1s, 0.5s; animation-duration: 1s, 0.5s;
@@ -310,19 +322,68 @@ p
animation-direction: alternate, alternate; animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1; animation-iteration-count: infinite, 1;
transition: all 0.3s; transition: all 0.3s;
opacity: 0.5;
transform: scale(1.2);
}
.logo:hover
{
animation-name: scale-in, spin;
animation-duration: 1s, 0.5s;
animation-timing-function: ease-in, linear;
animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1;
transition: all 0.3s;
opacity: 1; opacity: 1;
transform: scale(1.8); transform: scale(1.8);
} }
.restart
{
animation-name: fade-in, scale-in;
animation-duration: 1s, 0.5s;
animation-timing-function: linear, linear;
animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1;
transition: all 0.3s;
opacity: 0.5;
transform: scale(1.2);
}
.restart:hover
{
animation-name: spin-scale;
animation-duration: 1s;
animation-delay: 0s;
animation-timing-function: linear;
animation-direction: alternate;
animation-iteration-count: infinite;
transition: all 0.3s;
opacity: 1;
}
.spin
{
transition-property: transform;
transition-duration: 0.5s;
animation-name: spin;
animation-duration: 0.5s;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
.table thead th a .table thead th a
{ {
color: #9b9b9b !important; color: #9b9b9b !important;
font-weight: normal; font-weight: normal;
} }
.table td, .table th .table td, .table th
{ {
padding: .75rem; padding: .35rem;
vertical-align: baseline; vertical-align: baseline;
border-top: 0px solid #dee2e6; border-top: 0px solid #dee2e6;
font-size: clamp(0.5em, 1vw, 0.8em); font-size: clamp(0.5em, 1vw, 0.8em);
@@ -331,17 +392,13 @@ p
.table thead tr .table thead tr
{ {
border-bottom: 2px solid #575757; border-bottom: 2px solid #325eb3;
background-color: #181818;
color: #717171;
} }
.table thead th .table thead th
{ {
vertical-align: bottom; vertical-align: bottom;
border-bottom: 0px solid #575757; border-bottom: 0px solid #325eb3;
font-size: clamp(0.5em, 1vw, 0.8em);
line-height: 2.5vmin;
} }
.table-hover tbody tr:hover .table-hover tbody tr:hover
@@ -355,27 +412,62 @@ p
color: #d0c273; color: #d0c273;
} }
#warning-firewall .ntfy
{
padding-top: 1.5vh;
padding-bottom: 1.5vh;
padding-left: 2vh;
padding-right: 2vh;
margin-bottom: 1vh;
line-height: 25px;
width: 100%;
}
#ntfy-restart
{
background-color: #1a1a1a;
font-size: clamp(0.5em, 1vw, 0.8em);
font-weight: 100;
border: 1px dashed #9e973a;
display: none;
}
#ntfy-firewall
{ {
background-color: #1A1A1A; background-color: #1A1A1A;
padding: 2vh;
margin-bottom: 1vh;
font-size: clamp(0.5em, 1vw, 0.8em); font-size: clamp(0.5em, 1vw, 0.8em);
font-weight: 100; font-weight: 100;
border: 1px dashed #FF6C00; border: 1px dashed #FF6C00;
width: 100%; display: none;
line-height: 25px;
} }
#warning-localhost #ntfy-localhost
{ {
background-color: #1A1A1A; background-color: #1A1A1A;
padding: 2vh;
font-size: clamp(0.5em, 1vw, 0.8em); font-size: clamp(0.5em, 1vw, 0.8em);
font-weight: 100; font-weight: 100;
border: 1px dashed #FF0048; border: 1px dashed #FF0048;
width: 100%; display: none;
line-height: 25px; }
span.success
{
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;
animation-name: fade-in, scale-in;
animation-duration: 1s, 0.5s;
animation-timing-function: ease-in, linear;
animation-direction: alternate, alternate;
animation-iteration-count: infinite, 1;
transition: all 0.3s;
opacity: 0.5;
transform: scale(1.1);
} }
span.notice span.notice
@@ -421,8 +513,8 @@ span.warning
code code
{ {
font-size: 96%; font-size: 96%;
color: #ff4985; color: #ff4985 !important;
word-break: break-word; word-break: break-word !important;
padding-right: 5px; padding-right: 5px;
padding-left: 4px; padding-left: 4px;
} }
@@ -481,3 +573,60 @@ code
z-index: 999; z-index: 999;
} }
} }
.aboveDimmer
{
z-index: 301;
position: relative;
}
@-webkit-keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes fadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.dimmer-out
{
transition: opacity 1s ease-in-out !important;
opacity: 0 !important;
}
.dimmer-in
{
opacity: 0.7;
animation-name: fade-in-dimmer, fade-in-dimmer-repeat;
animation-delay: 0s, 0.7s;
animation-duration: 0.7s, 1s;
animation-timing-function: ease-in, linear;
animation-direction: alternate, alternate;
animation-iteration-count: 1, infinite;
transition: opacity 250ms ease-in, visibility 0ms ease-in 250ms;
}
#dimmer
{
z-index: 300;
position: fixed;
background-color: #000000;
width: 100%;
float: left;
height: 100%;
visibility: hidden;
top: 0px;
left: 0px;
}

View File

@@ -10,6 +10,7 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
</head> </head>
<body> <body>
<!-- Header -->
<div class="header"> <div class="header">
<nav class="navbar sticky-top container"> <nav class="navbar sticky-top container">
<div class="navbar-brand"> <div class="navbar-brand">
@@ -25,17 +26,19 @@
</nav> </nav>
</div> </div>
<!-- Header Notification: description -->
<div class="container"> <div class="container">
<div class="container header-container"> <div class="container header-container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<div class="about">This page displays your most recent copies of the <code><%= fileM3U %></code> playlist and <code><%= fileXML %></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><%= fileXML %></code> and <code><%= fileTAR %></code> have identical guide data, however the <code><%= fileTAR %></code> is compressed and will import into your IPTV application much faster.</div> <div class="about">This page displays your most recent copies of the <code><%= fileM3U %></code> playlist and <code><%= fileXML %></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><%= fileXML %></code> and <code><%= fileGZP %></code> have identical guide data, however the <code><%= fileGZP %></code> is compressed and will import into your IPTV application much faster.</div>
</div> </div>
</div> </div>
</div> </div>
<!-- Header Fontawesome Icons -->
<div class="container main-container"> <div class="container main-container">
<table id="list" class="table table-sm table-hover"> <table id="list" class="table table-dark table-striped">
<thead> <thead>
<tr class="d-none d-md-table-row"> <tr class="d-none d-md-table-row">
<td class="icon cell-icon"></td> <td class="icon cell-icon"></td>
@@ -94,12 +97,12 @@
</svg> </svg>
<!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> --> <!-- <i class="fa fa-fw fa-solid fa-file-lines" aria-hidden="true"></i> -->
</td> </td>
<td class="file cell-tar"> <td class="file cell-gzp">
<a id="tar-name" target="_blank"></a> <a id="gzp-name" target="_blank"></a>
</td> </td>
<td class="link cell-link"><a id="tar-link" target="_blank"></a></td> <td class="link cell-link"><a id="gzp-link" target="_blank"></a></td>
<td class="size cell-size"><span id="tar-size"></span></td> <td class="size cell-size"><span id="gzp-size"></span></td>
<td class="date cell-date"><span id="tar-date"></span></td> <td class="date cell-date"><span id="gzp-date"></span></td>
<td class="desc cell-desc">XML / EPG guide data <code>(compressed)</code></td> <td class="desc cell-desc">XML / EPG guide data <code>(compressed)</code></td>
</tr> </tr>
</tbody> </tbody>
@@ -107,10 +110,12 @@
</div> </div>
</div> </div>
<!-- Footer -->
<footer class="footer"> <footer class="footer">
<div class="container" style="padding-bottom:20px;"> <div class="container" style="padding-bottom:20px;">
<div id="warning-firewall" class="sticky-bottom"></div> <div id="ntfy-restart" class="ntfy ntfy-success sticky-bottom"></div>
<div id="warning-localhost" class="sticky-bottom"></div> <div id="ntfy-firewall" class="ntfy ntfy-warning sticky-bottom"></div>
<div id="ntfy-localhost" class="ntfy ntfy-danger sticky-bottom"></div>
</div> </div>
<div class="footer-inner"> <div class="footer-inner">
<div class="container"> <div class="container">
@@ -122,11 +127,30 @@
</div> </div>
</footer> </footer>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">TVApp2</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Do you ever feel like a plastic bag.... drifting through the wind?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Welcome to costco</button>
<button type="button" class="btn btn-primary">I'll be back</button>
</div>
</div>
</div>
</div>
<script> <script>
const urlBase = window.location.origin; const urlBase = window.location.origin;
const urlM3U = urlBase + "/playlist"; const urlM3U = urlBase + "/playlist";
const urlXML = urlBase + "/epg"; const urlXML = urlBase + "/epg";
const urlTAR = urlBase + "/gzip"; const urlGZP = urlBase + "/gzip";
document.getElementById("m3u-name").textContent = "<%= fileM3U %>"; document.getElementById("m3u-name").textContent = "<%= fileM3U %>";
document.getElementById("m3u-name").href = urlM3U; document.getElementById("m3u-name").href = urlM3U;
@@ -142,43 +166,72 @@
document.getElementById("xml-size").textContent = "<%= sizeXML %>"; document.getElementById("xml-size").textContent = "<%= sizeXML %>";
document.getElementById("xml-date").textContent = "<%= dateXML %>"; document.getElementById("xml-date").textContent = "<%= dateXML %>";
document.getElementById("tar-name").textContent = "<%= fileTAR %>"; document.getElementById("gzp-name").textContent = "<%= fileGZP %>";
document.getElementById("tar-name").href = urlTAR; document.getElementById("gzp-name").href = urlGZP;
document.getElementById("tar-link").textContent = urlTAR; document.getElementById("gzp-link").textContent = urlGZP;
document.getElementById("tar-link").href = urlTAR; document.getElementById("gzp-link").href = urlGZP;
document.getElementById("tar-size").textContent = "<%= sizeTAR %>"; document.getElementById("gzp-size").textContent = "<%= sizeGZP %>";
document.getElementById("tar-date").textContent = "<%= dateTAR %>"; document.getElementById("gzp-date").textContent = "<%= dateGZP %>";
</script> </script>
<script> <script>
/*
Notify > Localhost
*/
document.addEventListener("DOMContentLoaded", function() document.addEventListener("DOMContentLoaded", function()
{ {
const host = window.location.hostname; const host = window.location.hostname;
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
if (host === "localhost" || host === "127.0.0.1") if (host === "localhost" || host === "127.0.0.1")
{ {
const warning = document.createElement("div"); const msg = "<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.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 \
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> \ 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> \
<br> \ <br> \
<p> Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> or \ <p> Learn how to locate your IP address on <a href='https://youtube.com/watch?v=UAhDHXN2c6E' target = '_blank' > Windows</a> or \
<a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.</p>"; <a href='https://youtube.com/watch?v=gaIYP4TZfHI' target = '_blank' > Linux</a>.</p>";
document.getElementById("warning-localhost").appendChild(warning); document.getElementById("ntfy-localhost").innerHTML = msg;
document.getElementById("ntfy-localhost").style.display = "block";
} else { } else {
document.getElementById("warning-localhost").style.display = "none"; document.getElementById("ntfy-localhost").style.display = "none";
} }
}); });
/*
Notify > Firewall
*/
document.addEventListener("DOMContentLoaded", function() document.addEventListener("DOMContentLoaded", function()
{ {
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80"); const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
const warningMessage = "<p><span class='notice'>Notice</span> Port <strong> " + port + " </strong> must be open and allowed through your <a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> \ const msg = "<p><span class='notice'>Notice</span> Port <strong> " + port + " </strong> must be open and allowed through your <a href='https://youtu.be/zOZWlTplrcA?si=nGXrHKU4sAQsy18e&t=18 target='_blank'>Windows</a> \
or <a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> OS firewall settings \ or <a href='https://youtu.be/7c_V_3nWWbA?si=Hkd_II9myn-AkNnS&t=12' target='_blank'>Linux</a> OS firewall settings \
This action enables devices such as Firestick or Android to connect to the server and request the playlist through the proxy.</p>"; 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; document.getElementById("ntfy-firewall").innerHTML = msg;
document.getElementById("ntfy-firewall").style.display = "block";
}); });
/*
Notify > Restart / Resync
*/
document.addEventListener("DOMContentLoaded", function()
{
const port = window.location.port || (window.location.protocol === "https:" ? "443" : "80");
const msg = "<p><span class='success'>Success</span> Your IPTV m3u channels and xml guide data has been successfully re-synced. \
Please refresh this window to see new data</p>";
document.getElementById("ntfy-restart").innerHTML = msg;
document.getElementById("ntfy-restart").style.display = "none";
});
/*
Activate Resync
*/
function toggleRestart() function toggleRestart()
{ {
$.ajax( $.ajax(
@@ -187,6 +240,36 @@
type: 'POST', type: 'POST',
data: { data: {
x: 1 x: 1
},
beforeSend: function( data )
{
const dimmer = document.createElement('div');
dimmer.setAttribute("id", "dimmer");
dimmer.style.visibility = "visible";
dimmer.classList.add("dimmer-in");
document.getElementsByTagName('body')[0].appendChild(dimmer);
document.getElementById("ntfy-firewall").style.display = "none";
document.getElementById("ntfy-localhost").style.display = "none";
document.getElementById("ntfy-restart").style.display = "none";
const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("restart");
iconResync[0].classList.add("spin");
},
success: function( data )
{
setTimeout(() =>
{
document.getElementById("ntfy-restart").style.display = "block"
const dimmer = document.getElementById("dimmer");
dimmer.classList.remove("dimmer-in");
dimmer.classList.add("dimmer-out");
dimmer.remove();
const iconResync = document.getElementsByClassName('fa-rotate');
iconResync[0].classList.remove("spin");
iconResync[0].classList.add("restart");
setTimeout(location.reload.bind(location), 1000);
}, 1000);
} }
}); });
} }