diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..404bfb6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+filesize.cache
+/feed_*.rss2
diff --git a/genfeed.php b/genfeed.php
deleted file mode 100644
index 878b52b..0000000
--- a/genfeed.php
+++ /dev/null
@@ -1,189 +0,0 @@
-loadHTML($src_data);
-
-$anchors = $dom->getElementsByTagName('a');
-
-$file_list = array();
-foreach ($anchors as $anchor) {
- $title = $anchor->nodeValue;
- $href = $anchor->attributes->getNamedItem('href')->nodeValue;
-
- if (substr($href, -4) !== '.mp3') {
- continue;
- }
-
- $title = urldecode(substr($href, 0, -4)); // strip off ".mp3" file extension
- $url = $sourceUrl . $href;
-
- $file_list[$url] = trim($title);
-}
-
-unset($dom);
-
-$namespaceURIs = [
- 'xmlns' => 'http://www.w3.org/2000/xmlns/',
- 'atom' => 'http://www.w3.org/2005/Atom',
- 'content' => 'http://purl.org/rss/1.0/modules/content/',
- 'itunes' => 'http://www.itunes.com/DTDs/Podcast-1.0.dtd',
- 'spotify' => 'http://www.spotify.com/ns/rss',
- 'psc' => 'https://podlove.org/simple-chapters/', // for chapter marks
- 'dcterms' => 'https://purl.org/dc/terms', // for validity periods
-];
-
-date_default_timezone_set('America/New_York');
-
-$dom = new DOMDocument('1.0', 'utf-8');
-$dom->formatOutput = true;
-$root = $dom->createElement('rss');
-$root->appendChild(new DOMAttr('version', '2.0'));
-$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', $namespaceURIs['atom']);
-$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:content', $namespaceURIs['content']);
-$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:itunes', $namespaceURIs['itunes']);
-$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:spotify', $namespaceURIs['spotify']);
-// $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:dcterms', $namespaceURIs['dcterms']);
-$root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:psc', $namespaceURIs['psc']);
-$dom->appendChild($root);
-
-$channel = $dom->createElement('channel');
-$channel->appendChild($dom->createElement('title', 'Electronic Warfare'));
-$channel->appendChild($dom->createElement('link', 'https://www.facebook.com/louis.overfiend'));
-$channel->appendChild($dom->createElement('copyright', 'BassDrive.com / Louis Overfiend'));
-$channel->appendChild($dom->createElement('itunes:author', 'BassDrive.com / Louis Overfiend'));
-
-$atom_link = $dom->createElement('atom:link');
-$atom_link->appendChild(new DOMAttr('href', $baseUrl . 'index.rss2'));
-$atom_link->appendChild(new DOMAttr('rel', 'self'));
-$atom_link->appendChild(new DOMAttr('type', 'application/rss+xml'));
-$channel->appendChild($atom_link);
-
-$channel->appendChild($dom->createElement('description', trim(str_replace('&', '&', file_get_contents('intro.txt')))));
-$channel->appendChild($dom->createElement('language', 'en-us'));
-
-$image = $dom->createElement('image');
-$image->appendChild($dom->createElement('url', $baseUrl . 'rsslogo.jpg')); // RSS spec says max. dimensions are 144x400
-$image->appendChild($dom->createElement('title', 'Electronic Warfare'));
-$image->appendChild($dom->createElement('link', 'https://www.facebook.com/louis.overfiend'));
-$image->appendChild($dom->createElement('width', '144'));
-$image->appendChild($dom->createElement('height', '144'));
-$channel->appendChild($image);
-$i_image = $dom->createElement('itunes:image');
-$i_image->appendChild(new DOMAttr('href', $baseUrl . 'biglogo.jpg')); // Apple requires min.(!) size of 1400x1400, 1:1 aspect
-$channel->appendChild($i_image);
-
-$channel->appendChild($dom->createElement('lastBuildDate', date(RFC_822)));
-$channel->appendChild($dom->createElement('docs', 'http://blogs.law.harvard.edu/tech/rss'));
-$channel->appendChild($dom->createElement('generator', 'Handcrafted with love'));
-$channel->appendChild($dom->createElement('managingEditor', 'markus@birth-online.de (Markus Birth)'));
-$channel->appendChild($dom->createElement('webMaster', 'markus@birth-online.de (Markus Birth)'));
-$channel->appendChild($dom->createElement('ttl', '60'));
-
-$skipDays = $dom->createElement('skipDays');
-$skipDays->appendChild($dom->createElement('day', 'Monday'));
-$skipDays->appendChild($dom->createElement('day', 'Tuesday'));
-$skipDays->appendChild($dom->createElement('day', 'Wednesday'));
-$skipDays->appendChild($dom->createElement('day', 'Thursday'));
-$skipDays->appendChild($dom->createElement('day', 'Friday'));
-$skipDays->appendChild($dom->createElement('day', 'Sunday'));
-$channel->appendChild($skipDays);
-
-$i_cat = $dom->createElement('itunes:category');
-$i_cat->appendChild(new DOMAttr('text', 'Music'));
-$channel->appendChild($i_cat);
-$channel->appendChild($dom->createElement('itunes:explicit', 'clean'));
-$channel->appendChild($dom->createElement('itunes:complete', 'no')); // "yes" = show has ended, no further episodes
-$channel->appendChild($dom->createElement('itunes:type', 'episodic')); // "serial" = to be consumed oldest to newest; "episodic" = can be consumed randomly
-
-//$s_limit = $dom->createElement('spotify:limit');
-//$s_limit->appendChild(new DOMAttr('recentCount', '3')); // At most this number of episodes appear in the client
-//$channel->appendChild($s_limit);
-
-//$channel->appendChild($dom->createElement('spotify:countryOfOrigin', 'us')); // set target market/territory by country, omit for "global"
-
-$len_cache = array();
-$cache_dirty = false;
-if (file_exists('genfeed.cache')) {
- $len_cache = json_decode(file_get_contents('genfeed.cache'), true);
-}
-
-function getSize($url)
-{
- global $len_cache, $cache_dirty;
- if (isset($len_cache[$url])) {
- return $len_cache[$url];
- }
-
- $context = stream_context_set_default(array(
- 'http' => array(
- 'method' => 'HEAD',
- ),
- ));
- $headers = get_headers($url, 1);
- $length = intval($headers['Content-Length']);
- $len_cache[$url] = $length;
- $cache_dirty = true;
- return $length;
-}
-
-function getDuration($size)
-{
- // 70445111 Bytes = 01:13:22.80 = 73:22.80 = 4402.80 seconds (128 kb/s)
- // 147530737 Bytes = 02:33:40.64 = 153:40.64 = 9220.64 seconds (128 kb/s)
- $seconds = $size / (128000 / 8);
-
- $hours = intdiv($seconds, 3600);
- $seconds = $seconds % 3600;
- $minutes = intdiv($seconds, 60);
- $seconds = $seconds % 60;
- return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
-}
-
-$latest_date = -1;
-foreach ($file_list as $url => $title) {
- $item = $dom->createElement('item');
- $guid = $dom->createElement('guid', $url);
- $guid->appendChild(new DOMAttr('isPermalink', 'true')); // guid is a static URL
- $item->appendChild($guid);
- $item->appendChild($dom->createElement('title', $title));
-
- $size = getSize($url);
-
- $enclosure = $dom->createElement('enclosure');
- $enclosure->appendChild(new DOMAttr('type', 'audio/mpeg'));
- $enclosure->appendChild(new DOMAttr('length', $size));
- $enclosure->appendChild(new DOMAttr('url', $url));
- $item->appendChild($enclosure);
-
- $item->appendChild($dom->createElement('itunes:duration', getDuration($size)));
-
- $datestr = substr($title, 1, 10);
- $pubDate = mktime(11, 00, 00, intval(substr($datestr, 5, 2)), intval(substr($datestr, 8, 2)), intval(substr($datestr, 0, 4)));
- $item->appendChild($dom->createElement('pubDate', date(RFC_822, $pubDate)));
-
- if ($pubDate > $latest_date) {
- $latest_date = $pubDate;
- }
-
- $channel->appendChild($item);
-}
-
-$channel->appendChild($dom->createElement('pubDate', date(RFC_822, $latest_date)));
-
-$root->appendChild($channel);
-
-$dom->save('podcast.rss2');
-
-if ($cache_dirty) {
- file_put_contents('genfeed.cache', json_encode($len_cache, JSON_PRETTY_PRINT));
-}
-
-header('Content-Type: application/rss+xml');
-echo file_get_contents('podcast.rss2');
diff --git a/global.ini b/global.ini
new file mode 100644
index 0000000..6671d43
--- /dev/null
+++ b/global.ini
@@ -0,0 +1,13 @@
+[config]
+; Public URL to this folder incl. trailing slash(!)
+base_url = "https://rpi4.mbirth.de/bassdrive-rss/"
+
+; Used for schedule times of shows
+time_zone = "America/New_York"
+
+; Format: mail@address.net (Full Name)
+managing_editor = "markus@birth-online.de (Markus Birth)"
+webmaster = "markus@birth-online.de (Markus Birth)"
+
+; How long before sites may refetch RSS (minutes)
+caching_ttl = 60
diff --git a/index.php b/index.php
new file mode 100644
index 0000000..752b987
--- /dev/null
+++ b/index.php
@@ -0,0 +1,74 @@
+
+ */
+
+require_once 'lib/BDAParser.php';
+require_once 'lib/RSSGenerator.php';
+
+use \BassDrive\BDAParser;
+use \BassDrive\RSSGenerator;
+
+$config = parse_ini_file('global.ini', true);
+if ($config === false) {
+ die('global.ini not found or invalid syntax. See https://www.php.net/manual/en/function.parse-ini-file .');
+}
+
+date_default_timezone_set($config['config']['time_zone']);
+$base_url = $config['config']['base_url'];
+
+if (!array_key_exists('show', $_GET)) {
+ header('Content-Type: text/html');
+ echo '
No show selected. Available shows (RSS feeds):
';
+ echo '';
+ $dirs = glob('shows/*', GLOB_ONLYDIR);
+ foreach ($dirs as $dir) {
+ $show = basename($dir);
+ $show_conf = parse_ini_file($dir . '/config.ini', true);
+ if ($show_conf === false) {
+ echo '- No config found or invalid: ' . $show . '
';
+ continue;
+ }
+ echo '- ';
+ echo '';
+ echo $show_conf['show']['title'] . ' (' . $show . ')';
+ echo '';
+ echo '
';
+ }
+ echo '
';
+ echo '';
+ exit();
+}
+
+// If we get here, a show was selected
+
+$show = $_GET['show'];
+$show_dir = realpath('shows/' . $show) . '/';
+
+$show_conf = parse_ini_file($show_dir . 'config.ini', true);
+if ($show_conf === false) {
+ die('Couldn\'t read (or parse) show\'s config.ini. See https://www.php.net/manual/en/function.parse-ini-file .');
+}
+
+// PARSE BASSDRIVE ARCHIVE
+$show_data = new BDAParser($show_dir, $show_conf['archive']['url'], $show_conf['archive']['mp3_bitrate']);
+$file_list = $show_data->fetchFiles();
+
+// GENERATE RSS XML
+$rss_gen = new RSSGenerator($base_url . '?show=' . $show, $base_url . 'shows/' . $show . '/');
+$rss_gen->setGlobalConfig($config);
+$rss_gen->setShowConfig($show_conf);
+$rss_gen->setShowIntro(file_get_contents($show_dir . 'intro.txt'));
+
+foreach ($file_list as $f) {
+ $rss_gen->addItem($f['url'], $f['title'], $f['size'], $f['duration'], $f['date']);
+}
+
+// OUTPUT RSS
+$rss = $rss_gen->getRss();
+file_put_contents('feed_' . $show . '.rss2', $rss);
+
+header('Content-Type: application/rss+xml');
+echo $rss;
diff --git a/intro.txt b/intro.txt
deleted file mode 100644
index c0edc73..0000000
--- a/intro.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-With a 19-year consecutive run on Bassdrive.com, Overfiend has earned his stripes in the
-Drum & Bass scene. In addition to his show, which has featured the likes of Ed Oberon,
-The Insiders, Ant TC1, Gerra and Stone, Actraiser, and more, he has also held multiple
-residencies in New York City and Phildelphia throughout his DJ career. 2020 will be the start of
-his 20th year on Bassdrive, and it promises to offer a healthy supply of the newest in
-Drum & Bass music as well as a helping of the finest special guests that the scene has to offer.
diff --git a/lib/BDAParser.php b/lib/BDAParser.php
new file mode 100644
index 0000000..7583492
--- /dev/null
+++ b/lib/BDAParser.php
@@ -0,0 +1,111 @@
+
+ */
+class BDAParser
+{
+ const RFC_822 = 'D, d M Y H:i:s T';
+
+ public function __construct($show_dir, $archive_url, $mp3_bitrate = 128)
+ {
+ $this->show_dir = $show_dir;
+ $this->archive_url = $archive_url;
+ $this->mp3_bitrate = $mp3_bitrate;
+ $this->cachefile = $show_dir . 'filesize.cache';
+ $this->initCache();
+ }
+
+ private function initCache()
+ {
+ $this->len_cache = array();
+ $this->cache_dirty = false;
+ if (file_exists($this->cachefile)) {
+ $this->len_cache = json_decode(file_get_contents($this->cachefile), true);
+ }
+ }
+
+ public function __destruct()
+ {
+ // Flush filesize cache if needed
+ if ($this->cache_dirty) {
+ file_put_contents($this->cachefile, json_encode($this->len_cache, JSON_PRETTY_PRINT), LOCK_EX);
+ }
+ }
+
+ public function fetchFiles()
+ {
+ $src_data = file_get_contents($this->archive_url);
+
+ $dom = new \DOMDocument();
+ $dom->loadHTML($src_data);
+
+ $anchors = $dom->getElementsByTagName('a');
+
+ $file_list = array();
+ foreach ($anchors as $anchor) {
+ $title = $anchor->nodeValue;
+ $href = $anchor->attributes->getNamedItem('href')->nodeValue;
+
+ if (substr($href, -4) !== '.mp3') {
+ continue;
+ }
+
+ $title = urldecode(substr($href, 0, -4)); // strip off ".mp3" file extension
+ $url = $this->archive_url . $href;
+ $size = $this->getSize($url);
+
+ array_push($file_list, array(
+ 'url' => $url,
+ 'title' => trim($title),
+ 'size' => $size,
+ 'duration' => $this->getDuration($size),
+ 'date' => $this->getDateFromTitle(trim($title)),
+ ));
+ }
+
+ return $file_list;
+ }
+
+ public function getSize($url)
+ {
+ if (isset($this->len_cache[$url])) {
+ return $this->len_cache[$url];
+ }
+
+ $context = stream_context_set_default(array(
+ 'http' => array(
+ 'method' => 'HEAD',
+ ),
+ ));
+ $headers = get_headers($url, 1);
+ $length = intval($headers['Content-Length']);
+ $this->len_cache[$url] = $length;
+ $this->cache_dirty = true;
+ return $length;
+ }
+
+ public function getDuration($size)
+ {
+ // 70445111 Bytes = 01:13:22.80 = 73:22.80 = 4402.80 seconds (128 kb/s)
+ // 147530737 Bytes = 02:33:40.64 = 153:40.64 = 9220.64 seconds (128 kb/s)
+ $bitrate = intval($this->mp3_bitrate) * 1000;
+ $seconds = $size / ($bitrate / 8);
+
+ $hours = intdiv($seconds, 3600);
+ $seconds = $seconds % 3600;
+ $minutes = intdiv($seconds, 60);
+ $seconds = $seconds % 60;
+ return sprintf('%02d:%02d:%02d', $hours, $minutes, $seconds);
+ }
+
+ public function getDateFromTitle($title)
+ {
+ $datestr = substr($title, 1, 10);
+ $iso_date = substr($datestr, 0, 4) . '-' . substr($datestr, 5, 2) . '-' . substr($datestr, 8, 2);
+ return $iso_date;
+ }
+}
diff --git a/lib/RSSGenerator.php b/lib/RSSGenerator.php
new file mode 100644
index 0000000..b131188
--- /dev/null
+++ b/lib/RSSGenerator.php
@@ -0,0 +1,176 @@
+
+ */
+class RSSGenerator
+{
+ const RFC_822 = 'D, d M Y H:i:s T';
+ const NAMESPACE_URIS = [
+ 'xmlns' => 'http://www.w3.org/2000/xmlns/',
+ 'atom' => 'http://www.w3.org/2005/Atom',
+ 'content' => 'http://purl.org/rss/1.0/modules/content/',
+ 'itunes' => 'http://www.itunes.com/DTDs/Podcast-1.0.dtd',
+ 'spotify' => 'http://www.spotify.com/ns/rss',
+ 'psc' => 'https://podlove.org/simple-chapters/', // for chapter marks
+ 'dcterms' => 'https://purl.org/dc/terms', // for validity periods
+ ];
+
+ public function __construct($feed_url, $show_url)
+ {
+ $this->self_url = $feed_url;
+ $this->show_url = $show_url;
+ $this->items = array();
+ }
+
+ public function setGlobalConfig($config_arr)
+ {
+ $this->global_conf = $config_arr;
+ }
+
+ public function setShowConfig($config_arr)
+ {
+ $this->show_conf = $config_arr;
+ }
+
+ public function setShowIntro($intro_text)
+ {
+ $this->show_intro = $intro_text;
+ }
+
+ public function addItem($url, $title, $size_bytes, $duration_secs, $pub_date)
+ {
+ array_push($this->items, array(
+ 'url' => $url,
+ 'title' => $title,
+ 'size' => $size_bytes,
+ 'duration' => $duration_secs,
+ 'date' => $pub_date,
+ ));
+ }
+
+ public function getRss()
+ {
+ $dom = new \DOMDocument('1.0', 'utf-8');
+ $dom->formatOutput = true;
+ $root = $dom->createElement('rss');
+ $root->appendChild(new \DOMAttr('version', '2.0'));
+ $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:atom', self::NAMESPACE_URIS['atom']);
+ $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:content', self::NAMESPACE_URIS['content']);
+ $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:itunes', self::NAMESPACE_URIS['itunes']);
+ $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:spotify', self::NAMESPACE_URIS['spotify']);
+ // $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:dcterms', self::NAMESPACE_URIS['dcterms']);
+ $root->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:psc', self::NAMESPACE_URIS['psc']);
+ $dom->appendChild($root);
+
+ $channel = $dom->createElement('channel');
+ $channel->appendChild($dom->createElement('title', $this->show_conf['show']['title']));
+ $channel->appendChild($dom->createElement('link', $this->show_conf['show']['link']));
+ $channel->appendChild($dom->createElement('copyright', $this->show_conf['show']['author']));
+ $channel->appendChild($dom->createElement('itunes:author', $this->show_conf['show']['author']));
+
+ $atom_link = $dom->createElement('atom:link');
+ $atom_link->appendChild(new \DOMAttr('href', $this->self_url));
+ $atom_link->appendChild(new \DOMAttr('rel', 'self'));
+ $atom_link->appendChild(new \DOMAttr('type', 'application/rss+xml'));
+ $channel->appendChild($atom_link);
+
+ $channel->appendChild($dom->createElement('description', trim(str_replace('&', '&', $this->show_intro))));
+ $channel->appendChild($dom->createElement('language', $this->show_conf['show']['language']));
+
+ $image = $dom->createElement('image');
+ $image->appendChild($dom->createElement('url', $this->show_url . $this->show_conf['show']['small_logo'])); // RSS spec says max. dimensions are 144x400
+ $image->appendChild($dom->createElement('title', $this->show_conf['show']['title']));
+ $image->appendChild($dom->createElement('link', $this->show_conf['show']['link']));
+ $image->appendChild($dom->createElement('width', $this->show_conf['show']['small_logo_width']));
+ $image->appendChild($dom->createElement('height', $this->show_conf['show']['small_logo_height']));
+ $channel->appendChild($image);
+ $i_image = $dom->createElement('itunes:image');
+ $i_image->appendChild(new \DOMAttr('href', $this->show_url . $this->show_conf['show']['big_logo'])); // Apple requires min.(!) size of 1400x1400, 1:1 aspect
+ $channel->appendChild($i_image);
+
+ $channel->appendChild($dom->createElement('lastBuildDate', date(self::RFC_822)));
+ $channel->appendChild($dom->createElement('docs', 'http://blogs.law.harvard.edu/tech/rss'));
+ $channel->appendChild($dom->createElement('generator', 'ew-rss 2020-02-15 by Markus Birth'));
+ $channel->appendChild($dom->createElement('managingEditor', $this->global_conf['config']['managing_editor']));
+ $channel->appendChild($dom->createElement('webMaster', $this->global_conf['config']['webmaster']));
+ $channel->appendChild($dom->createElement('ttl', $this->global_conf['config']['caching_ttl']));
+
+ $skipDays = $dom->createElement('skipDays');
+ $days_on = explode(',', $this->show_conf['show']['schedule_days']);
+ if (!in_array('Mo', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Monday'));
+ }
+ if (!in_array('Tu', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Tuesday'));
+ }
+ if (!in_array('We', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Wednesday'));
+ }
+ if (!in_array('Th', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Thursday'));
+ }
+ if (!in_array('Fr', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Friday'));
+ }
+ if (!in_array('Sa', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Saturday'));
+ }
+ if (!in_array('Su', $days_on)) {
+ $skipDays->appendChild($dom->createElement('day', 'Sunday'));
+ }
+ $channel->appendChild($skipDays);
+
+ $i_cat = $dom->createElement('itunes:category');
+ $i_cat->appendChild(new \DOMAttr('text', $this->show_conf['show']['category']));
+ $channel->appendChild($i_cat);
+ $channel->appendChild($dom->createElement('itunes:explicit', $this->show_conf['show']['explicit']));
+ $channel->appendChild($dom->createElement('itunes:complete', $this->show_conf['show']['complete']));
+ $channel->appendChild($dom->createElement('itunes:type', $this->show_conf['show']['type']));
+
+ //$s_limit = $dom->createElement('spotify:limit');
+ //$s_limit->appendChild(new DOMAttr('recentCount', '3')); // At most this number of episodes appear in the client
+ //$channel->appendChild($s_limit);
+
+ if (array_key_exists('country_of_origin', $this->show_conf['show'])) {
+ // set target market/territory by country, omit for "global"
+ $channel->appendChild($dom->createElement('spotify:countryOfOrigin', $this->show_conf['show']['country_of_origin']));
+ }
+
+ $latest_date = -1;
+ foreach ($this->items as $i) {
+ $item = $dom->createElement('item');
+ $guid = $dom->createElement('guid', $i['url']);
+ $guid->appendChild(new \DOMAttr('isPermalink', 'true')); // guid is a static URL
+ $item->appendChild($guid);
+ $item->appendChild($dom->createElement('title', $i['title']));
+
+ $enclosure = $dom->createElement('enclosure');
+ $enclosure->appendChild(new \DOMAttr('type', 'audio/mpeg'));
+ $enclosure->appendChild(new \DOMAttr('length', $i['size']));
+ $enclosure->appendChild(new \DOMAttr('url', $i['url']));
+ $item->appendChild($enclosure);
+
+ $item->appendChild($dom->createElement('itunes:duration', $i['duration']));
+ $dt = explode('-', $i['date']);
+ $tt = explode(':', $this->show_conf['show']['schedule_time']);
+ $pubDate = mktime($tt[0], $tt[1], $tt[2], $dt[1], $dt[2], $dt[0]);
+ $item->appendChild($dom->createElement('pubDate', date(self::RFC_822, $pubDate)));
+
+ if ($pubDate > $latest_date) {
+ $latest_date = $pubDate;
+ }
+
+ $channel->appendChild($item);
+ }
+
+ $channel->appendChild($dom->createElement('pubDate', date(self::RFC_822, $latest_date)));
+
+ $root->appendChild($channel);
+
+ return $dom->saveXML();
+ }
+}
diff --git a/biglogo.jpg b/shows/ew/biglogo.jpg
similarity index 100%
rename from biglogo.jpg
rename to shows/ew/biglogo.jpg
diff --git a/shows/ew/config.ini b/shows/ew/config.ini
new file mode 100644
index 0000000..8083fba
--- /dev/null
+++ b/shows/ew/config.ini
@@ -0,0 +1,36 @@
+[archive]
+url = "http://archives.bassdrivearchive.com/6%20-%20Saturday/Electronic%20Warfare%20-%20The%20Overfiend/"
+
+; MP3 bitrate for duration calculation (in 1000k, e.g. 96, 128, 192, 256)
+mp3_bitrate = 128
+
+[show]
+title = "Electronic Warfare"
+link = "https://www.facebook.com/louis.overfiend"
+author = "BassDrive.com / Louis Overfiend"
+language = "en-us"
+
+; Country code of origin for target market/region (comment out to target globally)
+;country_of_origin = us
+
+; Apple Podcasts Connect category, see https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12
+category = "Music"
+
+; Day(s) the show is on-air (Mo, Tu, We, Th, Fr, Sa, Su) - combine multiple days with comma, e.g. "Tu,Th,Sa"
+schedule_days = "Sa"
+schedule_time = "11:00:00"
+
+; Contains explicit stuff? "yes" or "clean" if not.
+explicit = "clean"
+
+; Show is complete (no new episodes)? "yes" or "no"
+complete = "no"
+
+; Files can be consumed randomly ("episodic") or should be consumed in order ("serial")
+type = "episodic"
+
+small_logo = "rsslogo.jpg"
+small_logo_width = 144
+small_logo_height = 144
+
+big_logo = "biglogo.jpg"
diff --git a/shows/ew/intro.txt b/shows/ew/intro.txt
new file mode 100644
index 0000000..90fd947
--- /dev/null
+++ b/shows/ew/intro.txt
@@ -0,0 +1,5 @@
+With a 19-year consecutive run on Bassdrive.com, Overfiend has earned his stripes in the Drum & Bass scene. In
+addition to his show, which has featured the likes of Ed Oberon, The Insiders, Ant TC1, Gerra and Stone, Actraiser,
+and more, he has also held multiple residencies in New York City and Phildelphia throughout his DJ career. 2020 will be
+the start of his 20th year on Bassdrive, and it promises to offer a healthy supply of the newest in Drum & Bass music
+as well as a helping of the finest special guests that the scene has to offer.
diff --git a/rsslogo.jpg b/shows/ew/rsslogo.jpg
similarity index 100%
rename from rsslogo.jpg
rename to shows/ew/rsslogo.jpg
diff --git a/shows/xposure/biglogo.jpg b/shows/xposure/biglogo.jpg
new file mode 100644
index 0000000..25fb7ac
Binary files /dev/null and b/shows/xposure/biglogo.jpg differ
diff --git a/shows/xposure/config.ini b/shows/xposure/config.ini
new file mode 100644
index 0000000..2450475
--- /dev/null
+++ b/shows/xposure/config.ini
@@ -0,0 +1,36 @@
+[archive]
+url = "http://archives.bassdrivearchive.com/2%20-%20Tuesday/XPOSURE%20Records%20Show%20-%20Ben%20XO/"
+
+; MP3 bitrate for duration calculation (in 1000k, e.g. 96, 128, 192, 256)
+mp3_bitrate = 128
+
+[show]
+title = "XPOSURE Records Show"
+link = "https://xo.am/"
+author = "BassDrive.com / Ben XO"
+language = "en-us"
+
+; Country code of origin for target market/region (comment out to target globally)
+;country_of_origin = us
+
+; Apple Podcasts Connect category, see https://help.apple.com/itc/podcasts_connect/#/itc9267a2f12
+category = "Music"
+
+; Day(s) the show is on-air (Mo, Tu, We, Th, Fr, Sa, Su) - combine multiple days with comma, e.g. "Tu,Th,Sa"
+schedule_days = "Tu"
+schedule_time = "15:00:00"
+
+; Contains explicit stuff? "yes" or "clean" if not.
+explicit = "clean"
+
+; Show is complete (no new episodes)? "yes" or "no"
+complete = "no"
+
+; Files can be consumed randomly ("episodic") or should be consumed in order ("serial")
+type = "episodic"
+
+small_logo = "rsslogo.jpg"
+small_logo_width = 144
+small_logo_height = 144
+
+big_logo = "biglogo.jpg"
diff --git a/shows/xposure/intro.txt b/shows/xposure/intro.txt
new file mode 100644
index 0000000..024938b
--- /dev/null
+++ b/shows/xposure/intro.txt
@@ -0,0 +1,15 @@
+Ben XO is a drum and bass and electro house DJ from London UK. He runs to this day the Internet's longest running drum
+and bass show, which airs on http://www.bassdrive.com/, starting in 2001. Since 2010 he has also had an Electro House
+radio show on DI.FM.
+
+Growing up on a diet of pirate radio in north London, UK, Ben XO began DJing in 1998 and is well known for his series
+of DJ demos that are regularly released onto the internet.
+
+Career highlights include a guest slot on Adam F’s Kiss FM show in 2005, a mix CD for the cover of Knowledge magazine
+in 2006, guest slots on national radio in Norway, Hungary and Estonia, and being interviewed on the subject of how the
+internet has changed drum’n'bass for Brian Belle-Fortune’s book “All Crews Muss Big Up”.
+
+He was the webmaster for both www.bassdrive.com and www.dogsonacid.com for over 3 years. He is now lead web developer
+at Last.fm.
+
+He also owns the record label Xposure Records.
diff --git a/shows/xposure/rsslogo.jpg b/shows/xposure/rsslogo.jpg
new file mode 100644
index 0000000..908f0af
Binary files /dev/null and b/shows/xposure/rsslogo.jpg differ