How to run your own podcast

Posted
Comments 0


12:37:21 < r00t^2> “Sysadministrivia: reading RSS feed specs, iTunes specs, and GooglePlay specs so your lazy motherfucking ass doesn’t have to.”
12:37:35 < jthan> hahaha
12:37:43 < jthan> you should just make that the subheading on the post

(If you got here from my Opensource.com article, you may be interested in the unedited version of the article as well.)

So we’ve done a lot of work on our podcast. Our previous site was an absolute mess, to be quite frank.

However, the new site is something we like and while sure, it isn’t perfect and a little bland to look at, it’s certainly much more functional.

Hopefully this should get you started. Note that I gloss over a lot of “common” knowledge (such as configuring DNS records and the like). If you don’t know how to do/understand those things, you most likely should be paying someone who does.

It also doesn’t cover how to add your podcast to iTunes syndication, nor GooglePlay syndication, etc. But there’s lots of articles out there to do that.

If you have any questions or need any clarification, feel free to contact us or add a comment.

The Basics

Hosting

You’ll obviously need hosting. We recommend a VPS as long as you know what you’re doing, because then you have root access. However, MOST of this should work fine with shared hosting. We use Linode (but aren’t sponsored by them).

We run off an Arch VPS instance, and run Nginx.

Nginx

The following is a complete Nginx config for how we run things with Textpattern:


server {
  listen [::]:80 ipv6only=off default_server;
  listen [::]:443 ssl ipv6only=off default_server;
  server_name sysadministrivia.com;

  ## SSL ##
  ssl_certificate /etc/nginx/ssl/certs/sysadministrivia-bundle.crt;
  ssl_certificate_key /etc/nginx/ssl/keys/sysadministrivia.key;
  ssl_session_timeout  5m;
  ssl_protocols TLSv1.1 TLSv1.2;
  ssl_ciphers  'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
  ssl_dhparam /etc/nginx/ssl/keys/dh4096.pem;
  ssl_prefer_server_ciphers   on;
  ssl_session_cache shared:SSL:10m;
  # HSTS #
  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/nginx/ssl/certs/sysadministrivia-bundle.crt;

  # DNS resolvers #
  resolver 4.2.2.1 4.2.2.2 valid=300s;
  resolver_timeout 10s;

  ## LOGGING ##
  access_log /var/log/nginx/vhost/sysadmin/access.log main;
  error_log /var/log/nginx/vhost/sysadmin/error.log;
  #error_log /var/log/nginx/vhost/sysadmin/error.log debug;

  ## DATA/CONTENT/CONNECTIONS ##
  root /srv/http/sysadmin;
  index index.php index.html index.htm;
  sendfile        on;
  tcp_nopush      on;
  tcp_nodelay     on;
  server_tokens   off;
  gzip            on;
  gzip_static     on;
  gzip_comp_level 5;
  gzip_min_length 1024;
  keepalive_timeout  65;
  error_page 404 = @redir_main;

  ## LETSENCRYPT ##
  location /.well-known/acme-challenge {
	root /var/lib/letsencrypt;
	default_type "text/plain";
	try_files $uri =404;
  }

  location /media {
	autoindex on;
  }

  ## Only allow specific logins
  # Redirect this to HTTPS!
  location ~ ^/textpattern/index\.php$ {
  	if ($scheme != 'https') {
  		return 301 https://sysadministrivia.com$request_uri;
  		}
	# Only allow login from the VPN
	allow 127.0.0.0/8;
	allow <VPN Subnet or VPN gateway IP>;
    deny all;
  	include includes/php-txp-simple.inc;
  }

  ## Raw XML feeds
  location ~* ^/feed/((pod|ogg)cast|itunes|google)\.xml$ {
	rewrite ^/feed/((pod|ogg)cast|itunes|google)\.xml$ /$1 redirect;
  }

  ## Handle OGG/PODcast meta info ##
  location ~* ^/feed\.xml?$ {
  	rewrite .*$ /feed/podcast.xml;
  }
  location ~* ^/subscribe/podcast {
	rewrite .*$ /feed/podcast.xml;
  }
  location ~* ^/subscribe/oggcast {
  	rewrite .*$ /feed/oggcast.xml;
  }

  # External feeds
  location /subscribe/itunes {
	rewrite ^/subscribe/itunes$ https://itunes.apple.com/us/podcast/sysadministrivia/id969859929;
  }
  location /subscribe/google {
	rewrite ^/subscribe/google$ https://play.google.com/music/m/I4mwufntcjlvzetk7b72hba7mem?t=Sysadministrivia;
  }

  # Make URLs flexible
  location ~* ^/notes/? {
  	rewrite .*$ /episodes;
  }
  location ~* ^/(category/)?season/0 {
  	rewrite ^/(category/)?season/0(/.*)?$ /category/season/pilot$2 redirect;
  }
  location ~* ^/season/[0-9]+$ {
  	rewrite ^/season/(.*) /category/season/$1;
  }

  # Redirect oopsies to /
  location @redir_main {
	return 302 /;
  }

  location ~* ^/feed/(oggcast|podcast|itunes|google)\.xml$ {
  	types {
  		application/rss+xml xml;
		}
  	default_type application/rss+xml;
  }

  location /raw {
  	autoindex on;
  	}

  location ~ /\.(htaccess|htpasswd|ini|phps|fla|psd|log|sh|tpl|git)$ {
  	deny all;
  	}

  location ~* /.(gif|jpg|jpeg|png|ico|wmv|3gp|avi|mpg|mpeg|mp4|flv|mp3|mid|js|css|wml|swf|ogg)$ {
  	expires max;
  	add_header Pragma public;
  	add_header Cache-Control "public, must-revalidate, proxy-revalidate";
  }

  location / {
  	# Needed for SEO Friendly URLs
  	try_files $uri $uri/ /index.php?$args;
  }

  # php-fpm configuration
  location ~ .php$ {
	include includes/php-txp.inc;
  }
}

