YouTube RSS shortcut

Robin had a neat idea to pull all your YouTube subscriptions - or your important ones, at least - into your RSS reader. I like that a lot but my requirements are a little different. I was following him right up until using Google Takeout to get an export of my data. It took hours and I could not figure out what would take so long. Then I remembered I have quite a few videos on there. So anyway my Google Takeout for YouTube is 53GB and I'm not downloading all that just to get one text file with my subscriptions in. Sorry, Google.

I had an idea that fits me better in the long term, even if it's a slight faff in the short term, and that is...you guessed it...Apple Shortcuts!

The problem is, when you copy a channel's URL from the YouTube app, it's almost always their vanity URL, which is no good for me. So I need a way to get their channel ID. There's also another problem, which has a solution, and that is I don't want Shorts in this feed. I just don't like them and I want to skip them entirely. Fortunately, Robin links to a Stack Overflow post where coco0419 has done all that legwork already.

So I need to:

  1. Read the HTML from an input URL
  2. Get the RSS link from that HTML
  3. Get the channel ID from that
  4. Swap out the UC prefix on the default RSS channel ID and swap to UULF, which means it's now a playlist ID
  5. Return that to my shortcut

Simple, right? Yes, but this sort of scraping is liable to just break at any given moment so I trust absolutely nothing here. Every step that might fail returns something useful that will help me to start debugging. This should be fairly robust at least for now, though.

Implemented this as a Craft action. I was going to do it as a Cloudflare worker like my share tracking blocker, but I don't really like developing Cloudflare workers so I just didn't.

  
    <?php

namespace modules\controllers;

use Craft;
use GuzzleHttp\Client;
// I'd normally use DOMDocument for this but it's quite annoying so I had a look through my composer.lock for something else
use Symfony\Component\DomCrawler\Crawler;
use craft\web\Response;

class ToolsController extends BaseController
{
    public function actionYoutubeRss():Response
    {
        // use my GraphQL token to verify requests because I'm too lazy to add another API key
        if (!$this->isVerified()) return $this->error('unverified');

        $url = Craft::$app->request->getQueryParam('url');
        if (!$url) return $this->error('no url');

        $client = new Client();
        $response = $client->get($url);
        $doc = new Crawler($response->getBody());
        // nicer, more modern way to extract this. I hate doing this stuff in DOMDocument
        $list = $doc->filter('link[rel="alternate"][type="application/rss+xml"]');
        // you can't call ->first() if there's no results so we need to protect that
        if ($list->count() === 0) return $this->error('no link rel alternate in source');

        $href = $list->first()->attr('href');
        if (!$href) return $this->error('link rel alternate application/rss+xml doesn\'t seem to have an href');

        // extract the query string and error if we can't find it
        $url = parse_url($href);
        if (!isset($url['query'])) return $this->error('could not extract query string from: ' . $href);

        // extract the channel id and error if we can't find it
        $channelId = $this->extractChannelId($url['query']);
        if (!$channelId) return $this->error('could not extract channel id from: ' . $href);

        // convert the channel id to a playlist id and error if the channel id didn't match what we expected
        $playlistId = $this->convertChannelIdToPlaylist($channelId);
        if (!$playlistId) return $this->error('channel id prefix does not match what we are expecting: ' . $channelId);

        // make sure that the URL we're going to use has an HTTP 200 response code
        $computedUrl = 'https://www.youtube.com/feeds/videos.xml?playlist_id=' . $playlistId;
        $check = $client->head($computedUrl);
        if ($check->getStatusCode() !== 200)
            return $this->error('computed RSS url does not have a 200 status code ('. $check->getStatusCode() . '): ' . $computedUrl);

        return $this->asJson([
            'success' => true,
            'data' => [
                'channel' => $href,
                'url' => $computedUrl,
            ]
        ]);
    }

    private function extractChannelId(string $query):string|null
    {
        $params = [];
        // parse_str used to unpack into the current scope so I always keep it separate from other code
        // you can't even do this any more but the thought of it still scares me
        parse_str($query, $params);
        if (!isset($params['channel_id'])) return null;

        return $params['channel_id'];
    }

    private function convertChannelIdToPlaylist(string $channelId):string|null
    {
        // check that the channel id starts with the prefix we're expecting
        $prefixRegex = '/^UC/';
        $check = preg_match($prefixRegex, $channelId);
        if ($check === 0) return null;

        // and convert it to one we want
        $fixed = preg_replace($prefixRegex, 'UULF', $channelId);

        return $fixed;

    }

    // there's a lot of error checking in this action
    private function error(string $message):Response
    {
        return $this->asJson([
            'success' => false,
            'message' => $message,
        ]);
    }
}
  

Nothing too crazy going on there. I just went to add comments and it's pretty easy to follow so that's nice. This then gets called from the following Shortcut (I wish there was a way to share these other than a screenshot, but also not sharing the entire thing).

Screenshot 2025 05 27 at 14 51 51

Traversing dictionaries (JSON/plist) is an absolute chore in Shortcuts, but at least you can do it!

Now the way I run this is either from the share sheet (but YouTube doesn't give you share sheet on their app, so it falls back to clipboard). I copy a profile URL, run this shortcut, open my RSS reader, and add this as a new feed, and it only pulls full videos in. Now I just need to go through all my subscriptions and actually do it but lazy.

phpcodeios-shortcuts
Email me
Lagoon Work