pub / yt2rss

Transforms a youtube channel into a podcast RSS feed to insert into a podcatcher
git clone https://src.jayvii.de/pub/yt2rss.git
Home | Log | Files | Exports | Refs | Submodules | README | LICENSE | RSS

index.php (9745B)


      1 <?php
      2 /*
      3 * SPDX-FileCopyrightText: 2024 JayVii <jayvii[AT]posteo[DOT]de>
      4 * SPDX-License-Identifier: AGPL-3.0-or-later
      5 */
      6 
      7 if (file_exists("./env.php")) {
      8     include "./env.php";
      9 }
     10 
     11 function analyze_video($video_file) {
     12     include_once("3rdparty/getid3/getid3/getid3.php");
     13     $getID3 = new getID3;
     14     $video_info = $getID3->analyze($video_file);
     15     return $video_info;
     16 }
     17 
     18 // Authentification
     19 if ($_GET["auth"] != $auth_key && !is_null($auth_key)) {
     20     $auth = false;
     21 } else {
     22     $auth = true;
     23 }
     24 
     25 if (!is_null($_GET["channel"]) && $auth) {
     26     
     27     // Fetch Youtube XML Feed
     28     $channel_xml = file(
     29         "https://www.youtube.com/feeds/videos.xml?channel_id=" . basename($_GET["channel"])
     30     );
     31     // Replace un-parsable items
     32     $channel_xml = str_replace(
     33         array("yt:", "media:"),
     34         array("yt_", "media_"),
     35         $channel_xml
     36     );
     37     // Cast Array to string
     38     $channel_xml = implode(PHP_EOL, $channel_xml);
     39     // Parse XML
     40     $channel_xml = simplexml_load_string($channel_xml);
     41     $channel_xml = json_encode($channel_xml);
     42     $channel_xml = json_decode($channel_xml, true);
     43 
     44 
     45     // Construct Podcatcher XML
     46     $rss_xml = "<rss " .
     47         "version=\"2.0\" " .
     48         "xmlns:atom=\"http://www.w3.org/2005/Atom\" " .
     49         "xmlns:content=\"http://purl.org/rss/1.0/modules/content/\" " .
     50         "xmlns:itunes=\"http://www.itunes.com/dtds/podcast-1.0.dtd\" " .
     51         "xmlns:dc=\"http://purl.org/dc/elements/1.1/\" " .
     52         "xmlns:podcast=\"https://podcastindex.org/namespace/1.0\"" .
     53         ">\n<channel>\n";
     54     $rss_xml = $rss_xml .
     55         "<docs>http://www.rssboard.org/rss-specification</docs>\n";
     56     $rss_xml = $rss_xml . "<title>" .
     57         str_replace(
     58             array("&"),
     59             "&amp;",
     60             $channel_xml["title"]
     61         ) . "</title>\n";
     62     $channel_id = str_replace(
     63         array("yt_channel:"),
     64         "",
     65         $channel_xml["id"]
     66     );
     67     $rss_xml = $rss_xml . "<link>https://www.youtube.com/channel/" .
     68         basename($_GET["channel"]) . "</link>\n";
     69     $rss_xml = $rss_xml . "<description>" . 
     70         str_replace(
     71             array("&"),
     72             "&amp;",
     73             $channel_xml["title"]
     74         ) .
     75         "</description>\n";
     76     $rss_xml = $rss_xml . "<pubDate>" . $channel_xml["published"] .
     77         "</pubDate>\n";
     78     // FIXME: fetch channel image rather than first video image
     79     $video_id = str_replace(
     80         array("yt_video:"),
     81         "",
     82         $channel_xml["entry"][0]["id"]
     83     );
     84     $rss_xml = $rss_xml . "<itunes:image href=\"https://i4.ytimg.com/vi/" .
     85         $video_id . "/hqdefault.jpg\"/>\n";
     86     $rss_xml = $rss_xml . "<atom:link href=\"https://" .
     87         $_SERVER["SERVER_NAME"] .
     88         "/?channel=" . basename($_GET["channel"]);
     89     if (!is_null($auth_key)) {
     90         $rss_xml = $rss_xml . "&amp;auth=" . $auth_key;
     91     }
     92     $rss_xml = $rss_xml . "\"" .
     93         " rel=\"self\" type=\"application/rss+xml\"/>\n";
     94 
     95     // Add media items
     96     foreach ($channel_xml["entry"] as $entry) {
     97         // Skip if title matches "exclude"
     98         if (!is_null($_GET["exclude"])) {
     99             $excluded = strstr($entry["title"], rawurldecode($_GET["exclude"]));
    100             if ($excluded) continue;
    101         }
    102         // Skip if title does not match "include"
    103         if (!is_null($_GET["include"])) {
    104             $included = strstr($entry["title"], rawurldecode($_GET["include"]));
    105             if (!$included) continue;
    106         }
    107         $video_id = str_replace(array("yt_video:"), "", $entry["id"]);
    108         // Get Video Length, size and type
    109         if (file_exists($video_id . ".opus")) {
    110             $video_info = analyze_video($video_id . ".opus");
    111             $video_size = $video_info["filesize"];
    112             $video_duration = $video_info["playtime_string"];
    113             $video_ftype = $video_info["mime_type"];
    114         } else {
    115             $video_size = 0;
    116             $video_duration = "00:00:00";
    117             $video_ftype = "audio/ogg";
    118         }
    119         $rss_xml = $rss_xml . "<item>\n";
    120         $rss_xml = $rss_xml . "<title>" .
    121             str_replace(array("&"), "&amp;", $entry["title"]) . "</title>\n";
    122         // FIXME: fetch true description!
    123         $rss_xml = $rss_xml . "<description>" .
    124             "Video-Link:" . PHP_EOL .
    125             $entry["link"]["@attributes"]["href"] . PHP_EOL . PHP_EOL .
    126             str_replace(
    127                 array("&"),
    128                 "&amp;",
    129                 $entry["media_group"]["media_description"]
    130             ) . "</description>\n";
    131         $rss_xml = $rss_xml . "<itunes:author>" . 
    132             str_replace(
    133                 array("&"),
    134                 "&amp;",
    135                 $entry["author"]["name"]
    136             ) .
    137             "</itunes:author>\n";
    138         $rss_xml = $rss_xml . "<pubDate>" . $entry["published"] .
    139             "</pubDate>\n";
    140         $rss_xml = $rss_xml . "<itunes:image href=\"https://i1.ytimg.com/vi/" .
    141             $video_id . "/hqdefault.jpg\"/>\n";
    142         $rss_xml = $rss_xml . "<enclosure url=\"https://" .
    143             $_SERVER["SERVER_NAME"] .
    144             "/?video=" . $video_id;
    145         if (!is_null($auth_key)) {
    146             $rss_xml = $rss_xml . "&amp;auth=" . $auth_key;
    147         }
    148         $rss_xml = $rss_xml . "\"".
    149             " type=\"" . $video_ftype . "\" length=\"" . $video_size . "\"/>\n";
    150         $rss_xml = $rss_xml . "<itunes:duration>" . $video_duration .
    151             "</itunes:duration>\n";
    152         $rss_xml = $rss_xml . "</item>\n";
    153     }
    154 
    155     $rss_xml = $rss_xml . "</channel>\n</rss>\n";
    156     header("Content-type: application/xml");
    157     print_r($rss_xml);
    158     die();
    159 
    160 } else if (!is_null($_GET["video"]) && $auth) {
    161     $download_retry = 0;
    162     while (
    163         !file_exists(basename($_GET["video"]) . ".opus") &&
    164         $download_retry <= 3
    165     ) {
    166         $download_retry++;
    167         passthru(
    168             "yt-dlp " .
    169             "-x " .
    170             "--audio-format opus " .
    171             "-o '%(id)s.%(ext)s' " .
    172             "https://www.youtube.com/watch?v=" . basename($_GET["video"])
    173         );
    174     }
    175     // If file has been downloaded properly, check whether the file is valid
    176     if (!(analyze_video(basename($_GET["video"]) . ".opus")["playtime_seconds"] > 0)) {
    177         // Remove if unvalid
    178         unlink(basename($_GET["video"]) . ".opus");
    179     }
    180     // If file still exists, return to user
    181     if (file_exists(basename($_GET["video"]) . ".opus")) {
    182         header("content-type: audio/ogg; codec=opus");
    183         header(
    184             "content-length: " . filesize(basename($_GET["video"]) . ".opus")
    185         );
    186         header(
    187             "content-disposition: inline; filename=" .
    188             basename($_GET["video"]) . ".opus"
    189         );
    190         readfile(basename($_GET['video']) . ".opus");
    191     } else {
    192         // otherwise return error and exit
    193         http_response_code(404);
    194     }
    195     die();
    196 } else {
    197 ?>
    198 
    199 <html>
    200     <head>
    201         <title>yt2rss</title>
    202         <link
    203             rel="stylesheet"
    204             type="text/css"
    205             href="/assets/css/simple.min.css"
    206         />
    207     </head>
    208 
    209     <body>
    210         <h1>yt2rss</h1>
    211         <p><a href="https://src.jayvii.de/pub/yt2rss">Learn more</a></p>
    212 
    213         <p>Usage:</p>
    214         <ul>
    215             <li>
    216                 <?php echo "https://" .
    217                     $_SERVER["SERVER_NAME"] .
    218                     "/?channel=UCt_Y3j8CpXx366et1RaHACA";
    219                 ?>
    220             </li>
    221             <li>
    222                 <?php echo "https://" .
    223                     $_SERVER["SERVER_NAME"] .
    224                     "/?video=TV8tEq56vHI";
    225                 ?>
    226             </li>
    227         </ul>
    228 
    229         <p>Videos can be included or excluded based on title-matches:</p>
    230         <ul>
    231             <li>
    232                 Include only videos whose title contain "vlog time":<br>
    233                 <?php
    234                     echo "https://" .
    235                         $_SERVER["SERVER_NAME"] .
    236                         "/?channel=UCt_Y3j8CpXx366et1RaHACA&include=vlog%20time";
    237                 ?>
    238             </li>
    239             <li>
    240                 Exclude all videos whose title contain "vlog time":<br>
    241                 <?php
    242                     echo "https://" .
    243                         $_SERVER["SERVER_NAME"] .
    244                         "/?channel=UCt_Y3j8CpXx366et1RaHACA&exclude=vlog%20time";
    245                 ?>
    246             </li>
    247         </ul>
    248 
    249         <?php
    250             // Authentification
    251             if (!is_null($auth_key)) {
    252         ?>
    253             <p>
    254                 This Service requires an authentification key. If you have one,
    255                 add it to the request URLs with:
    256             </p>
    257             <ul>
    258                 <li>
    259                     <?php
    260                         echo "https://" .
    261                             $_SERVER["SERVER_NAME"] .
    262                             "/?auth=MY_KEY&channel=UCt_Y3j8CpXx366et1RaHACA";
    263                     ?>
    264                 </li>
    265                 <li>
    266                     <?php
    267                         echo "https://" .
    268                             $_SERVER["SERVER_NAME"] .
    269                             "/?auth=MY_KEY&video=TV8tEq56vHI";
    270                     ?>
    271                 </li>
    272             </ul>
    273             <p>
    274                 If you do not have an authentification key, please contact the
    275                 server admin.
    276             </p>
    277             <?php
    278                 if ($auth) {
    279             ?>
    280                 <p style="color:white;background-color:green;">
    281                     You are authenticated :)
    282                 </p>
    283             <?php
    284                 } else {
    285             ?>
    286                 <p style='color:white;background-color:red;'>
    287                     You are NOT authenticated :(
    288                 </p>
    289         <?php
    290                 }
    291             }
    292         ?>
    293         </body>
    294     </html>
    295 <?php
    296 }
    297 ?>