I have a few old podcast series that are not available online anymore. Every now and then, I enjoy listening to an old episode. I keep them in a hard drive connected to a Raspberry Pi, which serves them over the local network. Then I connect to this share on my mobile device and consume the content. It works fine most of the time. The problem is it’s hard to remember the last episode I listened to since everything is treated as files with no history. I thought I could leverage my podcast app on my phone if I served these files via an RSS feed. This tutorial will show how to generate the RSS feed using C# and serve the content over your local network. If this sounds like a problem you would like to solve, let’s get started.

Prerequisites

To follow this tutorial, you need the following software installed:

Set up the Web Server

Since you will host only static files (RSS feed which is an XML file and some audio files), an Nginx instance running in a Docker container is sufficient.

First, designate a local directory on your computer to put the files. In the tutorial, I will use the following path: ~/Temp/webroot. Modify this to match your environment.

Run the following command to start your podcast server:

docker run --name podcast-server -p 9876:80 -v ~/Temp/webroot:/usr/share/nginx/html:ro -d nginx

The command above;

  • Maps port 9876 on your machine to the internal port 80 in the container. (-p 9876:80)
  • Runs the container in the background as a daemon (-d)
  • Mounts the ~/Temp/webroot directory on your machine to the /usr/share/nginx/html directory on the container. This means when Nginx serves content in its HTML directory, it looks into the ~/Temp/webroot directory. This way, you can manage the content without going into the container’s file system.

If you open a browser tab and go to http://localhost:9876, you should get a 403 Forbidden response from the web server. This is expected because you haven’t put any files to serve yet.

Set up Content

To test the application, let’s start with a small amount of content. Go to file-examples.com and download 2 MP3 files and rename them as “episode1.mp3” and “episode2.mp3”. So the root of your web server should look like this:

Contents of the root directory showing webroot directory, content directory under it and two files named episode1.mp3 and episode2.mp3

