Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

LyricsFetcher - The Easiest Way to Find Lyrics for your Songs

Rate me:
Please Sign up or sign in to vote.
4.93/5 (82 votes)
29 Oct 2009GPL325 min read 200.7K   2.4K   184   44
An article describing the development of a non-trivial C#/.NET application to fetch lyrics for songs.

Image 1

The easiest way to find lyrics for your songs

Foreword

Blessed are the cheese makers...

As Monty Python observed, not quite hearing what was said can have profound consequences. So, when I listen to songs, I like to know what the song is actually saying. But, I am also lazy – tracking down the lyrics for each new track/album I buy doesn't appeal to my inherent slothfulness. So was born LyricsFetcher, which takes the work out of tracking down lyrics for songs and updating them within your music library.

I know there are other mechanisms to do this (iLyrics or MiniLyrics, for example), but none of them worked quite how I wanted. Plus, it seemed like a fun task to tackle when I had nothing else to do.

The user guide for the application can be found here. This article is a programmer's guide for those who want to use/understand the code.

Programmer's introduction

LyricsFetcher presents a full, non-trivial application that demonstrates the following techniques:

  • Multi-threading. Several features of LyricsFetcher are long running operations. The application handles them in a multi-threaded fashion in order to stay responsive.
  • iTunes and Windows Media Player integration. iTunes and Windows Media Player are the two most common media management applications on Windows (yes, I know there are others). LyricsFetcher shows how to interact with these systems.
  • COM interactions. LyricsFetcher gives an example of how easy it is to interact with other applications through their COM interfaces.
  • Web resources. LyricsFetcher gives an example of using a SOAP service as well as accessing resources through simple HTTP.

Application overview

The functional description of LyricsFetcher is simple: it loads a list of tracks from a music library, and then tries to find lyrics for those songs. It can also try to find metadata (title and artist) for songs. From this simple description, it's clear that the project neatly divides into four functional areas:

  • Song management. This covers how we find the music library, how we load the information about the songs, and how we write changes back into the library.
  • Lyrics fetching. Once we have a list of songs, the area covers how we can find lyrics for them: where do we look, how do we fetch them.
  • Metadata fetching. For those songs where the title or artist are missing or suspect, how can we find the metadata for them?
  • User interface. How do we give the user access to the above functional areas?

Starting with the songs

Let's deal with the song management subsystem first.

Our first task is to decide what music library we are going to manage. It is a design decision that we will work with both iTunes and Windows Media Player, but how do we know which the user wants to use? LyricsFetcher chooses iTunes, by default, if it is installed; otherwise, it uses Windows Media Player (the user can easily change this within the application).

How do we know if iTunes is installed? The simplest way is to try to run it – if it isn't installed, this will throw an error.

C#
public bool HasITunes {
    get {
        if (this.iTunesApp == null) {
            try {
                this.iTunesApp = new iTunesAppClass();
            }
            catch (Exception) {
                // Couldn’t create iTunes app.
                // It’s probably not installed
            }
        }
        return (this.iTunesApp != null);
    }
}

This is fine, except for one thing: if iTunes is installed, it actually runs iTunes. This is often what is needed – but not always. If the user has both iTunes and WMP installed, but is using LyricsFetcher for WMP, they will not want iTunes to run every time they launch LyricsFetcher. So, we need another way of telling if iTunes is installed.

iTunes ships with a strange OCX file called ITDetector. A little poking around shows that this is exactly what we need: this will tell us if iTunes is installed without running that application. I added a reference to this OCX to the LyricsFetcher project, and now we can tell if iTunes is installed, like this:

C#
public bool HasITunes {
    get {
        iTunesDetectorClass detector = new iTunesDetectorClass();
        return detector.IsiTunesAvailable;
    }
}

However, this is a classic example of clever stupidity. It should have been immediately obvious to me (but was not) that this OCX is installed by iTunes, so if iTunes wasn't installed, the ITDetector OCX also wouldn't be around to tell me if iTunes was installed. Also, a couple of people reported that on 64-bit Windows, this OCX didn't work. So, I resorted to the low-tech solution of just looking for a key in the Registry -- very dull.

C#
public bool HasITunes {
    get {
        // Resort to low tech solution
        string regKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Apple Computer, Inc.\iTunes";
        string value = Registry.GetValue(regKey, "ProgramFolder", "") as string;
        return !String.IsNullOrEmpty(value);
    }
}

[Update: Oct. 2009] This is dull -- and wrong. On 64-bit versions of Windows, checking this Registry key doesn't work since Windows-on-Windows (the subsystem that allows 32-bit applications to run on a 64-bit Operating System) does weird things to the Registry. So, on 64-bit Windows, this test fails too.

COMmunicating with iTunes

Once we know what music library we will use, we then need a way to read all our songs from our music library. Both iTunes and WMP support COM, so this was my chance to play with COM in a .NET application. [In this discussion, I am only going to talk about iTunes, though using WMP is similar.]

Using COM within a .NET application is simplicity itself. Add a reference to the type library or OCX to your project, and the magic of .NET takes care of everything else. The COM objects can be used as full C# objects, all conversions are handled transparently, and even IntelliCode works flawlessly. This is a far cry from the early days of COM development.

So, to control iTunes through its COM interface, we simply add a reference to the iTunes type library to the LyricsFetcher project. To do this, right click on your project and choose "Add References...". On the dialog that eventually opens, select the "COM" tab, then scroll through that list looking for "iTunes X.X Type Library". Choose the most recent version, and then click "OK".

Image 2

Visual Studio (or SharpDevelop) will generate an interop assembly, and then all iTunes objects will be available within the application. You may want to download the iTunes SDK to have the full documentation available.

Now that we can handle iTunes, we need a model object to represent each song. It might look something like this:

C#
using iTunesLib; // iTunes COM classes live in this namespace
    
public class Song
{
    #region Constructors
 
    public Song() {
    }
 
    public Song(string title, string artist, string album, string genre) {
        this.Title = title;
        this.Artist = artist;
        this.Album = album;
        this.Genre = genre;
    }
 
    public Song(IITTrack track) :
        this(track.Name, track.Artist, track.Album, track.Genre) {
        IITFileOrCDTrack fileTrack = track as IITFileOrCDTrack;
        if (fileTrack != null) {
            try {
                this.Lyrics = fileTrack.Lyrics;
            }
            catch (COMException) {
                // If the file is corrupt, missing
                // or just plain obstinate, this can fail.
            }
        }
    }
 
    #endregion
        
    #region Public properties
    
    public string Album { get; set; }
    public string Artist { get; set; }
    public string Genre { get; set; }
    public string Lyrics { get; set; }
    public string Title { get; set; }
    
    #endregion
}

There is nothing particularly interesting about this: a couple of constructors and public properties. I personally don't mind public fields, but so many people have an unreasoning allergy to such things, that I used C#'s abbreviated property declaration rather than simple public fields.

The only interesting bit is actually getting the lyrics. The iTunes library is a collection of IITTracks, but IITTrack objects don't have a Lyrics property – only IITFileOrCDTrack objects have that. So, we have to downcast the track, and if that works, we try to get the lyrics. Even though it looks simple, there are lots of things going on underneath, and many of them can go wrong. So, we catch and ignore COM exceptions, since there is nothing else we can do about them.

Loading - Take I

Once we have our base model class, we can try to load our songs from iTunes. My first pass at loading songs from the iTunes library looked something like this:

C#
iTunesAppClass iTunesApp = new iTunesAppClass();
IITTrackCollection tracks = iTunesApp.LibraryPlaylist.Tracks;
List<Song> songs = new List<Song>();
for (int i=1; i <= tracks.Count; i++) {
    IITTrack track = tracks[i];
    if (track.Kind == iTunesLib.ITTrackKind.ITTrackKindFile)
        songs.Add(new Song(track));
}

This code shows one of the foibles of iTunes: all collections are 1-based, not 0-based. Also, the collections do not have enumerator interfaces, so you also cannot say:

C#
foreach (IITTrack track in iTunesApp.LibraryPlaylist.Tracks)

Apart from those gotchas, this code is clean, simple, and obvious – just the way code should be.

But, when I ran this code, the performance was abysmal! Loading the 3000 or so songs in my music library took 50 seconds. I am not only lazy, but I am impatient too. Fifty seconds is far too long to wait while my application loaded. So, I needed to speed up this loading: the big O word (no, not that one, I meant optimization).

I have two personal rules when it comes to optimization:

  • Don't do it! Always write the simplest, most obvious code, without a second thought for performance.
  • If you really have to do it, don't guess – use a profiler. The slowest part of your code will almost always be something you don't expect. The profiler will show you exactly what code is slow. JetBrain's dotTrace is excellent (commercial, but with a trial version available), as is EQATEC's Profiler (free!).

The culprit line is this.Lyrics = fileTrack.Lyrics;.

Why is this line so slow? The other information is stored in the library's index, but the lyrics are stored in the music file itself. To fetch the lyrics, iTunes has to open the related MP3 (or AAC) file, parse the structure, and extract the lyrics tag. Obviously, this is a lot more time consuming than just reading the Artist field.

Loading - Take II

The second pass involved caching the lyrics so that they are read from the media file only once. The first time the application runs, it will still have a long load time, but every run after that should be much faster. To make this happen, we needed:

  • A LyricsCache class (code not shown here).
  • A separate method that asks iTunes for the lyrics.
C#
public class Song
{
    public Song(IITTrack track) :
        this(track.Name, track.Artist, track.Album, track.Genre) {
        this.Track = track;
    }
 
    public void GetLyrics() {
        IITFileOrCDTrack fileTrack = this.Track as IITFileOrCDTrack;
        if (fileTrack != null) {
            try {
                this.Lyrics = fileTrack.Lyrics;
            }
            catch (COMException) {
                // If the file is corrupt, missing
                // or just plain obstinate, this can fail.
            }
        }
    }
}

The library loading code had to be changed to use the cache where possible:

C#
iTunesAppClass iTunesApp = new iTunesAppClass();
IITTrackCollection tracks = iTunesApp.LibraryPlaylist.Tracks;
List<Song> songs = new List<Song>();
LyricsCache cache = LyricsCache.LoadCache();
for (int i = 1; i <= tracks.Count; i++) {
    IITTrack track = tracks[i];
    if (track.Kind == iTunesLib.ITTrackKind.ITTrackKindFile) {
        Song song = new Song(track);
        if (cache.HasLyrics(song))
            song.Lyrics = cache.GetLyrics(song);
        else
            song.GetLyrics();
        songs.Add(song);
    }
}

This separates the expensive operation (getting the lyrics), and only uses it if the cache can't help.

After caching the lyrics, the load time of my 3000 songs fell from 50 seconds to 20 seconds. This was much better, but I still wasn't quite happy. Twenty seconds is still a long time for an impatient person to wait.

Loading - Take III

Profiling the twenty second load time didn't show any particular hot spot. If the songs were to be loaded faster, I needed a new approach.

iTunes has a property XMLLibrary, which is the path to the XML file that holds all the index information about its music library. For my library of 3000 songs, this file is about 5.5 megabytes. It might be possible to read the song information straight from the XML. But surely, parsing all that data couldn't be faster than simply asking iTunes for it (which already has all the information loaded)? To actually try this, I had to delve into .NET's handling of XML files.

After the happy simplicity of COM, .NET's handling of XML was a disappointment. I have been spoilt by Python's ElementTree, which is simple and elegant. Even PHP (which never aims for elegance) handles XML in a way that is at least obvious. But, .NET handles XML in a way that is neither elegant nor obvious.

After struggling with XMLTextReader, XMLReader, and XMLDocument, I eventually settled on XMLPathDocument and its associated XPathNavigator. This pair of classes allows you to deal with XML as a hierarchically structured document (unlike XMLReader and friends). After much trial and error, the code to read the library from the XML file looks something like this:

C#
iTunesAppClass iTunesApp = new iTunesAppClass();
int maxSongs = iTunesApp.LibraryPlaylist.Tracks.Count;
XPathDocument doc = new XPathDocument(iTunesApp.XmlPath);
XPathNavigator nav = doc.CreateNavigator();
 
// Move to plist, then master library and tracks
nav.MoveToChild("plist", "");
nav.MoveToChild("dict", "");
nav.MoveToChild("dict", "");
 
// Move to first track info
bool success = nav.MoveToChild("dict", "");
 
// Read each song until we have enough or no more
List<Song> songs = new List<Song>();
while (success && this.Songs.Count < maxSongs) {
    success = nav.MoveToFirstChild();
 
    // Read each piece of information about the song
    Dictionary<string, string> data = new Dictionary<string, string>();
    while (success) {
        string key = nav.Value;
        nav.MoveToNext();
        data[key] = nav.Value;
        success = nav.MoveToNext();
    }
 
    // Create and add the song if it's not one we want to ignore
    if (data.Count > 0) {
        Song song = new Song(data["Name"], data["Artist"],
            data["Album"], data["Genre"], data["Persistent ID"]);
        if (cache.HasLyrics(song))
            song.Lyrics = cache.GetLyrics(song);
        else
            song.GetLyrics();
        songs.Add(song);
    }
    nav.MoveToParent();
    success = nav.MoveToNext("dict", "");
}

If you compare this with our first pass, we have come a long way from simple and elegant. But, the trade off is speed. This code loads 3000 songs in about 1 second. I'm happy with that. Admittedly, the first time the application loads a library, it is still slow -- it still has to parse the lyrics from the media files. However, on subsequent runs, the loading time is almost negligible.

Showing some class

For those of you who like class diagrams, LyricsFetcher uses the following structure to manage songs:

Song management class structure

Showing what you've got

Now that we have our list of instantly loaded songs, we have to show them to the user. A ListView is the obvious choice -- but I hate ListViews. They are annoying, and boring to program. You can eventually wrestle them into doing what you want, but I'd rather use my energy elsewhere. So, for this project, I chose to use an ObjectListView from the ObjectListView CodeProject article. For those who have never used one of these, an ObjectListView is a ListView wrapper that makes the ListView much easier to use. You configure the ObjectListView within the IDE, and it can then transform a list of model objects into a fully functional list view.

So, in the IDE, we configure an ObjectListView to show the various bits that we want, and when the library has finished loading, we only need one line of code to show the songs to the user:

C#
this.olvSongs.SetObjects(library.Songs); 

ObjectListView takes care of everything else: data extraction, images, sorting, search by typing, all just happen. You can even right click on the header to choose which columns you want to see.

The devil is in the details

The ObjectListView handles the central part of the user interface. For the rest of the interface, we have to do some more work.

Image 4

The lower part of the user interface shows more information about the currently selected track. When the user changes the selected row, we need to know about it and then update the details:

C#
this.olvSongs.SelectionChanged += 
  new System.EventHandler(this.olvSongs_SelectionChanged);
...
  
private void olvSongs_SelectionChanged(object sender, EventArgs e) {
    this.UpdateDetails();
    this.EnableControls();
}
 
private void UpdateDetails() {
    Song song = this.olvSongs.SelectedObject as Song;
    if (song == null) {
        this.textBoxTitle.Text = "";
        this.textBoxArtist.Text = "";
        this.textBoxAlbum.Text = "";
        this.textBoxGenre.Text = "";
        this.textBoxLyrics.Text = "";
    } else {
        this.textBoxTitle.Text = song.Title;
        this.textBoxArtist.Text = song.Artist;
        this.textBoxAlbum.Text = song.Album;
        this.textBoxGenre.Text = song.Genre;
        this.textBoxLyrics.Text = song.Lyrics;
    }
}

This code uses the SelectionChanged event, which is an event provided by ObjectListView. For a normal ListView, SelectedIndexChanged would be the normal event to listen for, but it has one major drawback: the event is called every time a row is selected or deselected. What is the problem with that? If the user has selected 1000 rows and then selects a different row, you will receive 1001 SelectedIndexChanged events: one for each row that is deselected, plus one for the row that was selected. If you do anything even moderately complex when the selection changes, the application can appear to stall while your event handlers do those calculations 1000 times. In contrast, the SelectionChanged event will only be fired once, no matter how many rows were selected or deselected.

Showing details - Take II

This is OK for a first attempt, but it doesn't work so well when the user selects two or more songs. The details section is simply blanked out. It would be better if it followed the fairly standard UI practice of showing values common to all the selected objects and blanking out the others. Like this:

Image 5

To make this happen, we could write five methods, each of which calculates the common value of one field. Or, we could write one method and use some Reflection magic to get a named property. Or, we could use a utility class, Munger, from the ObjectListView project. A Munger encapsulates the work of getting (and setting) a named property from a model object. It's like using Reflection, but without the work.

C#
private void UpdateDetails() {
    IList songs = this.olvSongs.SelectedObjects;
    this.UpdateOneDetail(this.textBoxTitle, "Title", songs);
    this.UpdateOneDetail(this.textBoxArtist, "Artist", songs);
    this.UpdateOneDetail(this.textBoxAlbum, "Album", songs);
    this.UpdateOneDetail(this.textBoxGenre, "Genre", songs);
    this.UpdateOneDetail(this.textBoxLyrics, "Lyrics", songs);
}
 
private void UpdateOneDetail(TextBox textBox, string propertyName, IList songs) {
    if (songs.Count == 0 || songs.Count > 1000)
        textBox.Text = "";
    else {
        Munger munger = new Munger(propertyName);
        string value = (string)munger.GetValue(songs[0]);
        for (int i = 1; i < songs.Count; i++) {
            if (value != (string)munger.GetValue(songs[i])) {
                value = "";
                break;
            }
        }
        textBox.Text = value;
    }
}

The intelligence is in the UpdateOneDetail() method. If there are no selected songs, or too many, we just blank out the field. Otherwise, we make a Munger for the desired named property, and then get the value of that named property from each song. If the song has the same value as all the others, we keep going; otherwise, we set the field to blank.

Finding the lyrics

OK. We've loaded our songs from iTunes. We've shown them to the user. Now, how can we find the lyrics for them (since that was the whole point of the exercise)?

Just scrapping by - Take I

There are many websites that let you find lyrics for your songs: ELyrics, MetroLyrics, and Lyrics007 are all very popular. One approach to the finding lyrics problem would be to figure out a web page that has the lyrics for a given song, download that web page, and pick the lyrics out of the HTML. The time-honored technique is called “scraping” because your program tries to scrape the required information out of the soup of HTML code.

The first version of LyricsFetcher did just that: generated a likely URL, downloaded that page, and scraped the HTML. Each site was different, of course, but after quite a bit of tweaking, it worked quite well. But, after a couple of weeks, it stopped working so well. Every time a site changed its layout, my scraping technique no longer worked, and I'd have to tweak the code again. After doing this a few times, I decided there had to be a better way.

SOAP and other clean approaches - Take II

A few lyrics sites offer a programmatic API, which don't rely on scraping at all. LyricsWiki offers a SOAP interface (but see below), and LyrDb, LyricsPlugin, and LyricsFly all have HTTP based interfaces. With these defined interfaces, LyricsFetcher can reliably find lyrics, without changes to the website layout ruining the process.

In using these interfaces, I got to play with .NETs Net handling classes. Like COM interaction, .NET makes handling Net resources very simple.

[Update: Oct. 2009] Under the threat of legal action, LyricsWiki was forced to remove its SOAP interface. So the service that is discussed below no longer exists. However, the mechanics are the same for any SOAP interface, so I will leave it here.

Using LyricsWiki's web interface was the easiest of all. The key to using a SOAP service is the web service definition (WSDL) file. Once you know where the WSDL file lives, you use “Add Web Reference” on your project and give the WSDL's URI as the reference. In our case, LyricsWiki's WSDL file is found here: http://lyricwiki.org/server.php?wsdl. Plug that value into the URL field, and Visual Studio will read the resource and show the services available.

Image 6

Once you add the reference, Visual Studio generates a cluster of files, the net effect of which is that all of the Web Services are now available as simple method calls.

C#
using LyricsFetcher.org.lyricwiki;
// The namespace created to hold the generated class 
 
public string GetLyrics(Song song) { 
    LyricWiki lyricWiki = new LyricWiki(); 
    LyricsResult lyricsResult = lyricWiki.getSong(song.Artist, song.Title); 
    return lyricsResult.lyrics; 
}

All of my clever and complicate scraping was replaced by these three lines! That was a good day – I threw away about 500 lines of code (with apologies to Ken Thompson).

Using LyrDb's HTTP-based API was not quite that easy, but it was still much better than the scraping approach. LyrDB uses a two-step API: the first step translates a title/artist combination into a list matching song IDs, the second step gets the lyrics for a particular song ID.

C#
public string GetLyrics(Song song) {
    string queryUrl = String.Format(
        "http://webservices.lyrdb.com/lookup.php?q={0}|{1}&for=match", 
        song.Artist, song.title);
    WebClient client = new WebClient();
    string result = client.DownloadString(queryUrl);
    if (result == String.Empty)
        return String.Empty;
 
    foreach (string x in result.Split('\n')) {
        string id = x.Split('\\')[0];
        Uri lyricsUrl = new Uri("http://webservices.lyrdb.com/getlyr.php?q=" + id);
        string lyrics = client.DownloadString(lyricsUrl);
        if (lyrics != String.Empty)
            return lyrics;
    }
    return String.Empty;
}

In this code, we use .NET's Façade class, WebClient, to access LyrDb's lookup service. Under the covers, WebClient decides what exact class to use based on the protocol of the URI. Here, we simply want the HTTP protocol. The DownloadString() method makes reading a Net resource very simple.

LyrDb's response to the lookup request will be several lines, each of which look like this: {songId}\{title}\{artist}. We want the song ID, which is the first bit of each line. Once we have the song ID, we use the second service to get the lyrics for the song. Not quite as simple as LyricWiki's SOAP service, but again, a massive improvement over HTML scraping.

Showing even more class

The class structure for fetching lyrics looks like this:

Image 7

Threading

Underlying both the loading of the song library and the fetching of the lyrics is threading. Both of these operations can take some time to perform, and we don't want the UI frozen while they are happening.

Of all the technologies mentioned in the article, threading is the most difficult to get right. It is the most difficult to debug, and the most frustrating to try and unit test. It gives rise to bugs that are impossible to reproduce – unless you are giving a demonstration to your biggest client, in which case, they will be easy to reproduce!

If you are new to threads, you have to read Sasha Barber's wonderful series of articles. Even if you are already an expert, you will probably still learn a few things.

LyricsFetcher's threading code is built around .NET's BackgroundWorker class. This combines a separate execution thread with the ability to signal progress and to be cancelled. In the LyricsFetcher project, I decided to aggregate rather than subclass BackgroundWorker (has-a rather than is-a), though it turned out that subclassing would have also worked fine. All the thread handling, cancelling, joining is collected into the BackgroundWorkerWithProgress class. Then, all long running tasks subclass this class and implement the DoWork() method.

So, our long running code to load the Windows Media Player Library looks (more or less) like this:

C#
public class WmpSongLoader : BackgroundWorkerWithProgress
{
    protected override object DoWork(DoWorkEventArgs e) {
        IWMPPlaylist tracks = Wmp.Instance.AllTracks;
 
        // How many tracks are there and how many songs should we fetch?
        int trackCount = tracks.count;
        int maxSongs = trackCount;
        if (this.MaxSongsToFetch > 0)
            maxSongs = Math.Min(trackCount, this.MaxSongsToFetch);
 
        this.ReportProgress(0, "Gettings songs...");
        for (int i = 0; i < trackCount && this.Songs.Count < 
                 maxSongs && this.CanContinueRunning; i++) {
            IWMPMedia track = tracks.get_Item(i);
            this.AddSong(new WmpSong(track));
            this.ReportProgress((i * 100) / maxSongs);
        }
        return true;
    }
}

As you can see, this code doesn't have to worry about (almost) anything to do with threading – it can focus on just what it needs to do. It just has to periodically check to see if the thread has been cancelled, and call ReportProgress() to let the world know how close it is to finishing.

What was the name of that song?

One limitation of LyricsFetcher was that it relied on having the correct name and artist for the songs. In v0.5.1, LyricsFetcher could find lyrics based on the song name alone, but if the song name was wrong, it was stuck.

LyricsFetcher now has the ability to look up a song's title and artist based solely on the music of the song. If a song's title or artist are missing or suspect, LyricsFetcher's metadata lookup can now try to find the right information about that song. For example, in my son's song library, there was the song "Come get me" by Usher. It is a well known song, but LyricsFetcher could not find any lyrics for it. But when I added the metadata lookup and used it on that song, it found that the song's title was actually "Yeah". With that title, LyricsFetcher found the lyrics easily.

This technology is called acoustic fingerprinting. LyricsFetcher uses MusicIP's Open Fingerprint Architecture library to make a "fingerprint" of a song, and then uses their audio database to try and match this fingerprint to a known song.

Making it happen

The first take at implementing this used the Open Fingerprint Architecture library directly. This library seems to only work with files in WAV format, so I collected various code resources to convert other audio formats to WAV format. With the generated fingerprint, I called directly into the MusicDNS service. Although this scheme worked, it was complicated and slow -- two aspects I really dislike in my code.

The second take uses a command line program provided by MuscIP, genpuid. Hubris is another of my characteristics, but that doesn't stop me from using someone else's code, especially when their code is twice as fast as mine! In my first implementation, finding metadata for a song took about 20 seconds; genpuid does the same thing in about 10 seconds. This was good enough to throw out my first implementation and use their program.

C#
private string FetchMetaData(string fileName) {
    this.cmdLineProcess = new Process();
    cmdLineProcess.StartInfo.FileName = "genpuid";
    cmdLineProcess.StartInfo.RedirectStandardOutput = true;
    cmdLineProcess.StartInfo.UseShellExecute = false;
    cmdLineProcess.StartInfo.CreateNoWindow = true;

    string arguments = @"{0} -xml -rmd=2 ""{1}""";
    cmdLineProcess.StartInfo.Arguments = 
                   String.Format(arguments, CLIENT_ID, fileName);

    cmdLineProcess.Start();
    cmdLineProcess.PriorityClass = ProcessPriorityClass.BelowNormal;
    // have to read before waiting!
    string result = cmdLineProcess.StandardOutput.ReadToEnd();
    cmdLineProcess.WaitForExit();
    this.cmdLineProcess = null;

    return result;
}
private Process cmdLineProcess;

One thing to remember when using subprocesses is that they are independent of the starting process. If your program quits, its subprocesses will continue to run. For the LyricsFetcher application, this means that when the user quits the application, we have to specifically kill any genpuid process that is still in progress:

C#
public override void Cancel() {
    base.Cancel();
    if (this.cmdLineProcess != null && !this.cmdLineProcess.HasExited)
        this.cmdLineProcess.Kill();
}

WMP - Making life interesting

Psychologists say that people need a degree of stress and frustration in order to find life interesting. With this project, integrating with WMP made my life far too interesting. This extra interest was provoked when trying to store lyrics back into WMP. According to the WMP SDK, doing this should be easy:

C#
if (!this.Media.isReadOnlyItem("WM/Lyrics")) {
    this.Media.setItemInfo("WM/Lyrics", this.Lyrics);
}

Indeed, this seemed to work fine. But, when I looked at the track via WMP's advanced tag editor, the lyrics were there – but hundreds of times! Once for every registered language!

Lyrics for every registered language

This was a little more than I had been aiming for. Worse, when I ran the code on another computer that had an older version of WMP (version 9), the lyrics were not updated at all! The code was exactly what MS said it should be, and should have worked just fine – but it didn't! What now?

Google is normally the answer to such quandaries. Almost all problems have already been solved by someone else. But, not in this case. I found a couple of people who experienced the multiple languages for lyrics problem, but their queries were never answered. If they had found a solution, they were guarding their hard-won knowledge.

Editing the metadata

Surely, there must be a more direct way to update the information within a media file. After many false starts, this CodeProject article on a MetaDataReader pointed me in the right direction. Window Media subsystem has a IWMMetaDataEditor interface. This, combined with the IWMHeaderInfo3, can (eventually) be used to edit the tags stored within a media file.

Ultimately, fixing this problem required only three lines. I changed this line:

C#
this.Media.setItemInfo("WM/Lyrics", this.Lyrics);

to these lines:

C#
using (MetaDataEditor editor = new MetaDataEditor(this.Media.sourceURL)) {
    editor.SetFieldValue("WM/Lyrics", this.Lyrics);
}

But a lot of work went into finding the right three lines!

This new code works on WMP 9, 10, and 11. It may well be redundant in WMP 12, but who knows?

This trick of writing the lyrics directly into the media file works because WMP does not store the lyrics in its own database, but always reads them from the media file itself. This trick would not have worked with most other attributes (like Genre or Artist) since those attributes are only read from the media file when the track is first imported, and they are then stored in WMP's internal database. Changing those attributes in the media file would not change the information in WMP.

One limitation on this metadata editor is that it only works on files with a format that Windows Media understands. WMA and MP3 files are handled fine, but AAC, OGG, and FLAC files cannot be updated in this way. This is a limitation, but it is exactly the same as Windows Media Player itself, which has disabled the metadata editing of these formats. In a later version, I will probably reimplement the metadata editor to use the deservedly popular AudioGenie library.

Other interesting bits

Network detection

LyricsFetcher can't find any lyrics if the Internet isn't available. There is no point in even trying. Rather than trying and failing, it would be better to know if the internet was available, and tell the user before they even tried. The System.Net.NetworkInformation namespace holds the classes to do this.

NetworkInterface.GetIsNetworkAvailable() tells us if the computer is connected to a network. Not quite the same as connected to the Internet, but better than nothing. Secondly, the NetworkChange class lets us know when the availability of the network changes. Putting these pieces together, we have the following code:

C#
using System.Net.NetworkInformation; 
 
private void InitializeNetworkAvailability() { 
    // Listen for network availability events 
    NetworkChange.NetworkAvailabilityChanged += 
        new NetworkAvailabilityChangedEventHandler( 
        delegate(object sender, NetworkAvailabilityEventArgs e) { 
            this.BeginInvoke(new MethodInvoker(this.CheckNetworkStatus)); 
        }       
    ); 
    // Display the current state 
    this.CheckNetworkStatus(); 
} 
 
private void CheckNetworkStatus() { 
    // Calculating this value is expensive so we cache it 
    this.isNetworkAvailable = NetworkInterface.GetIsNetworkAvailable(); 
    // ... now update the UI to reflect the state of the network 
}

Then, within our code, we use isNetworkAvailable to decide what functions should be available.

DTD when the network is not available

Everything is always more complex than you imagine. I was very pleased with reading the iTunes XML file to speed up the library loading. It was fast and worked fine. But, the first time I tried to run LyricsFetcher without a network connection, the XML parser threw an exception.

The problem is that .NET's XML parser is a validating parser, so it tries to read the DTD for the iTunes XML library. When the network isn't available, this DTD cannot be read, and the XML parser falls in a heap. Try as I might, I could not find a way to turn off the validation or to get the parser to simply ignore the DTD. I suspect it could be done using an XMLResolver, but I didn't manage to get the correct combination of magic words and wand movements.

My hack solution was to load the whole XML file into memory, chop out the DTD, and then parse the resulting string. Ugly, but it works.

[Update: mikey_reppy showed me the right way to do this. Simply set XMLResolver to null! The code now loads the XML file directly, and doesn't have to worry about unavailable DTDs.]

To do

  • Write back lyrics into our sources.
  • Provide a UI for modifying the genres and kinds of files that should be ignored.
  • Provide monitor mode. In this mode, LyricsFetcher would sit in the background and load lyrics for songs as they are played.
  • Make the application localizable.
  • Add a section to this article talking about Unit Testing and why it is so helpful.

Conclusion

LyricsFetcher is a sample application that is genuinely useful, as well as shows reasonable techniques to solve common problems.

I hope others find it useful.

History

27 October 2009 - Version 0.6.1

  • Removed LyricWiki as a source of lyrics. Under threat of legal action, they were forced to remove their API.
  • Better handle cases when iTunes refuses to accept COM commands.
  • Improved cleaning up and formatting lyrics (no more black diamonds instead of single apostrophes).

10 April 2009 - Version 0.6

  • [Big change] Added ability to fetch metadata.
  • Un-broadened the search criteria somewhat. The broadening made in v0.5.1 sometimes made some very strange (and completely wrong) matches. The new scheme still allows name only matches, but the name has to be an exact match. This can still lead to false hits, but not nearly so often.
  • Lyrics are now decoded into Unicode so that accented characters are now retrieved correctly.
  • Lyrics cache is now updated in all cases -- rather than just most of them.
  • Now detects iTunes more reliably -- fingers crossed.

21 March 2009 - Version 0.5.1

  • Broadened the search criteria so that lyrics can be found by the name of the track alone. This does mean that some false hits will be found, that is, LyricsFetcher will sometimes find the wrong lyrics for a song.
  • Fixed bug where LyricsFetcher would crash on machines where iTunes had never been installed.
  • Added LyricsPlugin as another lyrics source.
  • Fixed bug where LyricsFetcher would crash when using WMP as the library and trying to update meta-data on formats that it can't update (like AAC).

14 March 2009 - Version 0.5

  • First public release.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)


