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 (10053B)


      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         if (
    168             strlen(
    169                 system("ps ax | grep -v grep | grep " . basename($_GET["video"]))
    170             ) < 1
    171         ) {
    172             passthru(
    173                 "yt-dlp " .
    174                 "-x " .
    175                 "--audio-format opus " .
    176                 "-o '%(id)s.%(ext)s' " .
    177                 "https://www.youtube.com/watch?v=" . basename($_GET["video"])
    178             );
    179         }
    180     }
    181     // If file has been downloaded properly, check whether the file is valid
    182     if (!(analyze_video(basename($_GET["video"]) . ".opus")["playtime_seconds"] > 0)) {
    183         // Remove if unvalid
    184         unlink(basename($_GET["video"]) . ".opus");
    185     }
    186     // If file still exists, return to user
    187     if (
    188         file_exists(basename($_GET["video"]) . ".opus") &&
    189         strlen(
    190             system("ps ax | grep -v grep | grep " . basename($_GET["video"]))
    191         ) < 1
    192     ) {
    193         header("content-type: audio/ogg; codec=opus");
    194         header(
    195             "content-length: " . filesize(basename($_GET["video"]) . ".opus")
    196         );
    197         header(
    198             "content-disposition: inline; filename=" .
    199             basename($_GET["video"]) . ".opus"
    200         );
    201         readfile(basename($_GET['video']) . ".opus");
    202     } else {
    203         // otherwise return error and exit
    204         http_response_code(404);
    205     }
    206     die();
    207 } else {
    208 ?>
    209 
    210 <html>
    211     <head>
    212         <title>yt2rss</title>
    213         <link
    214             rel="stylesheet"
    215             type="text/css"
    216             href="/assets/css/simple.min.css"
    217         />
    218     </head>
    219 
    220     <body>
    221         <h1>yt2rss</h1>
    222         <p><a href="https://src.jayvii.de/pub/yt2rss">Learn more</a></p>
    223 
    224         <p>Usage:</p>
    225         <ul>
    226             <li>
    227                 <?php echo "https://" .
    228                     $_SERVER["SERVER_NAME"] .
    229                     "/?channel=UCt_Y3j8CpXx366et1RaHACA";
    230                 ?>
    231             </li>
    232             <li>
    233                 <?php echo "https://" .
    234                     $_SERVER["SERVER_NAME"] .
    235                     "/?video=TV8tEq56vHI";
    236                 ?>
    237             </li>
    238         </ul>
    239 
    240         <p>Videos can be included or excluded based on title-matches:</p>
    241         <ul>
    242             <li>
    243                 Include only videos whose title contain "vlog time":<br>
    244                 <?php
    245                     echo "https://" .
    246                         $_SERVER["SERVER_NAME"] .
    247                         "/?channel=UCt_Y3j8CpXx366et1RaHACA&include=vlog%20time";
    248                 ?>
    249             </li>
    250             <li>
    251                 Exclude all videos whose title contain "vlog time":<br>
    252                 <?php
    253                     echo "https://" .
    254                         $_SERVER["SERVER_NAME"] .
    255                         "/?channel=UCt_Y3j8CpXx366et1RaHACA&exclude=vlog%20time";
    256                 ?>
    257             </li>
    258         </ul>
    259 
    260         <?php
    261             // Authentification
    262             if (!is_null($auth_key)) {
    263         ?>
    264             <p>
    265                 This Service requires an authentification key. If you have one,
    266                 add it to the request URLs with:
    267             </p>
    268             <ul>
    269                 <li>
    270                     <?php
    271                         echo "https://" .
    272                             $_SERVER["SERVER_NAME"] .
    273                             "/?auth=MY_KEY&channel=UCt_Y3j8CpXx366et1RaHACA";
    274                     ?>
    275                 </li>
    276                 <li>
    277                     <?php
    278                         echo "https://" .
    279                             $_SERVER["SERVER_NAME"] .
    280                             "/?auth=MY_KEY&video=TV8tEq56vHI";
    281                     ?>
    282                 </li>
    283             </ul>
    284             <p>
    285                 If you do not have an authentification key, please contact the
    286                 server admin.
    287             </p>
    288             <?php
    289                 if ($auth) {
    290             ?>
    291                 <p style="color:white;background-color:green;">
    292                     You are authenticated :)
    293                 </p>
    294             <?php
    295                 } else {
    296             ?>
    297                 <p style='color:white;background-color:red;'>
    298                     You are NOT authenticated :(
    299                 </p>
    300         <?php
    301                 }
    302             }
    303         ?>
    304         </body>
    305     </html>
    306 <?php
    307 }
    308 ?>