Now, if you request one of these files in your browser (e.g. http://localhost:9876/content/episode1.mp3), you should be able to hear the MP3 playing. In the next section, you will implement the application that creates the RSS feed so that you can consume the feed via your podcatcher too.

Implement the RSS Generator

An RSS feed is simply an XML file. It includes the name and description of the show, as well as the titles and URLs of the individual episodes. If this feed were meant to be published publicly, you would add more details such as icons, categories, iTunes-specific tags etc., but for personal consumption, the following format is sufficient:

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel>
    <title>{Show Title}</title>
    <description>{Show Description}</description>
    <category>{Category}</category>
    <item>
      <title>{Episode Title}</title>
      <description>{Episode Description}</description>
      <enclosure url="{Episode URL}" type="audio/mpeg"/>
    </item>
  </channel>
</rss>

Create a new dotnet console application:

dotnet new console --name RssFeedGenerator --output .

Open the project with your IDE.

To serialize to the XML above, you will need a data structure. There are tools that can generate C# classes from a sample XML, so you don’t have to manually create the classes yourself. I generated the following at Xml2Charp. The result looks like this (after a bit of formatting):

/* 
 Licensed under the Apache License, Version 2.0
 
 http://www.apache.org/licenses/LICENSE-2.0
 */

using System.Xml.Serialization;

namespace RssFeedGenerator
{
    [XmlRoot(ElementName="enclosure")]
    public class Enclosure
    {
        [XmlAttribute(AttributeName="url")]
        public string Url { get; set; }
        [XmlAttribute(AttributeName="type")]
        public string Type { get; set; }
    }

    [XmlRoot(ElementName="item")]
    public class Item
    {
        [XmlElement(ElementName="title")]
        public string Title { get; set; }
        [XmlElement(ElementName="description")]
        public string Description { get; set; }
        [XmlElement(ElementName="enclosure")]
        public Enclosure Enclosure { get; set; }
    }

    [XmlRoot(ElementName="channel")]
    public class Channel
    {
        [XmlElement(ElementName="title")]
        public string Title { get; set; }
        [XmlElement(ElementName="description")]
        public string Description { get; set; }
        [XmlElement(ElementName="category")]
        public string Category { get; set; }
        [XmlElement(ElementName="item")]
        public List<Item> Item { get; set; }
    }

    [XmlRoot(ElementName="rss")]
    public class Rss
    {
        [XmlElement(ElementName="channel")]
        public Channel Channel { get; set; }
        [XmlAttribute(AttributeName="version")]
        public string Version { get; set; }
    }
}

Create a file called Rss.cs in your project and paste the above code. The biggest change I made to the auto-generated version is to replace the single Item property in the Channel class with a List<Item>, as you will need multiple entries per podcast.

Now, update Program.cs with the code below:

using System.Xml.Serialization;
using RssFeedGenerator;

string serverIPAddress = "192.168.1.20";
int serverPort = 9876;
var contentFullPath = "/Temp/webroot/content";
var feedFullPath = "/Temp/webroot/feed.rss";
var audioRootUrl = $"http://{serverIPAddress}:{serverPort}";

var rss = new Rss
{
    Version = "2.0",
    Channel = new Channel
    {
        Title = "[Local] Test Podcast",
        Description = "Testing generating RSS feed from local MP3 files",
        Category = "test",
        Item = new List<Item>()
    }
};

var allMp3s = new DirectoryInfo(contentFullPath)
    .GetFiles("*.mp3", SearchOption.AllDirectories)
    .OrderBy(x => x.Name);

foreach (var mp3 in allMp3s)
{
    rss.Channel.Item.Add(new Item
    {
        Description = mp3.Name,
        Title = mp3.Name,
        Enclosure = new Enclosure
        {
            Url = $"{audioRootUrl}/{mp3.Directory.Name}/{mp3.Name}",
            Type = "audio/mpeg"
        }
    });
}

var serializer = new XmlSerializer(typeof(Rss));
using (var writer = new StreamWriter(feedFullPath))
{
    serializer.Serialize(writer, rss);
}

Make sure to update the settings at the top of the file before you run the application.

Run the application by running the following command in the terminal:

dotnet run

Under your web server’s root directory, you should see the feed.rss file that looks like this:

<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="2.0">
  <channel>
    <title>[Local] Test Podcast</title>
    <description>Testing generating RSS feed from local MP3 files</description>
    <category>test</category>
    <item>
      <title>episode1.mp3</title>
      <description>episode1.mp3</description>
      <enclosure url="http://192.168.1.20:9876/content/episode1.mp3" type="audio/mpeg" />
    </item>
    <item>
      <title>episode2.mp3</title>
      <description>episode2.mp3</description>
      <enclosure url="http://192.168.1.20:9876/content/episode2.mp3" type="audio/mpeg" />
    </item>
  </channel>
</rss>

At this point, you have your RSS feed and all your content under your web server. The final step is to consume this content using your podcast app.

Add Your Podcast to your Podcast App

My podcast app of choice is Overcast. I’m very happy with it and have been using it for many years. The following might be a limitation of Overcast, but apparently, it cannot access feeds over the local network. So to tackle this issue, I used NGrok to tunnel web traffic to my local web server.

If you are having the same issue, install ngrok and run the following command:

ngrok http 9876

This should generate a public URL and route traffic to your local server. In my case, it looks like this:

ngrok output showing the traffic is routed to localhost:9876

Now you can access your feed via the {public URL}/feed.rss.

In Overcast, I add the URL by clicking the + button on the top right and then clicking Add URL link.

After it fetches and parses the RSS feed, it should appear in your podcast list.

The only thing that’s left is to open the podcast and play the episodes:

Even though Overcast cannot fetch the RSS feed over the local network, it can still play the episodes locally. You can stop ngrok and continue to play the episodes. The downside of this approach is if this podcast is an active one and you want to refresh the feed, you will need to delete and re-add the feed because the ngrok address will have changed the next time you try to get the updates.

Conclusion

I love hosting my own content in my own network. Even though having some offline podcasts stored locally is not a common use case, I had to implement my solution to solve the issue and decided to make it public and share it in case anyone else would like to use the same approach or build on it and make it better. The final source code can be found in my GitHub repository.

Categories: csharpdotnet

Volkan Paksoy

Volkan Paksoy is a software developer with more than 15 years of experience, focusing mostly on C# and AWS. He’s a home lab and self-hosting fan who loves to spend his personal time developing hobby projects with Raspberry Pi, Arduino, LEGO and everything in-between.