pub / tw2html

Checks online status of streams on twitch.tv and lets you watch them
git clone https://https://src.jayvii.de/pub/tw2html.git
Home | Log | Files | Exports | Refs | README | RSS

commit 4da0f2c1af9de7a6b4a2979158ad52f4b58b2b71
parent 7b74f244c238b329439bdf9baf92341b02f4cd19
Author: JayVii <jayvii[AT]posteo[DOT]de>
Date:   Mon, 21 Oct 2024 17:19:50 +0200

feat: refactor

checking is done client-side and asynchronously via JS, which is A LOT faster.

Diffstat:
MREADME.md | 23+++++------------------
Massets/css/custom.css | 14++++++++++++--
Dassets/js/darkmode.js | 29-----------------------------
Massets/js/twitch.js | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Mindex.php | 182++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
5 files changed, 325 insertions(+), 162 deletions(-)

diff --git a/README.md b/README.md @@ -7,25 +7,12 @@ Checks online status of streams on twitch.tv and lets you watch them right here! ## Usage -You can simply call the "?streams" endpoint with a comma-separated list of your -favorite streams: +Simply open the t2html page, scroll down to the "Streams" section, insert the account +names you want to check on in a comma-separated list +(`streamerA,streamerB,streamerC,...`) and click the "submit" button. -``` -index.php?streams=gamingonlinux,linustechtips,... -``` - -Assuming you host this service under the URL "https://tw2html.example.com", you -can open up the streamlist like so: - -``` -https://tw2html.example.com/?streams=gamingonlinux,linustechtips -``` - -The site will show you a loading screen, while the status of streams are fetched -in the background. Once the process is finished, the site refreshes by itself -and shows you a list of online streams. You can watch them directly on tw2html -via iframe-embedding (this transfers your IP and user-agent information to -twitch.tv). +The list of streams will also be stored in a cookie, which is refreshed every time you +open the page. ## Requirements diff --git a/assets/css/custom.css b/assets/css/custom.css @@ -1,7 +1,7 @@ * { scroll-behavior: smooth; } -.preview { +.preview, .player, .player_container { height: auto; width: 100%; aspect-ratio: 16/9 !important; @@ -12,7 +12,7 @@ #searchbar { width: 100%; } -.chatwrapper, .chatwrapper > iframe { +.chat_container, .chat_container > iframe { width: 100%; height:100%; min-height: 800px; @@ -29,3 +29,13 @@ a.button:hover { color: var(--bg) !important; } + +.hidden { + display: none; +} + +textarea[name=streams] { + width: 100%; + min-height: 400px; +} + diff --git a/assets/js/darkmode.js b/assets/js/darkmode.js @@ -1,29 +0,0 @@ -// Toggle Theme function -function toggleTheme(set_mode = null) { - // Set Mode and switch toggle icon - if (set_mode === 0) { - document.querySelector("body").classList = ["list"]; - document.querySelector("#darkmodetoggle").style="display:inherit;"; - document.querySelector("#lightmodetoggle").style="display:none;"; - } else { - document.querySelector("body").classList = ["list dark"]; - document.querySelector("#darkmodetoggle").style="display:none;"; - document.querySelector("#lightmodetoggle").style="display:inherit;"; - } - // Mark mode in session storage - sessionStorage.setItem("dark-mode", set_mode); -} -// On Load: set dark mode if necessary -function initialTheme() { - if ( - // Case 1: Dark Mode is preferred and no cookie is set - (window.matchMedia('(prefers-color-scheme: dark)').matches && - sessionStorage.getItem("dark-mode") === null) || - // Case 2: Dark Mode is set via session Storage - sessionStorage.getItem("dark-mode") == 1 - ) { - toggleTheme(set_mode = 1); - } else { - toggleTheme(set_mode = 0); - } -} diff --git a/assets/js/twitch.js b/assets/js/twitch.js @@ -1,55 +1,200 @@ -function toggle_player(stream) { - var img = document.querySelector("#img-" + stream); - var ply = document.querySelector("#play-" + stream); - if (img != null) { - var player = document.createElement("iframe"); - player.allowFullscreen = "true"; - player.width = "100%"; - player.height = "100%"; - player.frameborder = "0"; - player.scrolling = "no"; - player.src = "https://player.twitch.tv/?channel=" + - stream + - "&parent=" + - window.location.hostname + - "&muted=false&volume=1&quality=auto"; - player.id = "play-" + stream; - player.classList.add("preview"); - img.replaceWith(player); +async function twitch_check_online(stream) { + + // Construct request + const status_url = new Request( + "https://static-cdn.jtvnw.net/previews-ttv/live_user_" + stream + + "-853x480.jpg" + ); + + // Get HTTP status code of status_url + const response_url = await fetch(status_url).then((response) => { + return response; + }); + + // refer online status from whether a redirect happened + const status = (status_url.url === response_url.url); + + // return online status + return(status); + +} + +async function tw2html_toggle_hidden(stream) { + + // get stream section + var section = document.getElementById("stream_" + stream); + + // check online status of stream + const status = await twitch_check_online(stream); + + // Debug output + console.log("Stream: " + stream + " | Status: " + status); + + // If stream is online, unhide it. If it is offline, hide it + if (status) { + + // Unhide section + section.classList.remove("hidden"); + + // Check whether player exists + var player = section.querySelectorAll("#player_" + stream); + + // Update preview image if player does NOT exist + if (player.length < 1) { + tw2html_update_preview(stream); + } + + } else { + + // Hide section + section.classList.add("hidden"); + + // Ensure iframe is removed + section.querySelectorAll("#player_" + stream).forEach(function(x) { + x.remove(); + }); + } - if (ply != null) { - var image = document.createElement("img"); - image.src = "https://static-cdn.jtvnw.net/previews-ttv/live_user_" + stream + "-1280x720.jpg"; - image.id = "img-" + stream; - image.classList.add("preview"); - ply.replaceWith(image); + + // Mark Loading indicator as done (unhide it) + document.getElementById("loading_indicator_" + stream).classList.remove( + "hidden" + ); + +} + +function tw2html_update_preview(stream) { + + // Construct current time + var now = new Date(); + + // Get image object + var img = document.getElementById("preview_" + stream); + + // Update preview image + img.src = "https://static-cdn.jtvnw.net/previews-ttv/live_user_" + stream + + "-1280x720.jpg" + + "?t=" + now.getTime(); // Add some index to force reload of image + + // Ensure preview image is visible + img.classList.remove("hidden"); + +} + +function tw2html_toggle_player(stream) { + + // get stream section + var section = document.getElementById("stream_" + stream); + + // if player-iframe exists, remove it and unhide preview image + var player = section.querySelectorAll("#player_" + stream); + + if (player.length > 0) { + + // unhide and update preview image + tw2html_update_preview(stream); + + // remove player + player.forEach(function(x) { x.remove(); }); + + // Hide player container + section.querySelector(".player_container").classList.add("hidden"); + + } else { + + // hide preview image + section.querySelector("#preview_" + stream).classList.add("hidden"); + + // unhide player container + section.querySelector(".player_container").classList.remove("hidden"); + + // Construct player iframe + var player = document.createElement("iframe"); + player.id = "player_" + stream; + player.classList.add("player"); + player.allowFullscreen = "true"; + player.width = "100%"; + player.height = "100%"; + player.frameborder = "0"; + player.scrolling = "no"; + player.src = "https://player.twitch.tv/?channel=" + + stream + + "&parent=" + + window.location.hostname + + "&muted=false&volume=1&quality=auto"; + + // inject player into stream container + section.querySelector(".player_container").prepend(player); + } + } -function toggle_chat(stream) { - var plho = document.querySelector("#chat-placeholder-" + stream); - var chat = document.querySelector("#chat-" + stream); - if (chat != null) { - var placeholder = document.createElement("div"); - placeholder.id = "chat-placeholder-" + stream; - chat.replaceWith(placeholder); +function tw2html_toggle_chat(stream) { + + // get stream section + var section = document.getElementById("stream_" + stream); + + // get chat object if it exists + var chat = section.querySelectorAll("#chat_" + stream); + + if (chat.length > 0) { + var chat = chat[0]; + } else { + var chat = null; } - if (plho != null) { - var chatembed = document.createElement("iframe"); - chatembed.width = "100%"; - chatembed.height = "100%"; - chatembed.frameborder = "0"; - chatembed.scrolling = "auto"; - chatembed.src = "https://www.twitch.tv/embed/" + - stream + - "/chat?parent=" + - window.location.hostname; - // Check for dark mode - if (window.matchMedia('(prefers-color-scheme: dark)')) { - chatembed.src = chatembed.src + "&darkpopout"; - } - chatembed.id = "chat-" + stream; - plho.replaceWith(chatembed); + + if (chat !== null) { + + chat.remove(); + + } else { + + // Create chat iframe + var chat = document.createElement("iframe"); + chat.id = "chat-" + stream; + chat.width = "100%"; + chat.height = "100%"; + chat.frameborder = "0"; + chat.scrolling = "auto"; + chat.src = "https://www.twitch.tv/embed/" + + stream + + "/chat?parent=" + + window.location.hostname; + + // If dark mode is used, apply it to the chat as well + if (window.matchMedia('(prefers-color-scheme: dark)')) { + chat.src = chat.src + "&darkpopout"; + } + + // inject chat into chat container + section.querySelector(".chat_container").prepend(chat); } + } +function tw2html_reload() { + + // Debug output + console.log("Reloading list of online streams...") + + // Hide loading indicator (hidden means "still loading") + document.querySelectorAll(".loading_indicator").forEach(function(x) { + x.classList.add("hidden"); + }) + + // gather list of sections (streams) + var sections = document.querySelectorAll(".streams"); + + // Loop through each section + for (i = 0; i < sections.length; i++) { + + // Gather stream name + var stream = sections[i].id.replace("stream_", ""); + + // check online status and untoggle hidden class asynchronously + tw2html_toggle_hidden(stream); + + } + +} diff --git a/index.php b/index.php @@ -7,27 +7,31 @@ <?php -// Fetch GET and POST arguments -if (is_null($_POST["streams"])) { - $loading_screen = true; - if (!is_null($_GET["streams"])) { - $channels = explode(",", $_GET["streams"]); - } else { - $channels = explode(",", $_COOKIE["streams"]); + /* Initilise streams array */ + $streams = array(); + + // Fetch POST argument or the COOKIE + // Cookie should have lowest priority (fallback), POST first (intend) + if (!is_null($_COOKIE["streams"])) { + $streams = explode(",", $_COOKIE["streams"]); + } + if (!is_null($_POST["streams"])) { + $streams = urldecode($_POST["streams"]); + $streams = preg_replace('/[^a-z0-9\-\_\.\,]+/', '', $streams); + $streams = explode(",", $streams); } -} else { - $loading_screen = false; - $channels = unserialize($_POST["streams"]); -} - -// Prepare streams array -$streams = array( - "stream" => $channels, - "desc" => array(), - "game" => array(), - "status" => array(), - "time" => array() -); + + // Sort streams by alphabet + sort($streams); + + /* refresh cookie */ + header( + "Set-Cookie: " . + "streams=" . implode(",", $streams) . ";" . + "Max-Age=" . 31536000 . "; " . /* 60 x 60 x 24 x 365 = 1 year */ + "Domain=" . $_SERVER["SERVER_NAME"] . "; " . + "SameSite=Strict;" + ); ?> @@ -41,74 +45,120 @@ $streams = array( <link rel="icon" type="image/png" sizes="128x128" href="assets/img/twitch_128.png"> <link rel="apple-touch-icon" href="assets/img/twitch.png"> <link rel="stylesheet" type="text/css" href="assets/css/simple.min.css"/> - <link rel="stylesheet" href="assets/css/loading.css" /> <link rel="stylesheet" href="assets/css/custom.css" /> <script async src="assets/js/twitch.js"></script> <link crossorigin="use-credentials" rel="manifest" href="manifest.json"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> </head> - <body> + <body onload="tw2html_reload();"> <header> + <!-- Buttons --> + <nav> + <a href="#" onclick="tw2html_reload();">Reload</a> + <a href="#streams">Streams</a> + <a href="https://src.jayvii.de/pub/tw2html">Development</a> + </nav> + <!-- Headline --> <h1>Streams</h1> - <!-- Save Button --> - <a - id="savebtn" - class="button" - href="#" - onclick="document.cookie='streams=<?php echo implode(",", $streams["stream"]); ?>;path=/;max-age=31536000;';document.querySelector('#savebtn').style.color='inherit';" + <!-- Loading Indicator --> + <div id="loading_indicators"> + <?php + foreach ($streams as $stream) { + ?> + <span + class="loading_indicator hidden" + id="loading_indicator_<?php echo $stream; ?>" + title="<?php echo $stream; ?>" + > + ■ + </span> <?php - if (implode(",", $streams["stream"]) != $_COOKIE["streams"]) { - echo "style=color:#db4325"; } ?> - > - Save - </a> - <!-- Reload Button --> - <a - class="button" - href="<?php echo "/?streams=" . implode(",", $streams["stream"]); ?>" - > - Reload - </a> - <!-- Live pseudo-button --> - <a class="button" href="#streams"> - Live: <span id="num_streams">0</span> - </a> + </div> </header> <!-- Search Bar --> - <form action="https://www.twitch.tv/search" method="GET" target="_blank" style="width:100%;margin-top:1em;"> - <input id="searchbar" type="text" id="searchInput" name="term" placeholder="Search on twitch.tv"> + <form + action="https://www.twitch.tv/search" + method="GET" + target="_blank" + style="width:100%;margin-top:1em;" + > + <input + id="searchbar" + type="text" + id="searchInput" + name="term" + placeholder="Search on twitch.tv" + > </form> - <div id="streams"> + <!-- Streams List --> + <?php -<?php + foreach ($streams as $stream) { -// Streams or Loading Screen -if (!$loading_screen) { - // Load Stream Data - include("lib/load_streams.php"); - // Build HTML from Stream Data - include("lib/build_html.php"); -} else { - // Load Loading Screen content - include("lib/loadingscreen.php"); -} + ?> -?> + <!-- Section for streamer <?php echo $stream; ?> (hidden by default) --> + <section class="hidden streams" id="stream_<?php echo $stream; ?>"> + + <!-- Headline: streamer name --> + <h2> + <a href="https://www.twitch.tv/<?php echo $stream; ?>" target="_blank"> + <?php echo $stream; ?> + </a> + </h2> + + <!-- player Container --> + <div class="player_container hidden"></div> - </div> + <!-- Preview Image --> + <img + id="preview_<?php echo $stream; ?>" + class="preview" + src="" + loading="lazy" + > + + <!-- Stream Button --> + <button onclick="tw2html_toggle_player('<?php echo $stream; ?>')"> + Toggle Stream + </button> + + <!-- Chat Collapsable --> + <details class="button"> + <summary onclick="tw2html_toggle_chat('<?php echo $stream; ?>');"> + Chat + </summary> + <div class="chat_container"></div> + </details> + + </section> + + <?php + + } + + ?> + + <!-- Stream List Form --> + <h2 id="streams">List of Streams</h2> + <p> + Please enter the Twitch.TV usernames of streams you want to check here, each + separated with a "," + </p> + <form action="/" method="POST"> + <textarea + name="streams" + placeholder="streamerA, streamerB, ..." + ><?php echo implode("," . PHP_EOL, $streams); ?></textarea> + <input type="submit" value="Submit &amp; Reload"> + </form> </body> - <!-- Script for counting active streams --> - <script> - var num_streams = document.querySelectorAll(".streamlisting").length; - document.querySelector("#num_streams").innerText = num_streams; - </script> - </html>