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)