### THE BELOW IS TO REDIRECT TO THE NAKED SITE *ONLY* ###
server {
  listen [::]:80;
  listen [::]:443 ssl;
  server_name www.sysadministrivia.com;

  ## SSL ##
  ssl_certificate /etc/nginx/ssl/certs/sysadministrivia-bundle.crt;
  ssl_certificate_key /etc/nginx/ssl/keys/sysadministrivia.key;
  ssl_session_timeout  5m;
  ssl_protocols TLSv1.1 TLSv1.2;
  ssl_ciphers  'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
  ssl_dhparam /etc/nginx/ssl/keys/dh4096.pem;
  ssl_prefer_server_ciphers   on;
  ssl_session_cache shared:SSL:10m;
  # HSTS #
  add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
  ssl_stapling on;
  ssl_stapling_verify on;
  ssl_trusted_certificate /etc/nginx/ssl/certs/sysadministrivia-bundle.crt;

  ## DNS ##
  resolver 4.2.2.1 4.2.2.2 valid=300s;
  resolver_timeout 10s;

  ## LOGGING ##
  access_log /var/log/nginx/vhost/sysadmin/access.log main;
  error_log /var/log/nginx/vhost/sysadmin/error.log;
  #error_log /var/log/nginx/vhost/sysadmin/error.log debug;

  # ditch the www. forevar. stop it, it's not 1995 anymore.
  return 301 $scheme://sysadministrivia.com$request_uri;

  # We want to force HTTPS for these URLs.
  location ~ ^/textpattern(/(.*)?)$ {
	return 301 https://sysadministrivia.com$request_uri;
  }
}

Note that your paths may differ, you may have to modify it or create some directories, etc. You may want to refer to my devblog’s post on getting an A+ in Qualys’ SSLabs for more information on how that SSL/TLS is set up (be sure to also refer to certs).

Domain name

You’ll need a domain name. Obviously. We use (but certainly aren’t sponsored by) Namecheap. If you use Linode, they have nameservers you can use. Or you can run your own nameserver (I recommend PowerDNS). Otherwise you’ll have to set your records up at your registrar (they usually offer gratis nameservers).

SSL/TLS Certificates

You’ll also need TLS certs. We use LetsEncrypt. I have some notes on on LE over at my devblog. Don’t forget to gen your own DH params because Logjam is a thing.

Software

Textpattern

Lastly, you’ll need the web application to drive/serve your podcast- at the very LEAST your XML feeds, but it’s a good idea to have the ability to present other media/content as well. For that, we use Textpattern. (Be sure to also see Nginx!)

However, you can’t just install it and start broadcasting feeds. After doing initial setup, you should run the Podloader’s SQL import. By default it creates (overwriting any existing) a table called “myTBL”. If you wanted to instead use a table called, say, “episodes”, you’d do sed -i -e 's/myTBL/episodes/g' blank.schema.sql first.

