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

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       "&amp;",
    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("&"), "&amp;", $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 &amp; 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 &amp; 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>