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:
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 & 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>