Once you’re ready to create the table, you’d simply import the .sql dump. Something like this should do it: mysql textpattern < blank.schema.sql. Obviously replace “textpattern” with whatever your TXP DB is named.

You’ll also need to install a couple Textpattern plugins:

  • adi_menu (we use 1.4beta2; you should be able to find a copy on the TXP forums)- not strictly necessary, but it’s very helpful. Our menu is laid out like this.
  • mg_setheader (we use version 0.1)
  • pax_grep (we’re on 0.2.1)
  • smd_query (we use 0.50)
  • soo_toc (version 0.1.4)- not strictly necessary, but useful for adding table of contents to make your shownotes easier to navigate

Once those things are done, we need to customize Textpattern a bit. You’ll need to create categories for your articles- you can see how we set ours up here. Note that this integrates very tightly with adi_menu so note the naming. You’ll also need to set up some links to get that menu working as well; you can see how we did it here.

Now we need to set up some pages (Presentation > Pages). These manage generation of the specific XML feeds (which is what podcast clients use). Note that the below assume you named your table “episodes”.

feed_google:


<txp:mg_setheader value="application/rss+xml"/><txp:smd_query query="SELECT episode FROM episodes"><txp:variable name="episodecnt">{smd_allrows}</txp:variable></txp:smd_query><?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0">
	<channel>
		<title><txp:site_name/></title>
		<link><txp:site_url/></link>
		<language>en-us</language>
		<copyright>YOUR COPYRIGHT HERE</copyright>
		<googleplay:author>THE PODCAST AUTHOR HERE</googleplay:author>
		<googleplay:email>YOUR EMAIL HERE</googleplay:email>
		<googleplay:image href='YOUR IMAGE URL HERE'/>
		<googleplay:description>YOUR DESCRIPTION HERE</googleplay:description>
		<googleplay:explicit>Yes</googleplay:explicit>
		<googleplay:category text="CATEGORY NAME HERE"/>
		<txp:article_custom form="feed_google" section="episodes" limit='<txp:variable name="episodecnt"/>'/>
		</channel>
</rss>

Then make sure you add your podcast when everything’s all done.

feed_itunes:


<txp:mg_setheader value="application/rss+xml"/><?xml version="1.0" encoding="UTF-8"?><txp:smd_query query="SELECT episode FROM episodes"><txp:variable name="episodecnt">{smd_allrows}</txp:variable></txp:smd_query>
<rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xml:lang="en" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title><txp:site_name/></title>
		<link><txp:site_url/></link>
		<atom:link href="<txp:site_url/>feed/itunes.xml" rel="self" type="application/rss+xml" />
		<description>YOUR DESCRIPTION HERE</description>
		<generator>Textpattern (and magic)</generator>
		<lastBuildDate><txp:php>echo date(DATE_RFC2822);</txp:php></lastBuildDate>
		<language>en-us</language>
		<copyright>YOUR COPYRIGHT HERE</copyright>
		<itunes:image href="YOUR IMAGE URL HERE" />
		<image>
			<url>YOUR THUMBNAIL IMAGE URL HERE</url>
			<title><txp:site_name/></title>
			<link><txp:site_url/></link>
		</image>
		<itunes:summary>YOUR DESCRIPTION HERE</itunes:summary>
		<itunes:subtitle><txp:site_slogan/></itunes:subtitle>
		<itunes:owner>
			<itunes:name>THE PODCAST OWNER HERE</itunes:name>
			<itunes:email>THEIR EMAIL HERE</itunes:email>
		</itunes:owner>
		<itunes:author>THE PODCAST AUTHOR HERE</itunes:author>
		<itunes:explicit>yes</itunes:explicit>
		<itunes:category text="CATEGORY 1 HERE"></itunes:category>
		<itunes:category text="CATEGORY 1 HERE"><itunes:category text="SUBCATEGORY HERE" /></itunes:category>
		<itunes:category text="CATEGORY 2 HERE"></itunes:category>
		<itunes:keywords>YOUR KEYWORDS HERE</itunes:keywords><txp:article_custom form="feed_itunes" section="episodes" sort="Posted desc" limit='<txp:variable name="episodecnt"/>'/>
		</channel>
</rss>

Then make sure you add your podcast when everything’s all done.

feed_mp3:


