Create a Podcast Feed with PHP
In this article, I’ll demonstrate how to use PHP to generate a podcast feed. We’ll create a simple administrative interface to configure the podcast metadata, add and list episodes, and then go through the generation of the podcast feed itself (which is simply an RSS document).
I’ll base the application on this skeleton Slim app, though many of the principles will remain the same whatever framework you choose. You can download the application itself from GitHub or work through the tutorial and build it as you go along.
Ready? Let’s get started!
Setting up the Application
The Slim skeleton app contains the basic set up for a web application powered by Slim, and also pulls in NotORM for querying databases and Twig for working with templates. We’ll also use the getID3 library to work with the audio files’ metadata, so open composer.json
file and add the following require:
"nass600/get-id3": "dev-master"
Run composer.phar install
and the application’s dependencies will be downloaded into a vendor
directory.
Create the directories data
and public/uploads
and ensure they are both writeable by the web server. The data
directory will store some additional application configuration details, and public/uploads
will hold our podcast audio uploads.
I’m going to use MySQL for storing the application’s data, but you can choose whichever RDBMS you feel comfortable with. Initially the database only needs to store episode information, so our schema will be rather simple:
CREATE TABLE episodes (
id INTEGER NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
summary TEXT NULL,
description TEXT NULL,
audio_file VARCHAR(255) NOT NULL,
created INTEGER NOT NULL,
PRIMARY KEY (id)
);
Copy the file config/config.php.example
to config/config.php
and update it with your database connection credentials. Also, it’s a good idea to add the data
and public/uploads
directories as configuration entries so we can reference them elsewhere in our code without hardcoding them.
The Configuration Page
Now we can create a page that will be used to configure the podcast feed itself. A feed incorporates a bunch of metadata such as a title, category information, the owner, and a summary of what it’s about. I’ll keep things as simple as possible and the configuration interface will ultimately be just a form which saves a serialized representation of the configuration to a file.
Cut and paste the following to data/configuration.txt
and make sure that the file is writeable by the web server.
a:12:{s:5:"title";s:16:"A Sample Podcast";s:8:"language";s:2:"en";s:9:"copyright";s:14:"SitePoint 2013";s:8:"subtitle";s:16:"Podcast subtitle";s:6:"author";s:9:"SitePoint";s:7:"summary";s:31:"Generating a podcast using PHP.";s:11:"description";s:58:"This is a demonstration of generating a Podcast using PHP.";s:10:"owner_name";s:9:"SitePoint";s:11:"owner_email";s:22:"no-reply@sitepoint.com";s:10:"categories";a:4:{i:0;s:30:"Education|Education Technology";i:1;s:18:"Education|Training";i:2;s:21:"Technology|Podcasting";i:3;s:26:"Technology|Software How-To";}s:8:"keywords";s:21:"PHP,podcasts,tutorial";s:8:"explicit";s:2:"no";}
Now we’ll create a very simple class to load and save the configuration. Save the following as lib/SimpleFileConfiguration.php
:
<?php
class SimpleFileConfiguration
{
const DATA_FILE = 'configuration.txt';
public $dataFile;
public function __construct(Pimple $c) {
$this->dataFile = $c['config']['path.data'] . $this::DATA_FILE;
}
public function load() {
$contents = file_get_contents($this->dataFile);
return unserialize($contents);
}
public function save($configuration) {
$contents = serialize($configuration);
file_put_contents($this->dataFile, $contents);
}
}
Add the instantiation of SimpleFileConfiguration
to include/services.php
: so it’ll be easily accessible throughout the application:
<?php
$c['PodcastConfig'] = $c->share(function ($c) {
return new SimpleFileConfiguration($c);
}
Create the file routes/configure.php
with the /configure
routes:
<?php
$app->get('/configure', function () use ($app, $c) {
$config = $c['PodcastConfig']->load();
$app->view()->setData(array(
'configuration' => $config
));
$app->render('configure.html');
});
$app->post('/configure', function () use ($app, $c) {
$data = $app->request()->post();
$c['PodcastConfig']->save($data);
$app->flash('success', 'Configuration Saved');
$app->redirect('/configure');
});
And finally, create the template file templates/configure.html
that will present the form to update the configuration values.
<form method="post" enctype="multipart/form-data" action="/configure">
<fieldset>
<legend>Details</legend>
<label>Title</label>
<input name="title" type="text" placeholder="Please enter a title..." value="{{ configuration.title }}" class="input-xlarge">
<label>Language</label>
<select name="language">
<option value="en">English</select>
<!-- You can put more languages here... -->
</select>
<label>Copyright</label>
<input name="copyright" type="text" placeholder="Please enter copyright information..." value="{{ configuration.copyright }}" class="input-xlarge">
<label>Subtitle</label>
<input name="subtitle" type="text" placeholder="Optionally enter a subtitle..." value="{{ configuration.subtitle }}" class="input-xlarge">
<label>Author</label>
<input name="author" type="text" placeholder="Please enter the Podcast's author..." value="{{ configuration.author }}" class="input-xlarge">
<label>Summary</label>
<textarea name="summary" cols="50" rows="2" placeholder="Please enter a summary..." class="input-xlarge">{{ configuration.summary }}</textarea>
<label>Description</label>
<textarea name="description" cols="50" rows="5" placeholder="Please enter a description..." class="input-xlarge">{{ configuration.description }}</textarea>
</fieldset>
<fieldset>
<legend>Owner Details</legend>
<label>Name</label>
<input name="owner_name" type="text" placeholder="Please enter a the podcast owner's name..." value="{{ configuration.owner_name }}" class="input-xlarge">
<label>E-mail</label>
<input name="owner_email" type="text" placeholder="Please enter a the podcast owner's e-mail..." value="{{ configuration.owner_email }}" class="input-xlarge">
</fieldset>
<fieldset>
<legend>Categorization</legend>
<label>Categories</label>
<select name="categories[]" multiple="true" class="input-xlarge">
<optgroup label="Arts">
<option value="Arts|Design">Design</option>
<option value="Arts|Fashion & Beauty">Fashion & Beauty</option>
<option value="Arts|Food">Food</option>
<option value="Arts|Literature">Literature</option>
...
</optgroup>
</select>
<label>Keywords</label>
<textarea name="keywords" cols="50" rows="2" placeholder="Optionally enter some keywords (comma-separated)..." class="input-xlarge">{{ configuration.keywords }}</textarea>
<label>Explicit content?</label>
<select name="explicit">
<option value="no" {% if configuration.explicit == 'no' %}selected="selected"{% endif %}>No</select>
<option value="yes" {% if configuration.explicit == 'yes' %}selected="selected"{% endif %}>Yes</select>
</select>
</fieldset>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Save Configuration</button>
</div>
</form>
I’ve copied the list of available podcast categories from the list defined by Apple for submission to iTunes. Some of them, such as Comedy, are self-contained categories and others have child categories. For those with children, I’ve used the pipe character as a separator in the option values.
Adding an Episode
Next up we’ll create the page where we can create a new podcast episode. Let’s define the routes in routes/podcast.php
:
<?php
$app->get('/episode', function () use ($app) {
$app->render('episode-add.html');
});
$app->post('/episode', function () use ($app, $c) {
$db = $c['db'];
$data = $app->request()->post();
$dir = $c['config']['path.uploads'];
$filepath = $dir . basename($_FILES['file']['name']);
move_uploaded_file($_FILES['file']['tmp_name'], $filepath);
$id = $db->episodes->insert(array(
'title' => $data['title'],
'author' => $data['author'],
'summary' => $data['summary'],
'description' => $data['description'],
'audio_file' => $filepath,
'created' => time()
));
$app->flash('success', 'Episode Created');
$app->redirect('/podcast');
});
I’m keeping things simple here; there’s no validation and uploading the audio file is very basic, but you get the idea. I’m also not going to go over implementing edit or delete functionality here; it’s pretty straightforward stuff that you can implement yourself later.
Now create the template file templates/episode-add.html
with the form to add a new podcast:
<form method="post" enctype="multipart/form-data" action="/episode">
<fieldset>
<legend>Details</legend>
<label>Title</label>
<input name="title" type="text" placeholder="Please enter a title...">
<label>Author</label>
<input name="author" type="text" placeholder="Please enter the author..." value="">
<label>Summary</label>
<textarea name="summary" cols="50" rows="2" placeholder="Please enter a summary..."></textarea>
<label>Description</label>
<textarea name="description" cols="50" rows="5" placeholder="Please enter a description..."></textarea>
<label>Audio File</label>
<input name="file" type="file" />
<div class="form-actions">
<button type="submit" class="btn btn-primary">Add Episode</button>
</div>
</fieldset>
</form>
Listing Podcast Episodes
To create an overview page which lists all of the episodes in the podcast, we can grab the list of episodes from the database using NotORM and pass the result directly to the view.
Add the following to routes/podcast.php
:
$app->get('/podcast', function () use ($app, $c) {
$db = $c['db'];
$app->view()->setData(array(
'podcast' => $db->episodes()->order('created DESC')
));
$app->render('podcast.html');
});
And then create templates/podcast.html
:
<table class="table table-bordered table-striped">
<thead>
<tr>
<td>Title</td>
<td>Summary</td>
</tr>
</thead>
<tbody>
{% for episode in podcast %}
<tr>
<td>{{ episode.title }}</td>
<td>{{ episode.summary }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
We need to publish a feed to make the podcast available, which means getting our hands dirty with some XML. For that I’ll define the route /podcast.xml
and use DOMDocument
.
<?php
$app->get('/podcast.xml', function () use ($app, $c) {
$db = $c['db'];
$conf = $c['PodcastConfig']->load();
$xml = new DOMDocument();
$root = $xml->appendChild($xml->createElement('rss'));
$root->setAttribute('xmlns:itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd');
$root->setAttribute('xmlns:media', 'http://search.yahoo.com/mrss/');
$root->setAttribute('xmlns:feedburner', 'http://rssnamespace.org/feedburner/ext/1.0');
$root->setAttribute('version', '2.0');
$link = sprintf(
'%s://%s/podcast',
$app->request()->getScheme(),
$app->request()->getHost()
);
$chan = $root->appendChild($xml->createElement('channel'));
$chan->appendChild($xml->createElement('title', $conf['title']));
$chan->appendChild($xml->createElement('link', $link));
$chan->appendChild($xml->createElement('generator', 'SitePoint Podcast Tutorial'));
$chan->appendChild($xml->createElement('language', $conf['language']));
...
foreach ($db->episodes()->order('created ASC') as $episode) {
$audioURL = sprintf(
'%s://%s/uploads/%s',
$app->request()->getScheme(),
$app->request()->getHost(),
basename($episode['audio_file'])
);
$item = $chan->appendChild($xml->createElement('item'));
$item->appendChild($xml->createElement('title', $episode['title']));
$item->appendChild($xml->createElement('link', $audioURL));
$item->appendChild($xml->createElement('itunes:author', $episode['title']));
$item->appendChild($xml->createElement('itunes:summary', $episode['summary']));
$item->appendChild($xml->createElement('guid', $audioURL));
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$enclosure = $item->appendChild($xml->createElement('enclosure'));
$enclosure->setAttribute('url', $episode['audio_file']);
$enclosure->setAttribute('length', filesize($episode['audio_file']));
$enclosure->setAttribute('type', finfo_file($finfo, $episode['audio_file']));
$item->appendChild($xml->createElement('pubDate', date('D, d M Y H:i:s O', $episode['created'])));
$getID3 = new getID3();
$fileinfo = $getID3->analyze($episode['audio_file']);
$item->appendChild($xml->createElement('itunes:duration', $fileinfo['playtime_string']));
}
$xml->formatOutput = true;
$res= $app->response();
$res['Content-Type'] = 'application/json';
print $xml->saveXML();
});
The highlights from the feed generation are:
- Following the requirements on Apple’s website, we create the XML document with the root element
rss
and provide the necessary channel information. I hardcoded thegenerator
tag here; really you can set it to whatever you like. - We iterate through the episodes and create an
item
element for each one. If we had a unique page for each episode – something pretty straightforward to set up – we could use it for the Globally Unique Identifier (GUID), but for now we’re just using the URL of the audio file itself since obviously that will be unique. - To create the
enclosure
element which contains the URL, file size, and MIME type of the actual audio file, we usefilesize()
and the Fileinfo extension to get the MIME type. - To include the duration of the audio track, we use the getID3 library.
- We finish everything off by setting the correct headers and outputting the XML.
Navigate to /podcast.xml
and you should see the XML for the podcast feed. Run it through a few feed validators (tools.forret.com/podcast/validator.php, castfeedvalidator.com and feedvalidator.org) for good measure, and then you’re ready to submit it to iTunes!
Summary
In this article, I’ve shown how you can build a simple application to create and publish your own podcasts. There are a number of things missing from this implementation, such as editing and deleting episodes, proper validation and security, etc. They fall outside the scope of this article but are simple to add. Feel free to download the source from GitHub and code away!
Image via Fotolia