Skip to content

Instantly share code, notes, and snippets.

@remiberthoz
Created May 24, 2025 15:09
Show Gist options
  • Select an option

  • Save remiberthoz/fce707f9cd0256f0176340e9f57a8ee5 to your computer and use it in GitHub Desktop.

Select an option

Save remiberthoz/fce707f9cd0256f0176340e9f57a8ee5 to your computer and use it in GitHub Desktop.
Convert a list of YouTube channels URLs to OMPL file for RSS feed readers

The script reads a file named yt2feed.txt and outputs a file named yt2feed.ompl. The input file is a newline-separated list of YouTube channel URLs in @Handle fromat, ie :

$ cat yt2feed.txt
https://www.youtube.com/@3blue1brown
https://www.youtube.com/@AlphaPhoenixChannel
https://www.youtube.com/@animalogic

This works as of 2025-05-24. YouTube may update it's behaviour and break the script.

import subprocess
import re

def title2name(title):
    groups = re.findall("(.+) - YouTube", title)
    if len(groups) == 1:
        return groups[0]

def channelHandleUrl2videoId(channelUrl):
    sp1 = subprocess.run(["curl", "-s", channelUrl], stdout=subprocess.PIPE, text=True, encoding="utf-8")
    videoIds = re.findall('"videoId":"(.+?)"', str(sp1.stdout))
    return list(set(videoIds))

def channelHandleUrl2channelName(channelUrl):
    sp1 = subprocess.run(["curl", "-s", channelUrl], stdout=subprocess.PIPE, text=True, encoding="utf-8")
    channelName = re.findall("<title>(.+?)</title>", str(sp1.stdout))
    if len(channelName) == 1:
        return title2name(channelName[0])

def videoId2channelId(videoId):
    videoUrl = f"https://www.youtube.com/watch?v={videoId}"
    sp1 = subprocess.run(["curl", "-s", videoUrl], stdout=subprocess.PIPE, text=True, encoding="utf-8")
    channelId = re.findall('"channelId":"(.+?)"', str(sp1.stdout))
    if len(channelId) == 1:
        return channelId[0]

def channelId2channelName(channelId):
    channelIdUrl = f"https://www.youtube.com/channel/{channelId}"
    sp1 = subprocess.run(["curl", "-s", channelIdUrl], stdout=subprocess.PIPE, text=True, encoding="utf-8")
    channelName = re.findall("<title>(.+?)</title>", str(sp1.stdout))
    if len(channelName) == 1:
        return title2name(channelName[0])

with open("yt2feed.txt") as foo:
    channelHandleUrls = [l.strip() for l in foo.readlines()]

channels_map = {}

for channelHandleUrl in channelHandleUrls:
    print(f"Processing {channelHandleUrl}")
    channelName = channelHandleUrl2channelName(channelHandleUrl)
    print(f"  channel is named {channelName}")
    videoIds = channelHandleUrl2videoId(channelHandleUrl)
    print(f"  found {len(videoIds)} videos")
    for videoId in videoIds:
        print(f"    testing video {videoId}", end=", ")
        thisChannelId = videoId2channelId(videoId)
        print(f"channel id is {thisChannelId}", end=", ")
        thisChannelName = channelId2channelName(thisChannelId)
        print(f"channel name is {thisChannelName}")
        if thisChannelName == channelName:
            channels_map[channelName] = thisChannelId
            print(f"    found a matching id: {thisChannelId}")
            break

opml_template_head = """\
<?xml version="1.0" encoding="utf-8"?>
<opml version="1.0">
	<head>
		<title>YouTube Feeds</title>
	</head>
	<body>\
"""

opml_template_foot = """
    </body>
</opml>\
"""

opml_template_outline = """
        <outline text="{name}" htmlUrl="https://www.youtube.com/@{name}" type="rss" xmlUrl="https://www.youtube.com/feeds/videos.xml?channel_id={id}"/>"""
opml_outline = "".join(
    opml_template_outline.format(name=channelName, id=channelId) for channelName, channelId in channels_map.items())

rendered = opml_template_head + opml_outline + opml_template_foot

with open("yt2feed.ompl", "w") as foo:
    foo.write(rendered)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment