index.php (13654B)
1 <!-- SPDX-License-Identifier: AGPL-3.0-or-later
2 SPDX-FileCopyrightText: 2021-2024 JayVii <jayvii[AT]posteo[DOT]de>
3 -->
4
5 <!DOCTYPE html>
6 <html lang="en">
7 <head>
8 <meta charset="utf-8">
9 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
10 <title>YouTube Videos</title>
11 <link rel="icon" type="image/png" href="/assets/favicon.png">
12 <link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon_16.png">
13 <link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon_32.png">
14 <link rel="icon" type="image/png" sizes="64x64" href="/assets/favicon_64.png">
15 <link rel="icon" type="image/png" sizes="128x128" href="/assets/favicon_128.png">
16 <link rel="apple-touch-icon" href="/assets/favicon.png">
17 <link rel="stylesheet" type="text/css" href="/assets/css/simple.min.css"/>
18 <link rel="stylesheet" type="text/css" href="/assets/css/custom.css"/>
19 <script async src="assets/js/yt.js"></script>
20 <link crossorigin="use-credentials" rel="manifest" href="/manifest.json">
21 <meta name="viewport" content="width=device-width, initial-scale=1.0">
22 </head>
23
24 <?php
25
26 /* Comparing function for sorting */
27 function compare_numeric(array $a, array $b, string $col) {
28 return $b[$col] - $a[$col];
29 }
30 function compare_string(array $a, array $b, string $col) {
31 return strcmp(strtolower($a[$col]), strtolower($b[$col]));
32 }
33
34 /* Get Channel ID */
35 function get_yt_channelid($channel_name) {
36 // set fallback for channel id
37 $channelid = $channel_name;
38 // fetch HTML for given channel ID
39 $html = explode(
40 PHP_EOL,
41 file_get_contents("https://www.youtube.com/" . $channel_name)
42 );
43 // If HTML could be fetched, extract lines with probable profile picture
44 if (!empty($html)) {
45 $channel_id_lines = preg_grep(
46 '/videos.xml\?channel_id\=/',
47 $html
48 );
49 $channelid = preg_replace(
50 '/^.*\"https:\/\/www\.youtube\.com\/feeds\/videos\.xml\?channel_id\=([^\"]+).*$/',
51 '${1}',
52 $channel_id_lines
53 );
54 }
55 // return the channel id
56 return array_values($channelid)[0];
57 }
58
59 /* Initilise videos template */
60 $video_template = array(
61 "vid" => array(),
62 "title" => array(),
63 "desc" => array(),
64 "time" => array(),
65 "views" => array(),
66 "aid" => array(),
67 "author" => array()
68 );
69
70 /* Initilise channels arrays */
71 $channels = array();
72 $channels_list = array();
73
74 /* Initilise videos array */
75 $videos = array();
76
77 // Fetch POST, GET arguments or the COOKIE
78 // Cookie should have lowest priority (fallback), POST first (intend)
79 // GET arguments are used for one-time usage (will not set cookie)
80 if (!is_null($_COOKIE["channels"])) {
81 $channels = explode(",", $_COOKIE["channels"]);
82 }
83 if (!is_null($_POST["channels"])) {
84 $channels = urldecode($_POST["channels"]);
85 $channels = preg_replace('/[^A-Za-z0-9\-\_\,\@]+/', '', $channels);
86 $channels = explode(",", $channels);
87 }
88 if (!is_null($_GET["channels"])) {
89 $channels = explode(",", $_GET["channels"]);
90 }
91
92 // replace usernames with channel-ids
93 $channel_names = preg_grep('/^@/', $channels);
94 foreach ($channel_names as $channel_name) {
95 array_push($channels, get_yt_channelid($channel_name));
96 }
97 $channels = array_values(preg_grep('/^[^@]/', $channels));
98
99 // Sort channels by alphabet and ensure each channel is unique
100 $channels = array_unique($channels);
101
102 /* refresh cookie */
103 if (is_null($_GET["channels"])) {
104 header(
105 "Set-Cookie: " .
106 "channels=" . implode(",", $channels) . ";" .
107 "Max-Age=" . 31536000 . "; " . /* 60 x 60 x 24 x 365 = 1 year */
108 "Domain=" . $_SERVER["SERVER_NAME"] . "; " .
109 "SameSite=Strict;"
110 );
111 }
112
113 for ($i = 0; $i < count($channels); $i++) {
114 $channel = $channels[$i];
115
116 // Fetch Youtube XML Feed
117 $channel_xml = file(
118 "https://www.youtube.com/feeds/videos.xml?channel_id=" . $channel
119 );
120
121 /* Skip to next entry, if channel could not be found */
122 if ($channel_xml === false) {
123 /* Remove entry from channels list */
124 array_splice($channels, $i, 1);
125 /* Skip item within loop */
126 continue;
127 }
128
129 // Replace un-parsable items
130 $channel_xml = str_replace(
131 array("yt:", "media:"),
132 array("yt_", "media_"),
133 $channel_xml
134 );
135
136 // Cast Array to string
137 $channel_xml = implode(PHP_EOL, $channel_xml);
138
139 // Parse XML
140 $channel_xml = simplexml_load_string($channel_xml);
141 $channel_xml = json_encode($channel_xml);
142 $channel_xml = json_decode($channel_xml, true);
143
144 // Get Channel name
145 $author = str_replace(
146 array("&"),
147 "&",
148 $channel_xml["entry"][0]["author"]["name"]
149 );
150
151 /* Fill channels list array */
152 array_push($channels_list, array("aid" => $channel, "author" => $author));
153
154 // Process Entries
155 foreach ($channel_xml["entry"] as $entry) {
156
157 // Robustness: Skip entry if it is not an array (broken return)
158 if (!is_array($entry)) {
159 continue;
160 }
161
162 // Skip entry if it is a short
163 $uri = $entry["link"]["@attributes"]["href"];
164 if (preg_match('/\/shorts\//', $uri) > 0) {
165 continue;
166 }
167
168 /* Copy video template array */
169 $video = $video_template;
170
171 // Get Video ID
172 $video["vid"] = str_replace(array("yt_video:"), "", $entry["id"]);
173
174 // Get Video Title
175 $video["title"] = str_replace(array("&"), "&", $entry["title"]);
176
177 // Get Video Description
178 $video["desc"] = preg_replace(
179 "/\n+/",
180 "<br>",
181 $entry["media_group"]["media_description"]
182 );
183 if (empty($video["desc"])) {
184 $video["desc"] = "";
185 }
186
187 // Get Time
188 $video["time"] = strtotime($entry["published"]);
189
190 // Get Views
191 $video["views"] = number_format(
192 $entry["media_group"]["media_community"]["media_statistics"]["@attributes"]["views"]
193 );
194
195 // Get Channel ID
196 $video["aid"] = $channel;
197
198 // Get Channel Name
199 $video["author"] = $author;
200
201 /* Add video to videos array */
202 array_push($videos, $video);
203
204 }
205
206 }
207
208 /* Sort videos and channels arrays */
209 function cmp_time(array $a, array $b) {
210 return compare_numeric($a, $b, "time");
211 }
212 function cmp_name(array $a, array $b) {
213 return compare_string($a, $b, "author");
214 }
215 usort($videos, "cmp_time");
216 usort($channels_list, "cmp_name");
217
218 ?>
219
220 <body>
221
222 <header>
223 <nav>
224 <a href="/">All Videos</a>
225 <?php
226 /* For GET-requests, draw simple reload button,
227 * otherwise re-submit channels form
228 */
229 if (is_null($_GET["channels"])) {
230 $jsfun = "document.getElementById('channel_form').submit();";
231 } else {
232 $jsfun = "window.location.reload();";
233 }
234 ?>
235 <a href="#" onclick="<?php echo $jsfun; ?>">
236 Refresh
237 </a>
238 <a href="#about">
239 About
240 </a>
241 </nav>
242 <h1>Videos</h1>
243 </header>
244
245 <form action="https://www.youtube.com/results" method="GET">
246 <input
247 id="searchbar"
248 type="text"
249 id="searchInput"
250 name="search_query"
251 placeholder="Search on YouTube..."
252 >
253 </form>
254
255 <!-- Channels List Form -->
256 <details id="channels">
257 <summary>List of Channels</summary>
258 <form action="/" method="POST" id="channel_form">
259 <details>
260 <summary>Import / Export</summary>
261 <p>
262 Please enter the YouTube channel-IDs you want to check here, each
263 separated with a <code>,</code>. You may also apply Usernames,
264 starting with a <code>@</code>:
265 </p>
266 <input
267 name="channels"
268 type="text"
269 value="<?php echo implode("," . PHP_EOL, $channels); ?>"
270 >
271 <input type="submit" value="Save & Reload">
272 </details>
273 <div id="channels_list">
274 <?php
275 /* Draw Input fields for each channel ID */
276 for ($i = 0; $i < count($channels_list); $i++) {
277 ?>
278 <label for="channel_<?php echo $i; ?>">
279 <?php
280 if ($channels_list[$i]["author"] != "") {
281 echo "<a href=\"/?channels=" . $channels_list[$i]["aid"] .
282 "\" target=\"_blank\">" . $channels_list[$i]["author"] .
283 "</a>";
284 } else {
285 echo "<mark class=\"error\">Error</mark>";
286 }
287 ?>
288 </label>
289 <input
290 name="channel_<?php echo $i; ?>"
291 class="channels_input"
292 type="text"
293 value="<?php echo $channels_list[$i]["aid"]; ?>"
294 oninput="yt2html_update_channels_list();"
295 >
296 <?php
297 }
298 ?>
299 <!-- Draw one empty input field for new entry by the user -->
300 <label for="channel_<?php echo ($i); ?>">
301 <mark>Add a new channel here</mark>
302 </label>
303 <input
304 name="channel<?php echo ($i); ?>"
305 class="channels_input"
306 type="text"
307 placeholder="@username"
308 oninput="yt2html_update_channels_list();"
309 >
310 </div>
311 <!-- Submit Button -->
312 <input type="submit" value="Save & Reload">
313 </form>
314 </details>
315
316 <!-- Video List -->
317 <?php
318 foreach ($videos as $video) {
319 ?>
320 <section id="video_<?php echo $video["vid"]; ?>">
321
322 <!-- Headline: streamer name -->
323 <h3 style="margin-bottom:5px;">
324 <a
325 target="_blank"
326 href="https://www.youtube.com/watch?v=<?php echo $video["vid"]; ?>"
327 >
328 <?php echo $video["title"]; ?>
329 </a>
330 </h3>
331
332 <!-- Video Information -->
333 <div style="width:100%;margin-bottom:1em;color:var(--border);">
334 <!-- Channel Name -->
335 <span style="margin-right:0.25em;">
336 <a href="/?channels=<?php echo $video["aid"]; ?>">
337 <?php echo $video["author"]; ?>
338 </a>
339 </span>
340 <!-- Timestamp -->
341 <span style="margin-left:0.25em;margin-right:0.25em;">
342 <?php echo date("Y-m-d, H:i:s", $video["time"]); ?>
343 </span>
344 <!-- Views -->
345 <span style="margin-left:0.25em;margin-right:0.25em;">
346 <?php echo "(" . $video["views"] . " Views)"; ?>
347 </span>
348 </div>
349
350 <!-- player Container -->
351 <div class="player_container hidden">
352 <?php $js_args = "'" . $video["vid"] . "'"; ?>
353 <button onclick="yt2html_toggle_player(<?php echo $js_args; ?>)">
354 Toggle Player
355 </button>
356 </div>
357
358 <!-- Preview Image -->
359 <div
360 id="preview_<?php echo $video["vid"]; ?>"
361 class="preview"
362 >
363 <img
364 src="https://i4.ytimg.com/vi/<?php echo $video["vid"]; ?>/hqdefault.jpg"
365 loading="lazy"
366 height="100%"
367 width="100%"
368 >
369 <div
370 class="play_button"
371 onclick="yt2html_toggle_player(<?php echo $js_args; ?>)"
372 >
373 </div>
374 </div>
375
376 <!-- Description Collapsable -->
377 <details>
378 <summary>
379 Description
380 </summary>
381 <?php echo $video["desc"]; ?>
382 </details>
383
384 </section>
385
386 <?php
387
388 }
389
390 ?>
391
392 <footer>
393 <h3 id="about">About</h3>
394 <details>
395 <summary>What is this?</summary>
396 <p>
397 This web-application is a way to <em>follow</em> YouTube-Channels,
398 without using either the YouTube.com website directly or requiring a
399 Youtube/Google account.
400 </p>
401 <p>
402 Instead, this utility uses YouTube's
403 <a href="https://en.wikipedia.org/wiki/RSS" target="_blank">
404 RSS-Feeds
405 </a> in order to fetch the latest videos uploaded to specified Channels.
406 Simply insert the Channel-ID or <em>Username</em> (starting with an
407 <code>@</code>) in the form at the top of the page and click "save".
408 </p>
409 <p>
410 For each channel, the newest 15 videos will be gathered, sorted
411 chronoligically and served you as a simple list. You can click the
412 thumbnail of a video-preview to start playback. This will embed the
413 YouTube-Video directly, i.e. your browser will connect to youtube.com
414 </p>
415 </details>
416 <details>
417 <summary>How can I contribute?</summary>
418 <p>
419 All development happens at
420 <a href="https://src.jayvii.de/pub/yt2html/" target="_blank">
421 src.jayvii.de/pub/yt2html
422 </a>. If you want to contribute or would like to self-host yt2html,
423 please head over there.
424 </p>
425 </details>
426 <details>
427 <summary>Privacy Statement</summary>
428 <p>
429 Besides Access Information, no information of site visitors are
430 collected.
431 </p>
432 <p>
433 When submitting a list of youtube-channels in the form, a cookie storing
434 that list is set, so the service knows which channels to poll.
435 </p>
436 <p>
437 Starting to watch a youtube video will connect your browser directly to
438 <code>www.youtube-nocookie.com</code>, thereby transfering your IP
439 address as well as potentially other data, leaked by your webbrowser.
440 You can find <a href="https://www.youtube.com/privacy" target="_blank">
441 YouTube’s privacy policy here
442 </a>.
443 </p>
444 <p>
445 No other data is shared with anyone.
446 </p>
447 </footer>
448
449 </body>
450 </html>