As part of my blog’s re-design I wanted to integrate my statistics from Last.FM which monitors what music you’re listening to and generates a stack of statistics about your listening habit (see About Last FM for more information).
Anyways, I started writing my own RSS macro when I came across one already developed by John Forsythe (http://www.jforsythe.com/) which did pretty much exactly what I was planning on developing, the only difference though was that his was hard-coded to preset node names whereas I was planning on using an XSL file to format mine to offer maximum flexibility in the long run so I updated his with the use of reflector (thanks to John Forsythe though!!).
There are a couple of difference to note with this code and John Forsythe's:
- The RSS retrieval is no longer handled by an external library -in this instance I wanted to keep this as simple and stand-alone as possible.
- There is no max item count at present -this is mainly because I didn't need it for the Last.FM Feed, I may alter that later.
Source code for a dasBlog XSL based RSS reader
using System;
using System.IO;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Text;
using System.Web;
using System.Web.UI;

using newtelligence.DasBlog.Runtime;
using newtelligence.DasBlog.Web.Core;

namespace TSDMacros
{...}

{
public class TheSiteDoctor
{...}

{
protected SharedBasePage requestPage;
protected Entry currentEntry;

public TheSiteDoctor(SharedBasePage page, Entry entry)
{...}

{

requestPage = page;

currentEntry = entry;

}

/// <summary>
/// A dasBlog macro to retrieve an RSS feed and apply XSL to
/// it before caching it for x minutes
/// </summary>
/// <param name="xslVPath">The virtual path of the XSL file</param>
/// <param name="rssPath">The RSS feed URL</param>
/// <param name="minutesToCache">Number of minutes to cache the file for</param>
/// <param name="debugMode">Output the debug information</param>
/// <returns>A control that can be inserted into a dasBlog template</returns>
public virtual Control GetRSS(
string xslVPath,
string rssPath,
int minutesToCache,
bool debugMode)
{...}

{
string cacheVDir =
"./content/getrsscache/";
string cachedFileLoc =
String.Empty;

StringBuilder output =
new StringBuilder();

bool writeToCache =
false;
bool cacheExpired =
false;
bool cacheExists =
false;
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<strong><start debug></strong><hr />\r\n");

output.AppendFormat(
"<i>RssPath</i>: {0}<br />\r\n", rssPath);

output.AppendFormat(
"<i>minutesToCache</i>: {0}<br />\r\n", minutesToCache);

output.AppendFormat(
"<i>CacheStorageFolder</i>: {0}<br />\r\n", cacheVDir);

output.Append(
"<hr />\r\n");

}
#endregion
Check whether we need to cache or not
#region Check whether we need to cache or not
if (minutesToCache >
0)
{...}

{

writeToCache =
true;
//Find the cache directory
string cacheDir =
HttpContext.Current.Server.MapPath(cacheVDir);
//Work out what the file would be called based on the RSS URL
cachedFileLoc = Path.Combine(cacheDir, HttpUtility.UrlEncode(TheSiteDoctor.GetMd5Sum(rssPath)) +
".cache");
Debug output
#region Debug output
if (debugMode)
{...}

{

output.AppendFormat(
"<i>cache file</i>: {0}\r\n", cachedFileLoc);

}
#endregion
if (!File.Exists(cachedFileLoc))
{...}

{

cacheExpired =
true;
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<i>cache age</i>: no file exists<br />");

}
#endregion
}
else
{...}

{

FileInfo info1 =
new FileInfo(cachedFileLoc);

TimeSpan span1 = (TimeSpan)(DateTime.Now - info1.LastWriteTime);
if (span1.TotalMinutes > minutesToCache)
{...}

{

cacheExists =
true;

cacheExpired =
true;

}
Debug output
#region Debug output
if (debugMode)
{...}

{

output.AppendFormat(
"<i>cache age</i>: : {0} min old <br />\r\n", span1.TotalMinutes);

}
#endregion
}

}
else
{...}

{
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<strong>caching disabled - CacheStorageAgeLimit=0</strong><br /><span style=\"color:red; font-weight: bold;\">FYI: All requests to this page will cause a new server request to the RssPath</span><br />");

}
#endregion
cacheExpired =
true;

}

#endregion
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<hr />");

}
#endregion
//Check whether or not the cache has expired
if (cacheExpired)
{...}

{
Debug output
#region Debug output
if (cacheExists & debugMode)
{...}

{

output.Append(
"<strong>file cache is expired, getting a new copy right now</strong><br />");

}
else if (debugMode)
{...}

{

output.Append(
"<strong>no cache, getting file</strong><br />");

}
#endregion
//The cache has expired so retrieve a new copy
output.Append(TheSiteDoctor.delegateRss(xslVPath, rssPath,
0, writeToCache, cachedFileLoc, debugMode));

}
else
{...}

{
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<strong>cool, we got the file from cache</strong><br />");

}
#endregion
//The cache still exists and is valid
StreamReader reader1 = File.OpenText(cachedFileLoc);

output.Append(reader1.ReadToEnd());

reader1.Close();

}
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<hr /><strong><end debug></strong>");

}
#endregion

