Skip to content

Instantly share code, notes, and snippets.

@yig
Last active December 20, 2023 03:12
Show Gist options
  • Save yig/4784a86c1343d2bed789dd243ce980fc to your computer and use it in GitHub Desktop.
Save yig/4784a86c1343d2bed789dd243ce980fc to your computer and use it in GitHub Desktop.
Create a valid podcast RSS feed from a base URL and a list of files.
#!/usr/bin/env python3
'''
Author: Yotam Gingold <yotam (strudel) yotamgingold.com>
License: Public Domain [CC0](http://creativecommons.org/publicdomain/zero/1.0/)
On GitHub as a gist: https://gist.github.com/yig/4784a86c1343d2bed789dd243ce980fc
'''
'''
Example:
$ python3 feedgen.py -o feed.xml "https://www.website.com/secretpodcast/" file1.m4a file2.mp3 ...
$ scp feed.xml file1.m4a file2.mp3 ... website.com:www/secretpodcast/
In your podcast app, add the feed: https://www.website.com/secretpodcast/feed.xml
'''
from pathlib import Path
import datetime
import hashlib
## Podcast feed requirements:
## https://podcasters.apple.com/support/823-podcast-requirements
## https://help.apple.com/itc/podcasts_connect/#/itcb54353390
## Example: https://help.apple.com/itc/podcasts_connect/#/itcbaf351599
HEADER = '''<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
<title>{title}</title>
<language>en-us</language>
<description>
{description}
</description>
<itunes:type>serial</itunes:type>
<itunes:image
href="https://applehosted.podcasts.apple.com/hiking_treks/artwork.png"
/>
<itunes:category text="Sports">
<itunes:category text="Wilderness"/>
</itunes:category>
<itunes:explicit>false</itunes:explicit>
'''
ITEM = '''
<item>
<itunes:episodeType>full</itunes:episodeType>
<itunes:season>{season}</itunes:season>
<itunes:episode>{episode}</itunes:episode>
<title>{title}</title>
<description>
{title}
</description>
<enclosure
length="{bytes}"
type="audio/x-m4a"
url="{URL}"
/>
<guid>{GUID}</guid>
<pubDate>{date}</pubDate>
<itunes:explicit>false</itunes:explicit>
</item>
'''
FOOTER = '''
</channel>
</rss>
'''
def ItemFromPath( URL, season, episode, path ):
## Convert to a pathlib Path for convenience.
path = Path(path)
stat = path.stat()
title = path.stem
bytes = stat.st_size
mtime = stat.st_mtime
## RSS pubDate is defined by RFC822
## https://stackoverflow.com/questions/12270531/how-to-format-pubdate-with-python
date = datetime.datetime.fromtimestamp(mtime).strftime("%a, %d %b %Y %H:%M:%S %z")
GUID = hashlib.sha256( path.name.encode('utf-8') ).hexdigest()
URL = URL.rstrip('/') + '/' + path.name
return ITEM.format( **locals() )
def FeedFromPaths( url, paths, season = 1, title = 'My Podcast', description = 'My Podcast Description' ):
items = [ ItemFromPath( url, season, i+1, path, ) for i, path in enumerate( sorted( paths ) ) ]
return HEADER.format( title = title, description = description ) + ''.join( items ) + FOOTER
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser( description = 'Generate a podcast feed from some files.' )
parser.add_argument( 'URL', help = 'The base URL of the podcast.' )
parser.add_argument( 'files', nargs='*', help = 'A list of audio files.' )
parser.add_argument( '--season', type = int, default = 1, help = 'The season of the podcast.' )
parser.add_argument( '--title', default = 'My Podcast', help = 'The title of the podcast.' )
parser.add_argument( '--description', default = 'My Podcast Description', help = 'A description of the podcast.' )
parser.add_argument( '-o', '--output', default = None, help = 'A file to save the podcast into. By default, prints to stdout.' )
args = parser.parse_args()
feed = FeedFromPaths( args.URL, args.files, season = args.season, title = args.title, description = args.description )
print( feed, file = open( args.output, 'w' ) if args.output is not None else None )
print( f"In your podcast app, add the feed: {args.URL.rstrip('/')}/feed.xml" )
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment