Brian Ritchie's Blog

My ramblings on .NET & other development topics

News



Twitter

Blog Roll

Connect with me

February 2011 - Posts

Populate a WCF syndication podcast using MP3 ID3 metadata tags

In the last post, I showed how to create a podcast using WCF syndication.  A podcast is an RSS feed containing a list of audio files to which users can subscribe.  The podcast not only contains links to the audio files, but also metadata about each episode.  A cool approach to building the feed is reading this metadata from the ID3 tags on the MP3 files used for the podcast.

One library to do this is TagLib-Sharp.  Here is some sample code:

   1:  var taggedFile = TagLib.File.Create(f);
   2:  var fileInfo = new FileInfo(f);
   3:  var item = new iTunesPodcastItem()
   4:  {
   5:      title = taggedFile.Tag.Title,
   6:      size = fileInfo.Length,
   7:      url = feed.baseUrl + fileInfo.Name,
   8:      duration = taggedFile.Properties.Duration,
   9:      mediaType = feed.mediaType,
  10:      summary = taggedFile.Tag.Comment,
  11:      subTitle = taggedFile.Tag.FirstAlbumArtist,
  12:      id = fileInfo.Name
  13:  };
  14:  if (!string.IsNullOrEmpty(taggedFile.Tag.Album))
  15:      item.publishedDate = DateTimeOffset.Parse(taggedFile.Tag.Album);

This reads the ID3 tags into an object for later use in creating the syndication feed.  When the MP3 is created, these tags are set...or they can be set after the fact using the Properties dialog in Windows Explorer.  The only "hack" is that there isn't an easily accessible tag for "subtitle" or "published date" so I used other tags in this example. Feel free to change this to meet your purposes.  You could remove the subtitle & use the file modified data for example.

That takes care of the episodes, for the feed level settings we'll load those from an XML file:

   1:  <?xml version="1.0" encoding="utf-8" ?>
   2:  <iTunesPodcastFeed
   3:    baseUrl =""
   4:    title=""
   5:    subTitle=""
   6:    description=""
   7:    copyright=""
   8:    category=""
   9:    ownerName=""
  10:    ownerEmail=""
  11:    mediaType="audio/mp3"
  12:    mediaFiles="*.mp3"
  13:    imageUrl=""
  14:    link=""
  15:    />

Here is the full code put together.

  • Read the feed XML file and deserialize it into an iTunesPodcastFeed class
  • Loop over the files in a directory reading the ID3 tags from the audio files
   1:  public static iTunesPodcastFeed CreateFeedFromFiles(string podcastDirectory, string podcastFeedFile)
   2:  {
   3:       XmlSerializer serializer = new XmlSerializer(typeof(iTunesPodcastFeed));
   4:       iTunesPodcastFeed feed;
   5:       using (var fs = File.OpenRead(Path.Combine(podcastDirectory, podcastFeedFile)))
   6:       {
   7:           feed = (iTunesPodcastFeed)serializer.Deserialize(fs);
   8:       }
   9:       foreach (var f in Directory.GetFiles(podcastDirectory, feed.mediaFiles))
  10:       {
  11:           try
  12:           {
  13:               var taggedFile = TagLib.File.Create(f);
  14:               var fileInfo = new FileInfo(f);
  15:               var item = new iTunesPodcastItem()
  16:                      {
  17:                          title = taggedFile.Tag.Title,
  18:                          size = fileInfo.Length,
  19:                          url = feed.baseUrl + fileInfo.Name,
  20:                          duration = taggedFile.Properties.Duration,
  21:                          mediaType = feed.mediaType,
  22:                          summary = taggedFile.Tag.Comment,
  23:                          subTitle = taggedFile.Tag.FirstAlbumArtist,
  24:                          id = fileInfo.Name
  25:                      };
  26:               if (!string.IsNullOrEmpty(taggedFile.Tag.Album))
  27:                    item.publishedDate = DateTimeOffset.Parse(taggedFile.Tag.Album);
  28:               feed.Items.Add(item);
  29:           }
  30:           catch
  31:           {
  32:            // ignore files that can't be accessed successfully
  33:           }
  34:      }
  35:      return feed;
  36:  }

Usually putting a "try...catch" like this is bad, but in this case I'm just skipping over files that are locked while they are being uploaded to the web site.

Here is the code from the last couple of posts.

 