Written By
Team Leader
Australia Australia
Phillip has been playing with computers since the Apple II was the hottest home computer available. He learned the fine art of C programming and Guru meditation on the Amiga.

C# and Python are his languages of choice. Smalltalk is his mentor for simplicity and beauty. C++ is to programming what drills are to visits to the dentist.

He worked for longer than he cares to remember as Lead Programmer and System Architect of the Objective document management system. (www.objective.com)

He has lived for 10 years in northern Mozambique, teaching in villages.

He has developed high volume trading software, low volume FX trading software, and is currently working for Atlassian on HipChat.

Comments and Discussions

 
GeneralRe: re: DTD when the network is not available Pin
mikey_reppy26-Mar-09 2:43
mikey_reppy26-Mar-09 2:43 
Generalnice program Pin
prattel18-Mar-09 22:52
prattel18-Mar-09 22:52 
GeneralRe: nice program Pin
Phillip Piper19-Mar-09 0:21
Phillip Piper19-Mar-09 0:21 
GeneralRe: nice program Pin
prattel20-Mar-09 9:43
prattel20-Mar-09 9:43 
GeneralRe: nice program Pin
Phillip Piper22-Mar-09 20:50
Phillip Piper22-Mar-09 20:50 
GeneralRe: nice program [modified] Pin
prattel23-Mar-09 0:46
prattel23-Mar-09 0:46 
GeneralRe: nice program Pin
Phillip Piper23-Mar-09 2:29
Phillip Piper23-Mar-09 2:29 
GeneralRe: nice program [modified] Pin
prattel23-Mar-09 10:16
prattel23-Mar-09 10:16 
[[Found by LyricsFetcher
Source: Lyrdb
Date: 2009-03-23 21:15:11]]

Thanks, it works Smile | :)

Just one minor localization thing for songs in my clown language:
& auml ; -> ä / & Auml ; -> Ä<br />
& ouml ; -> ö / & Ouml ; -> Ö<br />
& uuml ; -> ü / & Uuml ; -> Ü


Those &[vowel]uml; are html encodings for these characters. It's without spaces. I had to put spaces in between so the webbrowser doesn't automatically convert them to their respective character encoding.

It's just a very, very minor thing you might want to consider.

Thanks a lot for your effort Smile | :)

modified on Monday, March 23, 2009 4:24 PM

GeneralCool article Pin
SaintKith18-Mar-09 6:31
SaintKith18-Mar-09 6:31 
GeneralError on demo application Pin
aldo hexosa17-Mar-09 23:26
professionalaldo hexosa17-Mar-09 23:26 
GeneralRe: Error on demo application Pin
Phillip Piper18-Mar-09 3:55
Phillip Piper18-Mar-09 3:55 
GeneralRe: Error on demo application Pin
Phillip Piper22-Mar-09 20:53
Phillip Piper22-Mar-09 20:53 
GeneralNice article but... Pin
Rick Hansen17-Mar-09 6:22
Rick Hansen17-Mar-09 6:22 
GeneralRe: Nice article but... Pin
Phillip Piper18-Mar-09 4:26
Phillip Piper18-Mar-09 4:26 
GeneralRe: Nice article but... Pin
Rick Hansen18-Mar-09 5:11
Rick Hansen18-Mar-09 5:11 
GeneralRe: Nice article but... Pin
Phillip Piper19-Mar-09 0:24
Phillip Piper19-Mar-09 0:24 
GeneralRe: Nice article but... Pin
Rick Hansen19-Mar-09 5:03
Rick Hansen19-Mar-09 5:03 
GeneralRe: Nice article but... Pin
Phillip Piper22-Mar-09 20:52
Phillip Piper22-Mar-09 20:52 
GeneralNot to be picky... Pin
NetDave16-Mar-09 5:34
NetDave16-Mar-09 5:34 
GeneralRe: Not to be picky... Pin
Phillip Piper16-Mar-09 11:40
Phillip Piper16-Mar-09 11:40 
GeneralFirst Class Pin
hughjaynus14-Mar-09 11:04
hughjaynus14-Mar-09 11:04 
GeneralVery cool Pin
Nedim Sabic14-Mar-09 7:37
Nedim Sabic14-Mar-09 7:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.