Parsing Last.fm Web Services' artist.getSimilar with C# and LINQ to XML

The following covers how to parse the XML response of artist.getSimilar, from Last.fm's Web Services.

Setup and assumptions

The first step is sign up for a free API account at Last.fm.

You'll also need to target .NET Framework 3.5 when you setup your project, so as to access LINQ functionality.

When writing the steps listed below, I was working on a Windows Forms Application, but the steps should be the same, or very similar, for other project types.

Creating the base class

The first thing I've done is created a new class file in my project called Lastfm.cs, resulting in the following.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace JamesRSkemp.WebServices {
    class Lastfm {
        private const string LastFmApiKey = "EnterYourApiKeyHere";

    }
}

We'll add a new method to the Lastfm class to return the base Url we'll need to make Web service requests.

     /// <summary>
        /// Get the base Url that we'll use to make Web service requests.
        /// </summary>
        /// <returns>The base Url to use to make Web service requests.</returns>
        static private string GetBaseRequestUrl() {
            string baseUrl = "http://ws.audioscrobbler.com/2.0/?api_key=" + LastFmApiKey;
            return baseUrl;
        }

Next we'll create a method to make a request to a Web service and return the full response.

     /// <summary>
        /// Gets the data from an HTTP request.
        /// </summary>
        /// <param name="requestUrl">The full Url of the request to make.</param>
        /// <returns>Returns a string with the text returned from the request.</returns>
        private string GetServiceResponse(string requestUrl) {
            string httpResponse = "";

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUrl);
            request.Timeout = 15000;
            HttpWebResponse response = null;
            StreamReader reader = null;

            try {
                response = (HttpWebResponse)request.GetResponse();
                reader = new StreamReader(response.GetResponseStream());

                httpResponse = reader.ReadToEnd();
            } finally {
                if (reader != null) {
                    reader.Close();
                }
                if (response != null) {
                    response.Close();
                }
            }

            return httpResponse;
        }

The following references must also be added.

using System.Net;
using System.IO;

When we use this class, we want to have our data returned in an easy to use format. For ease, we'll have it return a DataTable. We'll have to add the appropriate reference first.

using System.Data;

We can then began our method as follows.

     public DataTable GetSimilarArtists(string artistName) {
            if (String.IsNullOrEmpty(artistName)) {
                throw new Exception("Artist name must be populated.");
            } else {
                string requestUrl = GetBaseRequestUrl();
                requestUrl += "&method=artist.getSimilar&artist=" + System.Web.HttpUtility.UrlEncode(artistName.Trim());

                string serviceResponse = GetServiceResponse(requestUrl);

                DataTable similarArtists = new DataTable();
                
                // TODO

                return similarArtists;
            }
        }

If we were to make a request now, we'd see that the data returned is formatted similar to the following, for a request for Bruce Springsteen. (For ease and sanity, data truncated.)

<?xml version="1.0" encoding="utf-8"?>
<lfm status="ok">
<similarartists artist="Bruce Springsteen">
<artist>
    <name>Bruce Springsteen & The E Street Band</name>
    <mbid>5a1283bf-81d5-4700-8919-683eeaaf2beb</mbid>
    <match>100</match>
    <url>www.last.fm/music/Bruce%2BSpringsteen%2B%2526%2BThe%2BE%2BStreet%2BBand</url>
    <image size="small">http://userserve-ak.last.fm/serve/34/8415485.jpg</image>
    <image size="medium">http://userserve-ak.last.fm/serve/64/8415485.jpg</image>
    <image size="large">http://userserve-ak.last.fm/serve/126/8415485.jpg</image>
    <image size="extralarge">http://userserve-ak.last.fm/serve/252/8415485.jpg</image>
    <image size="mega">http://userserve-ak.last.fm/serve/500/8415485/Bruce+Springsteen++The+E+Street+Band+estreet.jpg</image>
    <streamable>1</streamable>
</artist>
[...]
</similarartists></lfm>

We'd first need to add a reference so that we can parse through the returned response.

using System.Xml.Linq;

We can now create our LINQ to XML query to access the similar artist's name and match percentage.

             var xmlResponse = XElement.Parse(serviceResponse);

                // Parse through the returned Xml for the name and match value for each similar artist.
                var artists = from artistsSimilar in xmlResponse.Descendants("artist")
                              select new {
                                  name = artistsSimilar.Element("name").Value,
                                  match = artistsSimilar.Element("match").Value
                              };

Next we can create the DataTable that we'll use to store the name and math values.

             DataTable similarArtists = new DataTable();
                similarArtists.Columns.Add("Artist");
                similarArtists.Columns.Add("Match", System.Type.GetType("System.Double"));