Posted: Feb 28 2011, 10:28 PM by brian_ritchie | with 1 comment(s)
Filed under: , , , , ,
Creating a podcast feed for iTunes & BlackBerry users using WCF Syndication

 In my previous post, I showed how to create a RSS feed using WCF Syndication.  Next, I'll show how to add the additional tags needed to turn a RSS feed into an iTunes podcast. 

 A podcast is merely a RSS feed with some special characteristics:

  • iTunes RSS tags.  These are additional tags beyond the standard RSS spec.  Apple has a good page on the requirements.
  • Audio file enclosure.  This is a link to the audio file (such as mp3) hosted by your site.  Apple doesn't host the audio, they just read the meta-data from the RSS feed into their system.

The SyndicationFeed class supports both AttributeExtensions & ElementExtensions to add custom tags to the RSS feeds.

A couple of points of interest in the code below:

  • The imageUrl below provides the album cover for iTunes (170px × 170px)
  • Each SyndicationItem corresponds to an audio episode in your podcast

So, here's the code:

   1:  XNamespace itunesNS = "http://www.itunes.com/dtds/podcast-1.0.dtd";
   2:  string prefix = "itunes";
   3:   
   4:  var feed = new SyndicationFeed(title, description, new Uri(link));
   5:  feed.Categories.Add(new SyndicationCategory(category));
   6:  feed.AttributeExtensions.Add(new XmlQualifiedName(prefix, 
   7:     "http://www.w3.org/2000/xmlns/"), itunesNS.NamespaceName);
   8:  feed.Copyright = new TextSyndicationContent(copyright);
   9:  feed.Language = "en-us";
  10:  feed.Copyright = new TextSyndicationContent(DateTime.Now.Year + " " + ownerName);
  11:  feed.ImageUrl = new Uri(imageUrl);
  12:  feed.LastUpdatedTime = DateTime.Now;
  13:  feed.Authors.Add(new SyndicationPerson() {Name=ownerName, Email=ownerEmail });
  14:  var extensions = feed.ElementExtensions;
  15:  extensions.Add(new XElement(itunesNS + "subtitle", subTitle).CreateReader());
  16:  extensions.Add(new XElement(itunesNS + "image", 
  17:      new XAttribute("href", imageUrl)).CreateReader());
  18:  extensions.Add(new XElement(itunesNS + "author", ownerName).CreateReader());
  19:  extensions.Add(new XElement(itunesNS + "summary", description).CreateReader());
  20:  extensions.Add(new XElement(itunesNS + "category", 
  21:      new XAttribute("text", category),
  22:      new XElement(itunesNS + "category", 
  23:             new XAttribute("text", subCategory))).CreateReader());
  24:  extensions.Add(new XElement(itunesNS + "explicit", "no").CreateReader());
  25:  extensions.Add(new XDocument(
  26:          new XElement(itunesNS + "owner",
  27:          new XElement(itunesNS + "name", ownerName),
  28:          new XElement(itunesNS + "email", ownerEmail))).CreateReader());
  29:   
  30:  var feedItems = new List<SyndicationItem>();
  31:  foreach (var i in Items)
  32:  {
  33:      var item = new SyndicationItem(i.title, null, new Uri(link));
  34:      item.Summary = new TextSyndicationContent(i.summary);
  35:      item.Id = i.id;
  36:      if (i.publishedDate != null)
  37:          item.PublishDate = (DateTimeOffset)i.publishedDate;
  38:      item.Links.Add(new SyndicationLink() { 
  39:           Title = i.title, Uri = new Uri(link), 
  40:           Length = i.size, MediaType = i.mediaType });
  41:      var itemExt = item.ElementExtensions;
  42:      itemExt.Add(new XElement(itunesNS + "subtitle", i.subTitle).CreateReader());
  43:      itemExt.Add(new XElement(itunesNS + "summary", i.summary).CreateReader());
  44:      itemExt.Add(new XElement(itunesNS + "duration", 
  45:      string.Format("{0}:{1:00}:{2:00}", 
  46:          i.duration.Hours, i.duration.Minutes, i.duration.Seconds)
  47:         ).CreateReader());
  48:      itemExt.Add(new XElement(itunesNS + "keywords", i.keywords).CreateReader());
  49:      itemExt.Add(new XElement(itunesNS + "explicit", "no").CreateReader());
  50:      itemExt.Add(new XElement("enclosure", new XAttribute("url", i.url), 
  51:          new XAttribute("length", i.size), new XAttribute("type", i.mediaType)));
  52:      feedItems.Add(item);
  53:  }
  54:   
  55:  feed.Items = feedItems;

If you're hosting your podcast feed within a MVC project, you can use the code from my previous post to stream it.

Once you have created your feed, you can use the Feed Validator tool to make sure it is up to spec.  Or you can use iTunes:

  1. Launch iTunes.
  2. In the Advanced menu, select Subscribe to Podcast.
  3. Enter your feed URL in the text box and click OK.

After you've verified your feed is solid & good to go, you can submit it to iTunes. 

  1. Launch iTunes.
  2. In the left navigation column, click on iTunes Store to open the store.
  3. Once the store loads, click on Podcasts along the top navigation bar to go to the Podcasts page.
  4. In the right column of the Podcasts page, click on the Submit a Podcast link.
  5. Follow the instructions on the Submit a Podcast page.

Here are the full instructions.  Once they have approved your podcast, it will be available within iTunes.

RIM has also gotten into the podcasting business...which is great for BlackBerry users.  They accept the same enhanced-RSS feed that iTunes uses, so just create an account with them & submit the feed's URL.  It goes through a similar approval process to iTunes.  BlackBerry users must be on BlackBerry 6 OS or download the Podcast App from App World.

In my next post, I'll show how to build the podcast feed dynamically from the ID3 tags within the MP3 files.



Posted: Feb 27 2011, 10:32 PM by brian_ritchie | with 8 comment(s)
Filed under: , , ,
Serving up a RSS feed in MVC using WCF Syndication

With .NET 3.5, Microsoft added the SyndicationFeed class to WCF for generating ATOM 1.0 & RSS 2.0 feeds.  In .NET 3.5, it lives in System.ServiceModel.Web but was moved into System.ServiceModel in .NET 4.0.

Here's some sample code on constructing a feed:

   1:  SyndicationFeed feed = new SyndicationFeed(title, description, new Uri(link));
   2:  feed.Categories.Add(new SyndicationCategory(category));
   3:  feed.Copyright = new TextSyndicationContent(copyright);
   4:  feed.Language = "en-us";
   5:  feed.Copyright = new TextSyndicationContent(DateTime.Now.Year + " " + ownerName);
   6:  feed.ImageUrl = new Uri(imageUrl);
   7:  feed.LastUpdatedTime = DateTime.Now;
   8:  feed.Authors.Add(new SyndicationPerson() { Name = ownerName, Email = ownerEmail });
   9:   
  10:  var feedItems = new List<SyndicationItem>();
  11:  foreach (var item in Items)
  12:  {
  13:      var sItem = new SyndicationItem(item.title, null, new Uri(link));
  14:      sItem.Summary = new TextSyndicationContent(item.summary);
  15:      sItem.Id = item.id;
  16:      if (item.publishedDate != null)
  17:          sItem.PublishDate = (DateTimeOffset)item.publishedDate;
  18:      sItem.Links.Add(new SyndicationLink() { Title = item.title, Uri = new Uri(link), Length = item.size, MediaType = item.mediaType });
  19:      feedItems.Add(sItem);
  20:  }
  21:  feed.Items = feedItems;

 

Then, we create a custom ContentResult to serialize the feed & stream it to the client:

   1:  public class SyndicationFeedResult : ContentResult
   2:  {
   3:       public SyndicationFeedResult(SyndicationFeed feed)
   4:           : base()
   5:       {
   6:           using (var memstream = new MemoryStream())
   7:           using (var writer = new XmlTextWriter(memstream, System.Text.UTF8Encoding.UTF8))
   8:           {
   9:               feed.SaveAsRss20(writer);
  10:               writer.Flush();
  11:               memstream.Position = 0;
  12:               Content = new StreamReader(memstream).ReadToEnd();
  13:               ContentType = "application/rss+xml" ;
  14:           }
  15:       }
  16:  }


Finally, we wire it up through the controller:

   1:  public class RssController : Controller
   2:  {
   3:      public SyndicationFeedResult Feed()
   4:      {   
   5:          var feed = new SyndicationFeed();
   6:          //  populate feed...
   7:          return new SyndicationFeedResult(feed);
   8:      }
   9:  }

 

In the next post, I'll discuss how to add iTunes markup to the feed to publish it on iTunes as a Podcast.

 

Posted: Feb 21 2011, 10:22 PM by brian_ritchie | with 2 comment(s)
Filed under: , , ,
More Posts