<txp:mg_setheader value="application/rss+xml"/><txp:smd_query query="SELECT episode FROM episodes"><txp:variable name="episodecnt">{smd_allrows}</txp:variable></txp:smd_query><?xml version="1.0" encoding="UTF-8"?>
<rss xml:lang="en" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>The <txp:site_name/> Podcast</title>
		<link><txp:site_url/></link>
		<atom:link href="<txp:site_url/>feed/podcast.xml" rel="self" type="application/rss+xml" />
		<description>YOUR DESCRIPTION HERE</description>
		<generator>Textpattern (and magic)</generator>
		<managingEditor>MANAGER@EMAIL.TLD (MANAGER NAME)</managingEditor>
		<webMaster>ADMIN@EMAIL.TLD (ADMIN NAME)</webMaster>
		<pubDate><txp:php>echo date(DATE_RFC2822);</txp:php></pubDate>
		<lastBuildDate><txp:php>echo date(DATE_RFC2822);</txp:php></lastBuildDate>
		<language>en-us</language>
		<copyright>YOUR COPYRIGHT HERE</copyright>
		<image>
			<url>YOUR IMAGE URL HERE</url>
			<title>The <txp:site_name/> Podcast</title>
			<link><txp:site_url/></link>
		</image>
		<docs>http://www.rssboard.org/rss-specification</docs>
		<category>CATEGORY 1 HERE</category>
		<category>CATEGORY 2 HERE</category>
		<category>CATEGORY 3 HERE</category>
		<category>CATEGORY 4 HERE</category>
		<txp:article_custom form="feed_mp3" section="episodes" limit='<txp:variable name="episodecnt"/>'/>
		</channel>
</rss>

feed_ogg:


<txp:mg_setheader value="application/rss+xml"/><?xml version="1.0" encoding="UTF-8"?><txp:smd_query query="SELECT episode FROM episodes"><txp:variable name="episodecnt">{smd_allrows}</txp:variable></txp:smd_query>
<rss xml:lang="en" version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<title>The <txp:site_name/> Oggcast</title>
		<link><txp:site_url/></link>
		<atom:link href="<txp:site_url/>feed/oggcast.xml" rel="self" type="application/rss+xml" />
		<description>YOUR DESCRIPTION HERE</description>
		<generator>Textpattern (and magic)</generator>
		<managingEditor>MANAGER@EMAIL.TLD (MANAGER NAME)</managingEditor>
		<webMaster>ADMIN@EMAIL.TLD (ADMIN NAME)</webMaster>
		<pubDate><txp:php>echo date(DATE_RFC2822);</txp:php></pubDate>
		<lastBuildDate><txp:php>echo date(DATE_RFC2822);</txp:php></lastBuildDate>
		<language>en-us</language>
		<copyright>YOUR COPYRIGHT HERE</copyright>
		<image>
			<url>YOUR IMAGE URL HERE</url>
			<title>The <txp:site_name/> Oggcast</title>
			<link><txp:site_url/></link>
		</image>
		<docs>http://www.rssboard.org/rss-specification</docs>
		<category>CATEGORY 1 HERE</category>
		<category>CATEGORY 2 HERE</category>
		<category>CATEGORY 3 HERE</category>
		<category>CATEGORY 4 HERE</category>
		<txp:article_custom form="feed_ogg" section="episodes" limit='<txp:variable name="episodecnt"/>'/>
		</channel>
</rss>

Then we need to set up some forms (Presentation > Forms). These are used to aggregate each episode into the feed’s XML.

feed_google:


<txp:variable name="seasonnum"><txp:pax_grep from="/(S[0-9]+)E[0-9]+/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:variable name="episodenum"><txp:pax_grep from="/S[0-9]+(E[0-9]+)/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:smd_query query='SELECT file_prefix,sha_mp3,bytesize_mp3,length FROM episodes WHERE episode="<txp:article_url_title />"'><txp:variable name="filename_full">media/<txp:variable name="seasonnum"/>/<txp:variable name="episodenum"/>/mp3/{file_prefix}.mp3</txp:variable><txp:variable name="cast_size">{bytesize_mp3}</txp:variable><txp:variable name="cast_length">{length}</txp:variable><txp:variable name="guid_hash">{sha_mp3}</txp:variable></txp:smd_query><txp:smd_query query='SELECT email FROM txp_users WHERE RealName="<txp:author/>"'><txp:variable name="rss_author">{email}</txp:variable></txp:smd_query>
		<item>
			<title><txp:title/></title>
			<googleplay:description><txp:if_excerpt><txp:pax_grep from="{<p>},{</p>}" to=""><txp:excerpt/></txp:pax_grep> (Full shownotes at <txp:site_url/>episodes/<txp:article_url_title/>)<txp:else/>Please see the shownotes at <txp:site_url/>episodes/<txp:article_url_title/></txp:if_excerpt></googleplay:description>
			<googleplay:author><txp:author/></googleplay:author>
			<googleplay:image href='YOUR IMAGE URL HERE'/>
			<googleplay:explicit>Yes</googleplay:explicit>
			<enclosure url="<txp:site_url/><txp:variable name="filename_full"/>" length='<txp:variable name="cast_size"/>' type="audio/x-mp3" />
			<guid><txp:variable name="guid_hash"/></guid>
			<pubDate><txp:posted format="%a, %d %b %Y %H:%M:%S %z" /></pubDate>
		</item>

