commit b1deee6c60ff8346a675c489b2dd0b638b226013
parent 8df3450c8640dce2a0604876804bb853400021c0
Author: JayVii <jayvii[AT]posteo[DOT]de>
Date: Mon, 21 Oct 2024 22:51:18 +0200
feat: refactor
entirely written in PHP now, instead of using an R-script in the background
Diffstat:
M | .htaccess | | | 17 | +++++------------ |
M | assets/css/custom.css | | | 49 | ++++++++++++++++++++++++++++++++----------------- |
M | assets/js/yt.js | | | 74 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------- |
A | index.php | | | 243 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | reload.php | | | 17 | ----------------- |
D | template.html | | | 45 | --------------------------------------------- |
D | url.csv.sample | | | 3 | --- |
D | yt.R | | | 233 | ------------------------------------------------------------------------------- |
8 files changed, 334 insertions(+), 347 deletions(-)
diff --git a/.htaccess b/.htaccess
@@ -9,16 +9,9 @@ IndexOptions +Charset=UTF-8
# Disallow browsing of certain sub-directories (redirect to 404)
RedirectMatch 404 ^/.git/.*$
-RedirectMatch 404 ^/.ssh/.*$
-RedirectMatch 404 ^/.config/.*$
-RedirectMatch 404 ^/.local/.*$
-RedirectMatch 404 ^/.cache/.*$
-RedirectMatch 404 ^/yt.R$
-RedirectMatch 404 ^/template.html$
-RedirectMatch 404 ^/url.csv$
+RedirectMatch 404 ^/.reuse/.*$
+RedirectMatch 404 ^/LICENSES/.*$
+RedirectMatch 404 ^/README.md$
+
+
-# Private Area
-AuthType Basic
-AuthName "Private Area! You need a password to access this."
-AuthUserFile /etc/apache2/.htpassword
-Require valid-user
diff --git a/assets/css/custom.css b/assets/css/custom.css
@@ -1,31 +1,46 @@
* {
- scroll-behavior: smooth;
+ scroll-behavior: smooth;
}
-
-img, iframe {
- height: auto;
+.preview, .player, .player_container {
+ height: auto;
+ width: 100%;
+ aspect-ratio: 4/3;
+ margin-top: 1em;
+ margin-bottom: 1em;
+ border: 0;
+}
+#searchbar {
width: 100%;
- border: 0;
+ margin-top:2em;
}
-iframe {
- aspect-ratio: 16/9;
+details {
+ width: 100% !important;
}
-a[href^="https"]:where([href*="www.youtube.com"])::after {
- content: " \2197";
+a.button:visited,
+a.button:hover
+{
+ color: var(--bg) !important;
}
-.thumbnails {
- width: 100%;
- aspect-ratio: 4/3;
+.hidden {
+ display: none;
}
-#searchbar {
- margin-top: 1em;
- width: 100%;
+textarea[name=channels] {
+ width: 100%;
+ min-height: 400px;
}
-details {
- width: 100% !important;
+@media only screen and (width<=720px) {
+ button, .button {
+ width: 100%;
+ align-content: center;
+ text-align: center;
+ }
+}
+
+a[href^="https"]:where([href*="www.youtube.com"])::after {
+ content: " \2197";
}
diff --git a/assets/js/yt.js b/assets/js/yt.js
@@ -1,21 +1,55 @@
-// Embed Youtube Video
-function embed_yt(oid, vid) {
- // Scroll to video section
- window.location = "#entry_" + oid;
- // Fetch thumbnail image to replace
- var img = document.querySelector("#thumbnail_" + oid);
- // Create player iframe-object and set
- var player = document.createElement("iframe");
- player.id = "videoplayer_" + oid;
- player.allowFullscreen = "true";
- player.sandbox = "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox";
- player.width = "100%";
- player.frameborder = "0";
- player.scrolling = "no";
- player.loading = "lazy";
- player.allow = "autoplay";
- player.src = "https://www.youtube-nocookie.com/embed/" + vid + "?autoplay=1";
- // Replace thumbnail image with player
- img.replaceWith(player);
-}
+function yt2html_toggle_player(index, video) {
+
+ console.log("index: " + index);
+ console.log("video: " + video);
+
+ // get video section
+ var section = document.getElementById("video_" + index);
+
+ // Preview image
+ var preview = section.querySelector("#preview_" + index);
+
+ // if player-iframe exists, remove it and unhide preview image
+ var player = section.querySelectorAll("#player_" + index);
+
+ if (player.length > 0) {
+
+ // unhide preview image
+ preview.classList.remove("hidden");
+
+ // remove player
+ player.forEach(function(x) { x.remove(); });
+
+ // Hide player container
+ section.querySelector(".player_container").classList.add("hidden");
+ } else {
+
+ // hide preview image
+ preview.classList.add("hidden");
+
+ // unhide player container
+ section.querySelector(".player_container").classList.remove("hidden");
+
+ // Construct player iframe
+ var player = document.createElement("iframe");
+ player.id = "player_" + index;
+ player.classList.add("player");
+ player.allowFullscreen = "true";
+ player.sandbox = "allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox";
+ player.width = "100%";
+ player.height = "100%";
+ player.frameborder = "0";
+ player.scrolling = "no";
+ player.src = "https://www.youtube-nocookie.com/embed/" + video +
+ "?autoplay=1";
+
+ // inject player into video container
+ section.querySelector(".player_container").prepend(player);
+
+ // Scroll to player
+ document.location = "#video_" + index;
+
+ }
+
+}
diff --git a/index.php b/index.php
@@ -0,0 +1,243 @@
+<!-- SPDX-License-Identifier: AGPL-3.0-or-later
+ SPDX-FileCopyrightText: 2021-2024 JayVii <jayvii[AT]posteo[DOT]de>
+-->
+
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+ <title>yt2html - Simply watch YouTube!</title>
+ <link rel="icon" type="image/png" href="/assets/favicon.png">
+ <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon_16.png">
+ <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon_32.png">
+ <link rel="icon" type="image/png" sizes="64x64" href="/assets/favicon_64.png">
+ <link rel="icon" type="image/png" sizes="128x128" href="/assets/favicon_128.png">
+ <link rel="apple-touch-icon" href="/assets/favicon.png">
+ <link rel="stylesheet" type="text/css" href="/assets/css/simple.min.css"/>
+ <link rel="stylesheet" type="text/css" href="/assets/css/custom.css"/>
+ <script async src="assets/js/yt.js"></script>
+ <script src="assets/js/thumbnails.js"></script>
+ <link crossorigin="use-credentials" rel="manifest" href="/manifest.json">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ </head>
+
+<?php
+
+ /* Sort by Time */
+ function cmp(array $a, array $b) {
+ return $b["time"] - $a["time"];
+ }
+
+ /* Initilise videos template */
+ $video_template = array(
+ "vid" => array(),
+ "title" => array(),
+ "desc" => array(),
+ "time" => array(),
+ "aid" => array(),
+ "author" => array()
+ );
+
+ /* Initilise channels array */
+ $channels = array();
+
+ /* Initilise videos array */
+ $videos = array();
+
+ // Fetch POST argument or the COOKIE
+ // Cookie should have lowest priority (fallback), POST first (intend)
+ if (!is_null($_COOKIE["channels"])) {
+ $channels = explode(",", $_COOKIE["channels"]);
+ }
+ if (!is_null($_POST["channels"])) {
+ $channels = urldecode($_POST["channels"]);
+ $channels = preg_replace('/[^A-Za-z0-9\-\_\,]+/', '', $channels);
+ $channels = explode(",", $channels);
+ }
+ if (!is_null($_GET["channels"])) {
+ $channels = explode(",", $_GET["channels"]);
+ }
+
+ /* refresh cookie */
+ if (is_null($_GET["channels"])) {
+ header(
+ "Set-Cookie: " .
+ "channels=" . implode(",", $channels) . ";" .
+ "Max-Age=" . 31536000 . "; " . /* 60 x 60 x 24 x 365 = 1 year */
+ "Domain=" . $_SERVER["SERVER_NAME"] . "; " .
+ "SameSite=Strict;"
+ );
+ }
+
+ foreach ($channels as $channel) {
+
+ // Fetch Youtube XML Feed
+ $channel_xml = file(
+ "https://www.youtube.com/feeds/videos.xml?channel_id=" . $channel
+ );
+
+ /* Skip to next entry, if channel could not be found */
+ if ($channel_xml === false) {
+ continue;
+ }
+
+ // Replace un-parsable items
+ $channel_xml = str_replace(
+ array("yt:", "media:"),
+ array("yt_", "media_"),
+ $channel_xml
+ );
+
+ // Cast Array to string
+ $channel_xml = implode(PHP_EOL, $channel_xml);
+
+ // Parse XML
+ $channel_xml = simplexml_load_string($channel_xml);
+ $channel_xml = json_encode($channel_xml);
+ $channel_xml = json_decode($channel_xml, true);
+
+ // Process Entries
+ foreach ($channel_xml["entry"] as $entry) {
+
+ /* Copy video template array */
+ $video = $video_template;
+
+ // Get Video ID
+ $video["vid"] = str_replace(array("yt_video:"), "", $entry["id"]);
+
+ // Get Video Title
+ $video["title"] = str_replace(array("&"), "&", $entry["title"]);
+
+ // Get Video Description
+ $video["desc"] = str_replace(
+ array("\n"),
+ "<br>",
+ preg_replace("/\n+/", "\n", $entry["media_group"]["media_description"])
+ );
+
+ // Get Time
+ $video["time"] = strtotime($entry["published"]);
+
+ // Get Channel ID
+ $video["aid"] = $channel;
+
+ // Get Channel Name
+ $author = str_replace(
+ array("&"),
+ "&",
+ $entry["author"]["name"]
+ );
+ $video["author"] = str_replace(
+ array("&"),
+ "&",
+ $entry["author"]["name"]
+ );
+
+ /* Add video to videos array */
+ array_push($videos, $video);
+
+ }
+
+ }
+
+ /* Sort videos array */
+ usort($videos, 'cmp');
+
+?>
+
+ <header>
+ <nav>
+ <a href="#" onclick="window.location.reload();">Refresh</a>
+ <a href="/">All Videos</a>
+ <a href="https://src.jayvii.de/pub/yt2html">Development</a>
+ </nav>
+ <h1>Videos</h1>
+ </header>
+
+ <form action="https://www.youtube.com/results" method="GET">
+ <input
+ id="searchbar"
+ type="text"
+ id="searchInput"
+ name="search_query"
+ placeholder="Search on YouTube..."
+ >
+ </form>
+
+ <!-- Channels List Form -->
+ <details id="channels">
+ <summary>List of Channels</summary>
+ <p>
+ Please enter the YouTube channel-IDs you want to check here, each
+ separated with a <code>,</code>
+ </p>
+ <form action="/" method="POST">
+ <textarea
+ name="channels"
+ placeholder="channelid1, channelid1, ..."
+ ><?php echo implode("," . PHP_EOL, $channels); ?></textarea>
+ <input type="submit" value="Submit & Reload">
+ </form>
+ </details>
+
+ <!-- Video List -->
+ <?php
+ $index = 0;
+ foreach ($videos as $video) {
+ ?>
+ <section id="video_<?php echo $index; ?>">
+
+ <!-- Headline: streamer name -->
+ <h2><?php echo $video["title"]; ?></h2>
+
+ <!-- player Container -->
+ <div class="player_container hidden"></div>
+
+ <!-- Preview Image -->
+ <img
+ id="preview_<?php echo $index; ?>"
+ class="preview"
+ src="https://i4.ytimg.com/vi/<?php echo $video["vid"]; ?>/hqdefault.jpg"
+ loading="lazy"
+ >
+
+ <!-- Video Buttons -->
+ <?php $js_args = $index . ",'" . $video["vid"] . "'"; ?>
+ <button onclick="yt2html_toggle_player(<?php echo $js_args; ?>)">
+ Toggle Player
+ </button>
+ <a
+ class="button"
+ href="/?channels=<?php echo $video["vid"]; ?>"
+ >
+ <?php echo $video["author"]; ?>
+ </a>
+ <a
+ class="button"
+ href="https://www.youtube.com/watch?v=<?php echo $video["vid"]; ?>"
+ target="_blank"
+ >
+ Open on YouTube
+ </a>
+
+ <!-- Chat Collapsable -->
+ <details class="button">
+ <summary>
+ Description
+ </summary>
+ <?php
+ echo "Date: " . date("Y-m-d, H:i:s", $video["time"]) . "<br><br>" .
+ $video["desc"];
+ ?>
+ </details>
+
+ </section>
+
+ <?php
+
+ $index++;
+
+ }
+
+?>
diff --git a/reload.php b/reload.php
@@ -1,17 +0,0 @@
-<!-- SPDX-License-Identifier: AGPL-3.0-or-later
- SPDX-FileCopyrightText: 2021-2024 JayVii <jayvii[AT]posteo[DOT]de>
--->
-
-<!DOCTYPE html>
-<html>
-<head>
- <!--<meta http-equiv="Refresh" content="0; url='/'" />-->
- <link rel="stylesheet" type="text/css" href="/assets/css/simple.min.css"/>
- <link rel="stylesheet" type="text/css" href="/assets/css/custom.css"/>
-</head>
-<body>
- <h1><a href="#done">Scroll down to check logs and go back</a></h1>
- <pre><?php passthru("Rscript ./yt.R"); ?></pre>
- <a id="done" href="/" class="button" style="width:100%;">Done!</a>
-</body>
-</html>
diff --git a/template.html b/template.html
@@ -1,45 +0,0 @@
-<!-- SPDX-License-Identifier: AGPL-3.0-or-later
- SPDX-FileCopyrightText: 2021-2024 JayVii <jayvii[AT]posteo[DOT]de>
--->
-
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <!-- Title may be filled in by script -->
- <title>%%TITLE%%</title>
- <link rel="icon" type="image/png" href="/assets/favicon.png">
- <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon_16.png">
- <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon_32.png">
- <link rel="icon" type="image/png" sizes="64x64" href="/assets/favicon_64.png">
- <link rel="icon" type="image/png" sizes="128x128" href="/assets/favicon_128.png">
- <link rel="apple-touch-icon" href="/assets/favicon.png">
- <link rel="stylesheet" type="text/css" href="/assets/css/simple.min.css"/>
- <link rel="stylesheet" type="text/css" href="/assets/css/custom.css"/>
- <script async src="assets/js/yt.js"></script>
- <script src="assets/js/thumbnails.js"></script>
- <link crossorigin="use-credentials" rel="manifest" href="/manifest.json">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- </head>
-
- <body onload="setThumbnails();">
-
- <header>
- <h1>Videos</h1>
- <a class="button" href="/">All Videos</a>
- <a class="button" href="javascript:toggleThumbnails();">Thumbnails</a>
- <a class="button" href="reload.php">Refresh</a>
- </header>
-
- <form action="https://www.youtube.com/results" method="GET">
- <input
- id="searchbar"
- type="text"
- id="searchInput"
- name="search_query"
- placeholder="Search on YouTube..."
- >
- </form>
-
- <!-- HERE COMES SCRIPT GENERATED CONTENT -->
diff --git a/url.csv.sample b/url.csv.sample
@@ -1,2 +0,0 @@
-"id","name","url"
-"1","FreeSoftwareFoundationEurope","https://www.youtube.com/feeds/videos.xml?channel_ID=UC68ldbHwL_-5qzETqOaAMWQ"
-\ No newline at end of file
diff --git a/yt.R b/yt.R
@@ -1,233 +0,0 @@
-#!/usr/bin/env Rscript
-# SPDX-License-Identifier: AGPL-3.0-or-later
-# SPDX-FileCopyrightText: 2021-2024 JayVii <jayvii[AT]posteo[DOT]de>
-
-# Stop script after 300 seconds, whether is is done or not
-setTimeLimit(elapsed = 300)
-
-# Load Packages ----------------------------------------------------------------
-if (!require("tidyRSS")) {
- install.packages("tidyRSS")
- library("tidyRSS")
-}
-if (!require("textutils")) {
- install.packages("textutils")
- library("textutils")
-}
-
-# Load URLs --------------------------------------------------------------------
-channels <- as.character(
- read.csv(file = "./url.csv", header = TRUE, sep = ",")$url
-)
-data <- matrix(
- data = NA,
- nrow = 1,
- ncol = 7,
- dimnames = list(
- NULL,
- c("title", "url", "author", "date", "time", "vid", "img")
- )
-)
-
-# functions --------------------------------------------------------------------
-
-fetch.yt <- function(channel) {
-
- # fetch RSS feed
- channel_data <- tidyRSS::tidyfeed(
- channel,
- clean_tags = TRUE,
- list = TRUE,
- parse_dates = FALSE
- )
-
- # strip information that is required for constructing HTML
- video_dat <- data.frame(
- cid = gsub(
- x = channel,
- pattern = "^.*channel_id=",
- replacement = ""
- ),
- title = channel_data$entries$entry_title,
- url = channel_data$entries$entry_link,
- author = channel_data$meta$feed_title,
- date = gsub(
- x = channel_data$entries$entry_published,
- pattern = "T.*$",
- replacement = ""
- ),
- time = gsub(
- x = channel_data$entries$entry_published,
- pattern = "^.*T|\\+.*$",
- replacement = ""
- ),
- vid = gsub(
- x = channel_data$entries$entry_link,
- pattern = "^.*\\?v=",
- replacement = ""
- ),
- img = paste0(
- gsub(
- x = channel_data$entries$entry_link,
- pattern = "^.*\\?v=",
- replacement = "https:\\/\\/i4.ytimg.com\\/vi\\/"
- ),
- "/hqdefault.jpg"
- )
- )
-
- # return video data
- return(video_dat)
-}
-
-# fetch data -------------------------------------------------------------------
-
-video_dat <- list()
-for (i in seq_along(channels)) {
- cat(paste("Fetching:", as.character(channels[i]), "\n"))
- video_dat[[i]] <- tryCatch(fetch.yt(channels[i]), error = function(e) NULL)
-}
-data <- do.call(rbind, video_dat)
-
-# encode text for ASCII compatibility
-for (var in c("author", "title")) {
- data[, var] <- textutils::HTMLencode(data[, var])
-}
-
-# edit data --------------------------------------------------------------------
-
-# sorting according to date and time
-dates <- as.numeric(
- gsub(x = data[, "date"], pattern = "-", replacement = "")
-)
-times <- as.numeric(
- gsub(x = data[, "time"], pattern = ":", replacement = "")
-)
-data <- data[rev(order(dates, times, na.last = FALSE)), ]
-
-# construct per channel HTML ---------------------------------------------------
-
-# unique, vectors of channels that returned some data
-channel_id <- unique(data[, "cid"])
-channel_name <- unique(data[, "author"])
-
-
-# initilise entry-per-channel object
-entry_pc <- list()
-
-# fill in entry-per-channel object
-for (chan in seq_along(channel_id)) {
-
- # choose entries for the current channel
- entries <- which(data[, "cid"] == channel_id[chan])
-
- # fill entry-per-channel-object with contents from current channel
- entry_pc[[chan]] <- paste0(
- "<section id=\"entry_", seq_along(entries), "\">",
- "<h2>",
- "<a href=\"", data[entries, "url"], "\">",
- data[entries, "title"],
- "</a>",
- "</h2>",
- "<div class=\"thumbnails\">",
- "<img src=\"", data[entries, "img"], "\"",
- " id=\"thumbnail_", seq_along(entries), "\" loading=\"lazy\"",
- " onclick=embed_yt(\"",
- seq_along(entries), "\",\"", data[entries, "vid"],
- "\")",
- ">",
- "</div>",
- "<p>",
- "<a class=\"button\" href=\"./", data[entries, "cid"], ".html\">",
- data[entries, "author"],
- "</a>",
- " on ", data[entries, "date"], " ", data[entries, "time"],
- "</p>",
- "</section>"
- )
-
-}
-
-names(entry_pc) <- channel_id
-
-# construct main HTML ---------------------------------------------------------
-
-# only list first "n" entries
-n <- seq_len(500)
-
-entry <- paste0(
- "<section id=\"entry_", n, "\">",
- "<h2>",
- "<a href=\"", data[n, "url"], "\">",
- data[n, "title"],
- "</a>",
- "</h2>",
- "<div class=\"thumbnails\">",
- "<img src=\"", data[n, "img"], "\"",
- " id=\"thumbnail_", n, "\" loading=\"lazy\"",
- " onclick=embed_yt(\"", n, "\",\"", data[n, "vid"], "\")",
- ">",
- "</div>",
- "<p>",
- "<a class=\"button\" href=\"./", data[n, "cid"], ".html\">",
- data[n, "author"],
- "</a>",
- " on ", data[n, "date"], " ", data[n, "time"],
- "</p>",
- "</section>"
-)
-
-# additional HTML -------------------------------------------------------------
-
-# Load template and fill in page title
-template <- paste0(readLines("./template.html", encoding = "UTF-8"), "\n")
-template <- sub(x = template, pattern = "%%TITLE%%", replacement = "Video-Feed")
-
-# insert content for top of the page
-top <- paste0("<p>Last Updated: ", Sys.time(), "</p><hr>")
-top_mainfeed <- paste0(
- "<details>",
- "<summary>Channel List</summary>",
- paste0(
- "<a href=\"", channel_id, ".html\">",
- channel_name,
- " (", sapply(X = entry_pc, FUN = length), ")",
- "</a>",
- collapse = "<br>"
- ),
- "</details><hr>"
-)
-bottom <- paste0("</body></html>")
-
-# print files -------------------------------------------------------------------
-
-html_output <- file("index.html", open = "wt", encoding = "UTF-8")
-sink(html_output)
-cat(
- template, "\n",
- top, "\n",
- top_mainfeed, "\n",
- entry, "\n",
- bottom, "\n"
-)
-sink()
-close(html_output)
-
-for (cid in channel_id) {
- html_output <- file(
- paste0(cid, ".html"), open = "wt", encoding = "UTF-8"
- )
- sink(html_output)
- cat(
- template, "\n",
- top, "\n",
- top_mainfeed, "\n",
- entry_pc[[cid]], "\n",
- bottom, "\n"
- )
- sink()
- close(html_output)
-}
-
-# EOF yt.R
-