index.php (13656B)
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 // Skip entry if it is a short
158 $uri = $entry["link"]["@attributes"]["href"];
159 if (preg_match('/\/shorts\//', $uri) > 0) {
160 continue;
161 }
162
163 /* Copy video template array */
164 $video = $video_template;
165
166 // Get Video ID
167 $video["vid"] = str_replace(array("yt_video:"), "", $entry["id"]);
168
169 // Get Video Title
170 $video["title"] = str_replace(array("&"), "&", $entry["title"]);
171
172 // Get Video Description
173 $video["desc"] = preg_replace(
174 "/\n+/",
175 "<br>",
176 $entry["media_group"]["media_description"]
177 );
178 if (empty($video["desc"])) {
179 $video["desc"] = "";
180 }
181
182 // Get Time
183 $video["time"] = strtotime($entry["published"]);
184
185 // Get Views
186 $video["views"] = number_format(
187 $entry["media_group"]["media_community"]["media_statistics"]["@attributes"]["views"]
188 );
189
190 // Get Channel ID
191 $video["aid"] = $channel;
192
193 // Get Channel Name
194 $video["author"] = $author;
195
196 /* Add video to videos array */
197 array_push($videos, $video);
198
199 }
200
201 }
202
203 /* Sort videos and channels arrays */
204 function cmp_time(array $a, array $b) {
205 return compare_numeric($a, $b, "time");
206 }
207 function cmp_name(array $a, array $b) {
208 return compare_string($a, $b, "author");
209 }
210 usort($videos, "cmp_time");
211 usort($channels_list, "cmp_name");
212
213 ?>
214
215 <body>
216
217 <header>
218 <nav>
219 <a href="/">All Videos</a>
220 <?php
221 /* For GET-requests, draw simple reload button,
222 * otherwise re-submit channels form
223 */
224 if (is_null($_GET["channels"])) {
225 $jsfun = "document.getElementById('channel_form').submit();";
226 } else {
227 $jsfun = "window.location.reload();";
228 }
229 ?>
230 <a href="#" onclick="<?php echo $jsfun; ?>">
231 Refresh
232 </a>
233 <a href="#about">
234 About
235 </a>
236 </nav>
237 <h1>Videos</h1>
238 </header>
239
240 <form action="https://www.youtube.com/results" method="GET">
241 <input
242 id="searchbar"
243 type="text"
244 id="searchInput"
245 name="search_query"
246 placeholder="Search on YouTube..."
247 >
248 </form>
249
250 <!-- Channels List Form -->
251 <details id="channels">
252 <summary>List of Channels</summary>
253 <form action="/" method="POST" id="channel_form">
254 <details>
255 <summary>Import / Export</summary>
256 <p>
257 Please enter the YouTube channel-IDs you want to check here, each
258 separated with a <code>,</code>. You may also apply Usernames,
259 starting with a <code>@</code>:
260 </p>
261 <input
262 name="channels"
263 type="text"
264 value="<?php echo implode("," . PHP_EOL, $channels); ?>"
265 >
266 <input type="submit" value="Save & Reload">
267 </details>
268 <div id="channels_list">
269 <?php
270 /* Draw Input fields for each channel ID */
271 for ($i = 0; $i < count($channels_list); $i++) {
272 ?>
273 <label for="channel_<?php echo $i; ?>">
274 <?php
275 if ($channels_list[$i]["author"] != "") {
276 echo "<a href=\"/?channels=" . $channels_list[$i]["aid"] .
277 "\" target=\"_blank\">" . $channels_list[$i]["author"] .
278 "</a>";
279 } else {
280 echo "<mark class=\"error\">Error</mark>";
281 }
282 ?>
283 </label>
284 <input
285 name="channel_<?php echo $i; ?>"
286 class="channels_input"
287 type="text"
288 value="<?php echo $channels_list[$i]["aid"]; ?>"
289 oninput="yt2html_update_channels_list();"
290 >
291 <?php
292 }
293 ?>
294 <!-- Draw one empty input field for new entry by the user -->
295 <label for="channel_<?php echo ($i); ?>">
296 <mark>Add a new channel here</mark>
297 </label>
298 <input
299 name="channel<?php echo ($i); ?>"
300 class="channels_input"
301 type="text"
302 placeholder="@username"
303 oninput="yt2html_update_channels_list();"
304 >
305 </div>
306 <!-- Submit Button -->
307 <input type="submit" value="Save & Reload">
308 </form>
309 </details>
310
311 <!-- Video List -->
312 <?php
313 $index = 0;
314 foreach ($videos as $video) {
315
316 /* Stop if 500 videos are listed */
317 if ($index >= 500) {
318 break;
319 }
320 ?>
321 <section id="video_<?php echo $index; ?>">
322
323 <!-- Headline: streamer name -->
324 <h3 style="margin-bottom:5px;">
325 <a
326 target="_blank"
327 href="https://www.youtube.com/watch?v=<?php echo $video["vid"]; ?>"
328 >
329 <?php echo $video["title"]; ?>
330 </a>
331 </h3>
332
333 <!-- Video Information -->
334 <div style="width:100%;margin-bottom:1em;color:var(--border);">
335 <!-- Channel Name -->
336 <span style="margin-right:0.25em;">
337 <a href="/?channels=<?php echo $video["aid"]; ?>">
338 <?php echo $video["author"]; ?>
339 </a>
340 </span>
341 <!-- Timestamp -->
342 <span style="margin-left:0.25em;margin-right:0.25em;">
343 <?php echo date("Y-m-d, H:i:s", $video["time"]); ?>
344 </span>
345 <!-- Views -->
346 <span style="margin-left:0.25em;margin-right:0.25em;">
347 <?php echo "(" . $video["views"] . " Views)"; ?>
348 </span>
349 </div>
350
351 <!-- player Container -->
352 <div class="player_container hidden">
353 <?php $js_args = $index . ",'" . $video["vid"] . "'"; ?>
354 <button onclick="yt2html_toggle_player(<?php echo $js_args; ?>)">
355 Toggle Player
356 </button>
357 </div>
358
359 <!-- Preview Image -->
360 <div
361 id="preview_<?php echo $index; ?>"
362 class="preview"
363 >
364 <img
365 src="https://i4.ytimg.com/vi/<?php echo $video["vid"]; ?>/hqdefault.jpg"
366 loading="lazy"
367 height="100%"
368 width="100%"
369 >
370 <div
371 class="play_button"
372 onclick="yt2html_toggle_player(<?php echo $js_args; ?>)"
373 >
374 </div>
375 </div>
376
377 <!-- Description Collapsable -->
378 <details>
379 <summary>
380 Description
381 </summary>
382 <?php echo $video["desc"]; ?>
383 </details>
384
385 </section>
386
387 <?php
388
389 $index++;
390
391 }
392
393 ?>
394
395 <footer>
396 <h3 id="about">About</h3>
397 <details>
398 <summary>What is this?</summary>
399 <p>
400 This web-application is a way to <em>follow</em> YouTube-Channels,
401 without using either the YouTube.com website directly or requiring a
402 Youtube/Google account.
403 </p>
404 <p>
405 Instead, this utility uses YouTube's
406 <a href="https://en.wikipedia.org/wiki/RSS" target="_blank">
407 RSS-Feeds
408 </a> in order to fetch the latest videos uploaded to specified Channels.
409 Simply insert the Channel-ID or <em>Username</em> (starting with an
410 <code>@</code>) in the form at the top of the page and click "save".
411 </p>
412 <p>
413 For each channel, the newest 15 videos will be gathered, sorted
414 chronoligically and served you as a simple list. You can click the
415 thumbnail of a video-preview to start playback. This will embed the
416 YouTube-Video directly, i.e. your browser will connect to youtube.com
417 </p>
418 </details>
419 <details>
420 <summary>How can I contribute?</summary>
421 <p>
422 All development happens at
423 <a href="https://src.jayvii.de/pub/yt2html/" target="_blank">
424 src.jayvii.de/pub/yt2html
425 </a>. If you want to contribute or would like to self-host yt2html,
426 please head over there.
427 </p>
428 </details>
429 <details>
430 <summary>Privacy Statement</summary>
431 <p>
432 Besides Access Information, no information of site visitors are
433 collected.
434 </p>
435 <p>
436 When submitting a list of youtube-channels in the form, a cookie storing
437 that list is set, so the service knows which channels to poll.
438 </p>
439 <p>
440 Starting to watch a youtube video will connect your browser directly to
441 <code>www.youtube-nocookie.com</code>, thereby transfering your IP
442 address as well as potentially other data, leaked by your webbrowser.
443 You can find <a href="https://www.youtube.com/privacy" target="_blank">
444 YouTube’s privacy policy here
445 </a>.
446 </p>
447 <p>
448 No other data is shared with anyone.
449 </p>
450 </footer>
451
452 </body>
453 </html>