output.Append(
"\r\n<!-- \r\ndasBlog RSS feed produced using the macro from Tim Gaunt\r\nhttp://blogs.thesitedoctor.co.uk/tim/\r\n-->");

return new LiteralControl(output.ToString());

}

/// <summary>
/// RSS feed retrieval worker method. Retrieves the RSS feed
/// and applies the specified XSL document to it before caching
/// a copy to the disk -this should be called after it has been
/// established the cache is out of date.
/// </summary>
/// <param name="xslVPath">The virtual path of the XSL file</param>
/// <param name="rssPath">The RSS feed URL</param>
/// <param name="timeoutSeconds">Number of seconds before the request should timeout</param>
/// <param name="writeCache">Whether to cache a copy on disk</param>
/// <param name="xmlPath">Physical path of the XML file on the disk</param>
/// <param name="debugMode">Output the debug information</param>
/// <returns>An XML document as a string</returns>
private static string delegateRss(
string xslVPath,
string rssPath,
int timeoutSeconds,
bool writeCache,
string xmlPath,
bool debugMode)
{...}

{

StringBuilder output =
new StringBuilder();
bool errorThrown =
false;
string cacheVDir =
"./content/getrsscache/";
string xslPath =
HttpContext.Current.Server.MapPath(xslVPath);

try
{...}

{
//TODO: Replace this with a HttpRequest and timeout to ensure the visitor is not left waiting for the file to load
//Load the XML
System.Xml.XmlDocument xmlDoc =
new System.Xml.XmlDocument();

xmlDoc.Load(rssPath);

//Load the XSL
System.Xml.Xsl.XslTransform xslDoc =
new System.Xml.Xsl.XslTransform();

xslDoc.Load(xslPath);

StringBuilder sb =
new StringBuilder();

StringWriter sw =
new StringWriter(sb);

//Apply the XSL to the XML document
xslDoc.Transform(xmlDoc,
null, sw);

//Append the resulting code to the output file
output.Append(sb.ToString());

}
catch (
Exception ex)
{...}

{

errorThrown =
true;
Debug output
#region Debug output
if (debugMode)
{...}

{
//Log the exception to the dasBlog exception handler
ErrorTrace.Trace(TraceLevel.Error, ex);

output.AppendFormat(
"<ul style=\"\"><li><strong>RSS request failed :(</strong> <br />{0}</li></ul>", ex.ToString());

}
#endregion
}

//Save a cache of the returned RSS feed if no errors occured
if (writeCache & !errorThrown)
{...}

{
//Find the cache's storage directory
DirectoryInfo dir =
new DirectoryInfo(
HttpContext.Current.Server.MapPath(cacheVDir));
//Check it exists
if (!dir.Exists)
{...}

{

dir.Create();
Debug output
#region Debug output
if (debugMode)
{...}

{

output.AppendFormat(
"<strong>just created the directory:</strong> {0}<br />",
HttpContext.Current.Server.MapPath(cacheVDir));

}
#endregion
}
//Create the file
StreamWriter writer1 = File.CreateText(xmlPath);

writer1.Write(output);

writer1.Flush();

writer1.Close();
Debug output
#region Debug output
if (debugMode)
{...}

{

output.Append(
"<strong>just wrote the new cache file</strong><br />");

}
#endregion
}

return output.ToString();

}

/// <summary>
/// Worker method to identify the MD5 checksum of a string
/// in this instance used to ensure the RSS file isn't already
/// cached (based on the URL supplied)
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public static string GetMd5Sum(
string str)
{...}

{

Encoder encoder1 = Encoding.Unicode.GetEncoder();
byte[] buffer1 =
new byte[str.Length *
2];

encoder1.GetBytes(str.ToCharArray(),
0, str.Length, buffer1,
0,
true);
byte[] buffer2 =
new MD5CryptoServiceProvider().ComputeHash(buffer1);

StringBuilder builder1 =
new StringBuilder();
for (
int minsToCache =
0; minsToCache < buffer2.Length; minsToCache++)
{...}

{

builder1.Append(buffer2[minsToCache].ToString(
"X2"));

}
return builder1.ToString();

}


}

}
To use it on the blog template

<% GetRSS("LastFM.xsl", "http://ws.audioscrobbler.com/1.0/user/timgaunt/recenttracks.xml", 25, false)|tsd %>
This is a pretty crude way of doing it IMHO because the XSL transforms the stream directly, eventually I’ll update the code so it includes a timeout (as John’s did) and having seen the performance implications on my blog, make sure the request is made asynchronously.
FWIW I have set my cache value to 25minutes, I did have it as 1min for fun but it killed the blog, why have I set it to 25mins? Well, most of my tracks I would think are 2-3minutes long, as I list 10 tracks at a time that’s 20-30minutes listening time so it’ll still keep a fairly accurate overview of my tracks without having massive performance issues on my blog :)
Incase you don't want to or know how to create this macro as a DLL I have created it for you :)
Download the complete dasBlog RSS feed macro (4KB - MD5 Hash: e3d7d6320109fd07259e8d246b754f13)
Liked this post? Got a suggestion? Leave a comment