pub / yt2html

Fetches Youtube content via RSS and provides a chronological timeline
git clone https://src.jayvii.de/pub/yt2html.git
Home | Log | Files | Exports | Refs | README | RSS

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+++++------------
Massets/css/custom.css | 49++++++++++++++++++++++++++++++++-----------------
Massets/js/yt.js | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Aindex.php | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dreload.php | 17-----------------
Dtemplate.html | 45---------------------------------------------
Durl.csv.sample | 3---
Dyt.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("&"), "&amp;", $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("&"), + "&amp;", + $entry["author"]["name"] + ); + $video["author"] = str_replace( + array("&"), + "&amp;", + $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 &amp; 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 -