feed_itunes:


<txp:variable name="seasonnum"><txp:pax_grep from="/(S[0-9]+)E[0-9]+/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:variable name="episodenum"><txp:pax_grep from="/S[0-9]+(E[0-9]+)/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:smd_query query='SELECT file_prefix,sha_mp3,bytesize_mp3,length FROM episodes WHERE episode="<txp:article_url_title />"'><txp:variable name="filename_full">media/<txp:variable name="seasonnum"/>/<txp:variable name="episodenum"/>/mp3/{file_prefix}.mp3</txp:variable><txp:variable name="cast_size">{bytesize_mp3}</txp:variable><txp:variable name="cast_length">{length}</txp:variable><txp:variable name="guid_hash">{sha_mp3}</txp:variable></txp:smd_query><txp:smd_query query='SELECT email FROM txp_users WHERE RealName="<txp:author/>"'><txp:variable name="rss_author">{email}</txp:variable></txp:smd_query>
		<item>
			<title><txp:title/></title>
			<itunes:subtitle>The <txp:site_name/> Podcast</itunes:subtitle>
			<itunes:summary><![CDATA[<txp:if_excerpt><txp:pax_grep from='{<p>},{</p>},{<span class="caps">},{</span>}' delimiter="," to=""><txp:excerpt/></txp:pax_grep> (Full shownotes at <txp:site_url/>episodes/<txp:article_url_title/>)<txp:else/>Please see the shownotes at <txp:site_url/>episodes/<txp:article_url_title/></txp:if_excerpt>]]></itunes:summary>
			<description>The <txp:site_name/> Podcast</description>
			<enclosure url="<txp:site_url/><txp:variable name="filename_full"/>" length="<txp:variable name='cast_size'/>" type='audio/mpeg' />
			<guid isPermaLink="false"><txp:variable name="guid_hash"/></guid>
			<itunes:duration><txp:variable name="cast_length"/></itunes:duration>
			<itunes:image href="YOUR IMAGE URL HERE" />
			<author><txp:variable name="rss_author"/> (<txp:author/>)</author>
			<itunes:author><txp:author/></itunes:author>
			<itunes:keywords>YOUR KEYWORDS HERE</itunes:keywords>
			<itunes:explicit>Yes</itunes:explicit>
			<pubDate><txp:posted format="%a, %d %b %Y %H:%M:%S %z" /></pubDate>
		</item>

feed_mp3:


<txp:variable name="seasonnum"><txp:pax_grep from="/(S[0-9]+)E[0-9]+/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:variable name="episodenum"><txp:pax_grep from="/S[0-9]+(E[0-9]+)/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:smd_query query='SELECT file_prefix,sha_mp3,bytesize_mp3,length FROM episodes WHERE episode="<txp:article_url_title />"'><txp:variable name="filename_full">media/<txp:variable name="seasonnum"/>/<txp:variable name="episodenum"/>/mp3/{file_prefix}.mp3</txp:variable><txp:variable name="cast_size">{bytesize_mp3}</txp:variable><txp:variable name="cast_length">{length}</txp:variable><txp:variable name="guid_hash">{sha_mp3}</txp:variable></txp:smd_query><txp:smd_query query='SELECT email FROM txp_users WHERE RealName="<txp:author/>"'><txp:variable name="rss_author">{email}</txp:variable></txp:smd_query>
		<item>
			<title><txp:title/></title>
			<description><txp:if_excerpt><txp:pax_grep from="{<p>},{</p>}" to=""><txp:excerpt/></txp:pax_grep> (Full shownotes at <txp:site_url/>episodes/<txp:article_url_title/>)<txp:else/>Please see the shownotes at <txp:site_url/>episodes/<txp:article_url_title/></txp:if_excerpt></description>
			<enclosure url="<txp:site_url/><txp:variable name="filename_full"/>" length='<txp:variable name="cast_size"/>' type="audio/mpeg" />
			<guid isPermaLink="false"><txp:variable name="guid_hash"/></guid>
			<author><txp:variable name="rss_author"/> (<txp:author/>)</author>
			<category>CATEGORY 1 HERE</category>
			<category>CATEGORY 2 HERE</category>
			<link><txp:site_url/>episodes/<txp:article_url_title/></link>
			<comments><txp:site_url/>episodes/<txp:article_url_title/>#comments-head</comments>
			<pubDate><txp:posted format="%a, %d %b %Y %H:%M:%S %z" /></pubDate>
		</item>

