Windows Media Player playing the Channel9 video podcasts as a playlist.
Introduction
This article and its code provide a simple way to take a podcast feed and convert it to a playlist file/URL that Windows Media Player, Windows Media Center, Winamp or other players can play by applying XSLT on RSS XML.
Background
I use Windows Media Player (WMP) to watch/listen to several video/audio podcasts. The problem is that it’s not the best player for podcasts and doesn't really have any built-in support for them. I've tried applications such as Juice and a few add-ins for Windows Media Center (such as TV Tonic), but they all seemed to always download the podcast files to a local directory and usually needed to be running in the background to keep the directories in sync with the latest episodes.
I wanted something to where I could just click on a link or plug in a URL in WMP and have it automatically start streaming the latest podcast episodes without waiting for anything to download. I also didn't want to ever have to clean up any directories by removing older files or install any other applications or plug-ins. I wanted to be able to click the next/previous buttons on my keyboard (or remote) and be able to skip to the next or previous episodes in the podcast, just like a playlist works.
The key here is that I wanted a RSS podcast feed, to behave just like a playlist in WMP. The problem is that WMP doesn't understand RSS XML, but it does understand the ASX (Advanced Stream Redirector) playlist XML (among other playlist formats). What I decided to do is create a C# script to take the RSS XML and convert it to ASX XML (or other playlist formats) using an XSLT transformation file.
A Solution
The solution here is just a single .ASPX file (RssToPL.aspx) along with whatever playlist format XSLT files you want to support. To keep this example simple, I just placed all the C# code into the single .ASPX file (no code behind). As long as your XSLT files (RssToAsx.xslt, etc.) are in the same directory as the .ASPX file, you should be set.
If you are utilizing this for your own personal use (like me), this can either be placed on your own personal web server (running on your local machine or elsewhere). If you are a content provider producing podcasts, this is a easy way to provide your users several different ways to access your content.
RssToPL.aspx
<%@ Page Language="C#" %>
<%@ OutputCache Duration="240" VaryByParam="*" %>
<%@ Import Namespace="System.IO"%>
<%@ Import Namespace="System.Linq"%>
<%@ Import Namespace="System.Xml.Linq"%>
<script language="C#" runat="server">
void Page_Load(object sender, EventArgs e)
{
string sAsxFileName;
string sRssUrl;
string sXslFileType = "ASX";
string sXslFile = Path.GetDirectoryName(Request.PhysicalPath) +
Path.DirectorySeparatorChar;
if (Request.QueryString["OutType"] != null)
{
sXslFileType = Request.QueryString["OutType"];
}
sXslFile += String.Format("RssTo{0}.xslt", sXslFileType);
if (Request.QueryString["RssUrl"] != null)
{
sRssUrl = Request.QueryString["RssUrl"];
sAsxFileName = FormatFileName(GetRssTitle(sRssUrl)) + "." + sXslFileType;
}
else
{
throw new ApplicationException
("No RSS Feed URL (RssUrl) QueryString parameter provided.");
}
Response.Buffer = true;
Response.ClearContent();
Response.ClearHeaders();
Response.Clear();
Response.ContentEncoding = Encoding.UTF8;
if ((Request.QueryString["Inline"] != null) &&
(Request.QueryString["Inline"].ToLower() == "true"))
{
Response.ContentType = GetXslOutputMethod(sXslFile);
Response.AppendHeader("content-disposition", "inline;filename=" + sAsxFileName);
}
else
{
Response.ContentType = "text/x-unknown-content-type";
Response.AppendHeader("content-disposition",
"attachment;filename=" + sAsxFileName);
}
Response.Write(TransformXML(sRssUrl, sXslFile));
Response.End();
}
private string FormatFileName(string fileName)
{
string sReturn = fileName;
sReturn = sReturn.Replace(" ", "");
sReturn = sReturn.Replace(":", "");
sReturn = sReturn.Replace("\\", "");
sReturn = sReturn.Replace("/", "");
sReturn = sReturn.Replace("@", "");
sReturn = sReturn.Replace("$", "");
sReturn = sReturn.Replace("+", "");
sReturn = sReturn.Replace("%", "");
sReturn = sReturn.Replace(">", "");
sReturn = sReturn.Replace("<", "");
sReturn = sReturn.Replace("?", "");
sReturn = sReturn.Replace("\"", "");
sReturn = sReturn.Replace("'", "");
sReturn = sReturn.Replace(",", "");
sReturn = sReturn.Replace("`", "");
return sReturn;
}
private string TransformXML(string xmlUrl, string xslUrl)
{
MemoryStream ms = new MemoryStream();
string sOutput;
try
{
System.Xml.XPath.XPathDocument oXPath =
new System.Xml.XPath.XPathDocument(xmlUrl);
System.Xml.Xsl.XslTransform oXSLT = new System.Xml.Xsl.XslTransform();
oXSLT.Load(xslUrl);
oXSLT.Transform(oXPath, null, ms);
ms.Position = 0;
StreamReader sr = new StreamReader(ms, Encoding.UTF8);
sOutput = sr.ReadToEnd().ToString();
}
catch (Exception ex)
{
sOutput = ex.ToString();
}
return sOutput;
}
private string GetRssTitle(string xmlUrl)
{
string sReturn;
try
{
XDocument xmlDoc = XDocument.Load(xmlUrl);
var qry = from r in xmlDoc.Descendants("channel")
select (string)r.Element("title");
sReturn = qry.First();
}
catch (Exception)
{
sReturn = "rss";
}
return sReturn;
}
private string GetXslOutputMethod(string xslUrl)
{
string sReturn = "xml";
try
{
XNamespace xsl = "http://www.w3.org/1999/XSL/Transform";
XDocument xmlDoc = XDocument.Load(xslUrl);
var qry = from r in xmlDoc.Descendants(xsl + "stylesheet")
select r.Element(xsl + "output");
XElement xslOutput = qry.First();
if (xslOutput != null) { sReturn = xslOutput.Attribute("method").Value; }
}
catch (Exception ex)
{
}
sReturn = sReturn == "text" ? "text/plain" : "text/xml";
return sReturn;
}
</script>
RssToASX.xslt
="1.0" ="utf-8"
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
-->
<xsl:output method="xml" encoding="utf-8"
indent="yes" omit-xml-declaration="yes" />
<xsl:template match="rss/channel">
<asx version="3.0">
<title>
<xsl:value-of select="title"/>
</title>
<abstract>
<xsl:value-of select="description" />
</abstract>
<xsl:apply-templates select="item" />
</asx>
</xsl:template>
<xsl:template match="item">
<xsl:variable name="item_enclosure" select="enclosure/@url"/>
<xsl:if test="string-length(enclosure/@url) > 10">
<entry>
<title>
<xsl:value-of select="title" />
</title>
<author>
<xsl:value-of select="author" />
</author>
<abstract>
<xsl:value-of select="description" />
</abstract>
<ref href="{$item_enclosure}"/>
</entry>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
This XSLT file will only return items with an enclosure, so you don’t have to worry about RSS feeds that mix in blog entries with the podcast.
To use this ASP.NET script, you just need to pass in the URL of the RSS feed and it will return the ASX file (as either inline XML or as a download). For example: http://[yourhost]/RssToPL.aspx?RssUrl=http://channel9.msdn.com/Media/Videos/feed/wmvhigh/
would return a playlist with the MSDN Channel 9 videos (in reverse chronological order).
To take this a step further, I also wanted to be able to have multiple podcasts show up in the same playlist. For example, I wanted both the ".NET Rocks" and "Hansel minutes" podcasts to show up interwoven together in one playlist, sorted by date (not all the ".NET Rocks" first and then all "Hansel minutes" last).
To do this, I decided to use Yahoo Pipes rather than write my own code. Yahoo Pipes is an awesome tool that basically lets you create custom RSS feeds (among other things) based off multiple sources and be able to manipulate them in many different ways. Yahoo Pipes does a lot more than that, but I won't get into it here. Also, if Yahoo Pipes seems a bit complicated for you, you could also use something like RSS Mix.
Using the Code
On the web server, place the RssToPL.aspx and RssToAsx.xslt into the same directory on your ASP.NET web server. The following table lists the QueryString
parameters:
Param |
Value/Description |
RssUrl |
The URL of the podcast RSS feed. |
Inline (optional) |
True or false . If set to true , the XML will show up in the browser window, otherwise the browser will prompt to download the ASX file. |
OutType (optional) |
The output file type of the playlist to return (M3U, WPL, ZPL, WVX, SMIL, PLS, etc.) and define the XSLT file to use. If this parameter is left out, it will default to ASX. |
On the client machine (the computer you are using to watch/listen to the podcasts), there are two ways I have effectively used this. One is just by creating a shortcut on your desktop (or some other folder) to the script URL. This way, if you double click the shortcut, it will open Internet Explorer then just prompt you to open the ASX file in Windows Media Player (or your default ASX player), then Internet Explorer automatically closes. I've found this works best with WMP, because it lists all the episodes in the "Now Playing" pane.
The other solution is just to create a hard coded ASX file on your computer that just has a reference to the script URL. For example, place either of the following entries in your ASX file:
<entry><ref href="http://[yourhost]/RssToPL.aspx?RssUrl=[RSSURL]" /></entry>
or:
<entryref href="http://[yourhost]/RssToPL.aspx?RssUrl=[RSSURL]" />
I’ve found this solution works best if you are using Windows Media Center.
Notes
- I chose the ASX format as the default because I have worked with it before, but outputting to other formats, such as .M3U, .WPL, .ZPL (Zune), .WVX, .SMIL, .PLS, etc., for other players should be as easy as adding another XSLT template file and changing the
OutType QueryString
parameter. I've included ASX, M3U, WPL XSLT files in the example. Also, I believe ASX playlists will work with other players such as RealPlayer, Winamp, Media Player Classic, Windows Media Player Mobile, VLC, etc., but it isn't something I've extensively tested.
- For complex feed URLs (especially URLs with other
QueryString
parameters in them, you'll probably need to encode the URL (Server.URLEncode
) for this script to work properly.