Finally we can loop through each result returned from our LINQ to XML query, adding a new row to the table, for each.

             if (artists.Count() > 0) {
                    DataRow artistsRow;
                    foreach (var artist in artists) {
                        artistsRow = similarArtists.NewRow();
                        artistsRow["Artist"] = artist.name;
                        artistsRow["Match"] = artist.match;
                        similarArtists.Rows.Add(artistsRow);
                    }
                }

Finally we return the populated DataTable.

             return similarArtists;

Assuming a Windows Form Application with a text box (textBox1), a DataGridView (dataGridView1), and a button (button1), we could do the following (assuming the appropriate references have been added).

     private void button1_Click(object sender, EventArgs e) {
            if (!String.IsNullOrEmpty(textBox1.Text)) {
                Lastfm lastFmRequest = new Lastfm();

                DataTable results = lastFmRequest.GetSimilarArtists(textBox1.Text);

                dataGridView1.DataSource = results;
            } else {
                MessageBox.Show("You must enter an artist to continue.");
                textBox1.Focus();
            }
        }

Taken together, that results in the following. (Download JamesRSkemp.WebServices.Lastfm.cs.)

/*
Created by James Skemp - http://jamesrskemp.com/
Version 1.0
More information at http://strivinglife.com/words/post/Parsing-Lastfm-Web-Services-artistgetSimilar-with-C-and-LINQ-to-XML.aspx
Shared under a Creative Commons Attribution 3.0 United States License - http://creativecommons.org/licenses/by/3.0/us/
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Data;
using System.Xml.Linq;

namespace JamesRSkemp.WebServices {
    class Lastfm {
        /// <summary>
        /// Key used to access Last.fm Web services.
        /// </summary>
        private const string LastFmApiKey = "EnterYourApiKeyHere";

        /// <summary>
        /// Return artists similar to the one passed, with a match percentage.
        /// </summary>
        /// <param name="artistName">The name of the artist to use for the request.</param>
        /// <returns>DataTable with artist names and match percentage, as a Double.</returns>
        public DataTable GetSimilarArtists(string artistName) {
            if (String.IsNullOrEmpty(artistName)) {
                throw new Exception("Artist name must be populated.");
            } else {
                string requestUrl = GetBaseRequestUrl();
                requestUrl += "&method=artist.getSimilar&artist=" + System.Web.HttpUtility.UrlEncode(artistName.Trim());

                string serviceResponse = GetServiceResponse(requestUrl);

                var xmlResponse = XElement.Parse(serviceResponse);

                // Parse through the returned Xml for the name and match value for each similar artist.
                var artists = from artistsSimilar in xmlResponse.Descendants("artist")
                              select new {
                                  name = artistsSimilar.Element("name").Value,
                                  match = artistsSimilar.Element("match").Value
                              };

                DataTable similarArtists = new DataTable();
                similarArtists.Columns.Add("Artist");
                similarArtists.Columns.Add("Match", System.Type.GetType("System.Double"));

                if (artists.Count() > 0) {
                    DataRow artistsRow;
                    foreach (var artist in artists) {
                        artistsRow = similarArtists.NewRow();
                        artistsRow["Artist"] = artist.name;
                        artistsRow["Match"] = artist.match;
                        similarArtists.Rows.Add(artistsRow);
                    }
                }

                return similarArtists;
            }
        }

        /// <summary>
        /// Get the base Url that we'll use to make Web service requests.
        /// </summary>
        /// <returns>The base Url to use to make Web service requests.</returns>
        private string GetBaseRequestUrl() {
            string baseUrl = "http://ws.audioscrobbler.com/2.0/?api_key=" + LastFmApiKey;
            return baseUrl;
        }

        /// <summary>
        /// Gets the data from an HTTP request.
        /// </summary>
        /// <param name="requestUrl">The full Url of the request to make.</param>
        /// <returns>Returns a string with the text returned from the request.</returns>
        private string GetServiceResponse(string requestUrl) {
            string httpResponse = "";

            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUrl);
            request.Timeout = 15000;
            HttpWebResponse response = null;
            StreamReader reader = null;

            try {
                response = (HttpWebResponse)request.GetResponse();
                reader = new StreamReader(response.GetResponseStream());

                httpResponse = reader.ReadToEnd();
            } finally {
                if (reader != null) {
                    reader.Close();
                }
                if (response != null) {
                    response.Close();
                }
            }

            return httpResponse;
        }
    }
}

Advanced error handling is missing, but this should give you a basic idea of how you can go about easily accessing Last.fm's Web Services, and parsing returned data.

Final thoughts

Questions? Comments? Concerns? Please add a comment below.

Updated 9/12/2009, 15:55: I was doing it in my implementation, but added simple check for a populated textBox1 to the above code.