feed_ogg:


<txp:variable name="seasonnum"><txp:pax_grep from="/(S[0-9]+)E[0-9]+/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:variable name="episodenum"><txp:pax_grep from="/S[0-9]+(E[0-9]+)/" to="${1}"><txp:article_url_title/></txp:pax_grep></txp:variable><txp:smd_query query='SELECT file_prefix,sha_ogg,bytesize_ogg,length FROM episodes WHERE episode="<txp:article_url_title />"'><txp:variable name="filename_full">media/<txp:variable name="seasonnum"/>/<txp:variable name="episodenum"/>/ogg/{file_prefix}.ogg</txp:variable><txp:variable name="cast_size">{bytesize_ogg}</txp:variable><txp:variable name="cast_length">{length}</txp:variable><txp:variable name="guid_hash">{sha_ogg}</txp:variable></txp:smd_query><txp:smd_query query='SELECT email FROM txp_users WHERE RealName="<txp:author/>"'><txp:variable name="rss_author">{email}</txp:variable></txp:smd_query>
		<item>
			<title><txp:title/></title>
			<description><txp:if_excerpt><txp:pax_grep from="{<p>},{</p>}" to=""><txp:excerpt/></txp:pax_grep> (Full shownotes at <txp:site_url/>episodes/<txp:article_url_title/>)<txp:else/>Please see the shownotes at <txp:site_url/>episodes/<txp:article_url_title/></txp:if_excerpt></description>
			<enclosure url="<txp:site_url/><txp:variable name="filename_full"/>" length='<txp:variable name="cast_size"/>' type="audio/ogg" />
			<guid isPermaLink="false"><txp:variable name="guid_hash"/></guid>
			<author><txp:variable name="rss_author"/> (<txp:author/>)</author>
			<category>CATEGORY 1 HERE</category>
			<category>CATEGORY 2 HERE</category>
			<link><txp:site_url/>episodes/<txp:article_url_title/></link>
			<comments><txp:site_url/>episodes/<txp:article_url_title/>#comments-head</comments>
			<pubDate><txp:posted format="%a, %d %b %Y %H:%M:%S %z" /></pubDate>
		</item>

Now, assuming everything else is all set up correctly, when you post shownotes, you’d format them as normally, and place them in the “episodes” section you created earlier, and set the category to the season, and then (this is the magic part, so make sure you do this) set the “URL-only title” under Meta to be the matching entry in your episodes table (if you’re using the blank.schema.sql dump, this column will be called “episode”- reference the queries done above).

Note that there will be some queries done every time the feed is fetched. For this reason, I recommend you set up nginx caching for your feed URLs but it isn’t necessary.

Uploading

You’ll also need a way to create and upload your media files. We use Mumble to converse with each other (we don’t record in the same room, since we live kind of far apart from each other). While Mumble has recording support built-in, we all actually run local instances of Audacity as we’ve found we get better recording with that, even for local tracks. We then export to FLAC (since it’s lossless) and upload to a central location via scp. I download the co-hosts’ tracks and do a bit of preliminary track cleanup, then re-upload. Our editor then downloads using WinSCP and does her thing, and uploads the (separately exported) edited FLAC tracks again. Then I use Podloader (a python script I wrote) to convert the final cut in FLAC over to MP3 and OGG, insert the entries in the “meta” table (see Textpattern), sign the episode files, and upload the episode media and GPG signatures.

WHEW.

Author
Categories HOWTOs

Comments

There are currently no comments on this article.

Comment...

Enter your comment below. Fields marked * are required. You must preview your comment before submitting it.