DasBlog RSS Feed Macro

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;

            #region Debug output
            if (debugMode)
            {
                output.Append("<strong>&lt;start debug&gt;</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

            #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");
                #region Debug output
                if (debugMode)
                {
                    output.AppendFormat("<i>cache file</i>: {0}\r\n", cachedFileLoc);
                }
                #endregion
                if (!File.Exists(cachedFileLoc))
                {
                    cacheExpired = true;
                    #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;
                    }
                    #region Debug output
                    if (debugMode)
                    {
                        output.AppendFormat("<i>cache age</i>: : {0} min old <br />\r\n", span1.TotalMinutes);
                    }
                    #endregion
                }
            }
            else
            {
                #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

            #region Debug output
            if (debugMode)
            {
                output.Append("<hr />");
            }
            #endregion
            //Check whether or not the cache has expired
            if (cacheExpired)
            {
                #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
            {
                #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();
            }
            #region Debug output
            if (debugMode)
            {
                output.Append("<hr /><strong>&lt;end debug&gt;</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;
                #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();
                    #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();
                #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, 0true);
            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();
        }

    }
}

XSL that I use for Last.FM

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" /> 
  <xsl:template match="/">
    <h2>Recent Tracks</h2>
    <ul>
    <xsl:for-each select="recenttracks/track">
        <li>
            <a href="{url}">
                <xsl:value-of select="artist" /> - <em><xsl:value-of disable-output-escaping="yes" select="name" /></em>
            </a>
        </li>
    </xsl:for-each>
    </ul>
    <p><a href="About-Last-FM.aspx" title="last.fm - The Social Music Revolution"><img alt="last.fm - The Social Music Revolution" src="images/lastfm_mini_black.gif" /></a></p>
  </xsl:template>
</xsl:stylesheet>

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)

Author

Tim

comments powered by Disqus