Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Retrieving CD Information from a remote Freedb database

0.00/5 (No votes)
29 Jul 2004 1  
Describes a library that can be used to retrieve Audio CD information from the CDDB compatible freedb database.

Sample Image - freedb.jpg

Introduction

This article presents a library that is used to retrieve CD audio information from a freedb database. The freedbd database is a CDDB standard database that is free to use and has a number of mirror sites around the world. See http://www.freedb.org/ for complete information on freedb.

Background

The freedb database can be accessed locally or remotely via CDDBP and HTTP. This library only deals with remote access via HTTP. CDDB currently has protocol levels from 1 through 6. This library supports protocol level 6. It may not work properly with servers that are not using a protocol level of 6. The freedb commands that this library currently supports are:

  • lscat - lists the genre categories, i.e., rock, pop, country.
  • sites - lists the freedb sites that can be used to retrieve freedb database information.
  • cddb query - queries a freedb database to determine if information exists for the specified CD.
  • cddb read - reads the CD information from the database.

Using the code

There are two class libraries included with the demo application.

  1. Freedb - classes for retrieving CD database information from freedb servers. This is the library that I created for this article.
  2. Ripper - this is a modified version of the C Sharp Ripper library presented on Code Project by Idael Cardoso. This library is used to access a CD drive and retrieve low level audio CD information. Based on code presented in the forum of that article, I modified the CDDrive class to include a couple of methods for calculating a disc ID and track offsets based on the freedb specifications. This information is used to query the freedb database as this article will show.

Demo Application

To build the demo application, extract the source zip file to a directory, open the solution FreedbDemo.sln, and do a build. Make sure FreedbDemo is set as your startup project and then run the application. Put an audio CD in your CD drive and click the "Load CD" button to load the top listview with the track information for the CD. Click any of the four buttons on the side to submit individual freedb commands whose results are placed in the bottom listbox.

The Freedb library classes that are used by the demo application are:

  • FreedbHelper - main class for interacting with the freedb servers.
  • QueryResult - created as a result of a "cddb query" command.
  • QueryResultCollection - a collection of QueryResult objects.
  • CDEntry - created as a result of a "cddb read" command.
  • Site - class created as a result of a "sites" command.
  • SiteCollection - a collection of Site objects.
  • Track - contains information for an individual track on a CD.
  • TrackCollection - a collection of Track objects.

I will describe how the various freedb commands are issued from within the demo application, and show relevant snippets of code.

In the main form of the application, I create a private member variable of type FreedbHelper. This instance is used for all accesses to the freedb remote database.

private FreedbHelper m_freedb = new FreedbHelper();

In the constructor of the form, the FreedbHelper object is initialized.

m_freedb.UserName = "test";
m_freedb.Hostname = "abc.company";
m_freedb.ClientName = "FreedbDemo";
m_freedb.Version = "1.0";

According to freedb documentation, the HostName and ClientName values should be set by the user of an application accessing freedb. The ClientName and Version should be provided by the application. In this demo, the HostName and ClientName variables can be changed using the Preferences Dialog.

sites command

The preferences dialog also contains a dropdown list box which is labeled "Select Default Server". The items used to populate this listbox are retrieved from freedb using the "sites" command. The code to populate the listbox is in the constructor of the form DLGPreferences.

private SiteCollection m_sites = null;

public DLGPreferences(FreedbHelper helper)
{
    ...
    m_freedb = helper;

    string result = m_freedb.GetSites(Freedb.Site.PROTOCOLS.HTTP,out m_sites);
    if (result == FreedbHelper.ResponseCodes.CODE_210)
        PopulateCombo();
    ...
}

The GetSites method will retrieve all the available sites from the freedb server and filter them based on the the Freedb.Site.PROTOCOLS parameter. The out parameter is a SiteCollection object which will contain a number of Site objects.

The site that is set as the default server becomes the address we use for subsequent calls to a freedb server. The only exception to this is the GetSites method itself which always uses the main freedb site.

General Note

All of the FreedbHelper methods that access the freedb database have a return parameter of a string that is the code received from the freedb server. Every freedb request can return a number of different codes. Refer to the freedb developer documentation for complete information on the commands and their specific return codes.

The data from each call is returned via out parameters. All calls to FreedbHelper methods should be wrapped in a try / catch block as they could throw an exception. The try / catch code has been snipped out of all code shown in this article.

There are a number of const strings in the FreedbHelper.ResponseCodes class that correspond to freedb return codes.

public class ResponseCodes
{
  public const string CODE_500 = "500"; // Invalid command, invalid parameters, etc.

  public const string CODE_200 = "200"; // Exact match 

  public const string CODE_202 = "202"; // No match 

    ... 
}

lscat command

In the demo, to execute the lscat, select the Categories button:

Here is the event handler code for the Categories button. The GetCategories method has no input parameters, an out parameter of a StringCollection and a return value of a string. If the method succeeds (Code 210), the categories are added to the main form's listbox LBResults.

private void BTNCategories_Click(object sender, System.EventArgs e)
{
    ...
    StringCollection coll; 
    string result = m_freedb.GetCategories(out coll);
    if (result == FreedbHelper.ResponseCodes.CODE_210)
        {
             LBResults.Items.Clear();
             foreach (string category in coll)
             {
                  this.LBResults.Items.Add(category);
             }
         }
    ...
}

cddb query

The cddb query command is used to determine if the freedb server has information for a specific CD. Here is the format of the command as defined in the freedb.org CDDB-protocol documentation:

cddb query discid ntrks off1 off2 ... nsecs

To obtain the disc ID, ntracks and other required information, we use the class CDDrive in the Ripper library. In the demo application, click the query button to perform a query.

In the screenshot above, the CD I performed the query against was No Doubt's Tragic Kingdom. Note that multiple results were returned. This either means that the database found more than one exact match for this particular CD or it found more than one inexact match.

Here is the complete Query button handler code:

CDDrive m_drive = null;

private void BTNQuery_Click(object sender, System.EventArgs e)
{
    // check if the CDDrive object has been created yet. 

    if (m_drive == null)
        LoadCD();
    if (m_drive.IsOpened == false || m_drive.IsCDReady() == false)
    {
        MessageBox.Show("CD Drive is not ready.");
        return;
    }

    string query;

    // get the CDDBQuery information we need from the CDDrive class

    try
    {
        query = m_drive.GetCDDBQuery();
    }
    
    catch (Exception ex)
    {
        //give up

        MessageBox.Show("CD Drive is not ready" + 
            " or cannot read cd. Details: " + ex.Message);
        return;
    }

    // Query out parameters

    // used if only one exact match is found

    QueryResult queryResult;
    // used if multiple exact or inexact matches are found

    QueryResultCollection coll;
    
    // call the freedb Query method.

    string code = m_freedb.Query(query,out queryResult,out coll);
    
    //clear out the listbox where we display the results.

    LBResults.Items.Clear();
    
    //CODE_200 means one and only one exact match was found

    //CODE_210 means a number of exact matches were found

    //CODE_211 means a number of inexact matches were found

    if (code == FreedbHelper.ResponseCodes.CODE_200)
        this.LBResults.Items.Add(queryResult);
    else if (code == FreedbHelper.ResponseCodes.CODE_210 
        || code == FreedbHelper.ResponseCodes.CODE_211 )
    {
        // add all the results to the listbox

        foreach (QueryResult qr in coll)
        {
            this.LBResults.Items.Add(qr);
        }
    }
    else MessageBox.Show("Query unsuccessful: " + code);
}

If the m_drive variable is null, LoadCD() is called to look for a CD drive, determine if there is a CD in the CD drive, and then read the table of contents from that CD.

As the source code comments indicate, the Query method can return a single QueryResult object or a collection of QueryResult objects. In the screenshot above, multiple results were returned. If a single QueryResult is returned then the QueryResultCollection out parameter will be null and vice versa. You can check the return code to determine which variable holds valid data.

The QueryResult object has an overloaded ToString() method which is what the WinForms ListBox control will use to display the QueryResult in the ListBox.

A QueryResult object is used as the input to the FreedbHelper.Read method, so if multiple matches come back, we have to ask the user to pick one of them.

cddb read

The read command will retrieve the complete database information for a specific CD. Here is the format of the command as defined in the freedb.org CDDB-protocol documentation:

cddb read categ discid

The categ is the category and the discid is the unique disk ID of the CD as returned from the query command or as calculated in the method GetCDDBDiskID() of the CDDrive object. These two values are also populated in the QueryResult object that is returned from a Query.

    public class QueryResult
    {
        private string m_ResponseCode;
        private string m_Category;
        private string m_Discid;
        private string m_Artist;
        private string m_Title;
        ...
    }

In the demo app, to do a read, first perform a query and then select the listbox row you wish to use as the input to the read, and click the "Get CD Entry" button.

If the read was successful, a CDEntry object is returned and the results are displayed using a simple MessageBox and the overloaded CDEntry ToString() method.

Here is the code for the "Get CD Entry" button handler:

private void BTNDetails_Click(object sender, System.EventArgs e)
{
    // make sure something is selected in the listbox

    if (LBResults.SelectedIndex == -1)
    {
        MessageBox.Show("No item selected");
        return;
    }
    // make sure that what is selected is a QueryResult object

    else if (!(LBResults.SelectedItem is QueryResult))
    {
        MessageBox.Show("Please perform a query and the select the item");
        return;
    }

    CDEntry cd;

    // perform the read passing in a QueryResult that

    // was returned from a previous Query request

    string code = m_freedb.Read((QueryResult)LBResults.SelectedItem, out cd);
    if (code != FreedbHelper.ResponseCodes.CODE_210)
        MessageBox.Show("Unable to retrieve cd entry. Code: " + code );

    MessageBox.Show("CD Entry Retrieved: " + cd.ToString());

}

Load CD Button

The "Load CD" button puts it all together. It does a query to determine if freedb has information for the CD in the drive. If multiple results are returned, it displays a dialog asking the user to select a specific result. It then does a read to obtain the results for the specific selection and populates the ListView control LVSongs. It uses many of the methods described above, so I will leave it up to the reader to go through the code.

FreedbHelper class

I wanted to go over a few of the methods in the FreedbHelper class to explain some details. I will show the code for the Query method, and then I will show the code for the Call method which is the method that sends and receives the actual request to the freedb server.

The four methods in FreedbHelper that wrap the freedb commands; GetCategories(), GetSites(), Query(), and Read() follow the same format:

  1. Build the command.
  2. Make the call.
  3. Retrieve the result code from the first string in the StringCollection that was returned.
  4. Perform a switch on the result code.
  5. If successful, create the appropriate result objects.
  6. Return the code and the results.

Here is the entire Query method:

public string Query(string querystring, 
  out QueryResult queryResult, out QueryResultCollection queryResultsColl)
{
    queryResult = null;
    queryResultsColl = null;
    StringCollection coll = null;

    // build the command that we are going to pass to the freedb server

    StringBuilder builder = 
        new StringBuilder(FreedbHelper.Commands.CMD_QUERY);
    builder.Append("+");
    builder.Append(querystring);
    
    //make call the actual call by calling our private method Call

    // the return value is of type StringCollection. 

    try
    {
        coll = Call(builder.ToString());
    }
    
    catch (Exception ex)
    {
        string msg = "Unable to perform cddb query.";
        Exception newex = new Exception(msg,ex);
        throw newex ;
    }
    
    // check if results came back

    if (coll.Count < 0)
    {
        string msg = "No results returned from cddb query.";
        Exception ex = new Exception(msg,null);
        throw ex;
    }

    // get the code that was returned in the first line

    string code = GetCode(coll[0]);
    if (code == ResponseCodes.CODE_INVALID)
    {
        string msg = "Unable to process results returned" + 
                     " for query: Data returned: " + coll[0];
        Exception ex = new Exception (msg,null);
        throw ex;
    }


    //do a switch on the code and process accordingly

    switch (code)
    {
        case ResponseCodes.CODE_500:
            return ResponseCodes.CODE_500;
    
        // Multiple results were returned

        // Put them into a queryResultCollection object

        case ResponseCodes.CODE_211:
        case ResponseCodes.CODE_210:
        {
            queryResultsColl = new QueryResultCollection();
            //remove the 210 or 211

            coll.RemoveAt(0);
            foreach (string line in coll)
            {
                QueryResult result = new QueryResult(line,true);
                queryResultsColl.Add(result);
            }
        
            return ResponseCodes.CODE_211;
        }
    
    
        // exact match 

        case ResponseCodes.CODE_200:
        {
            queryResult = new QueryResult(coll[0]);
            return ResponseCodes.CODE_200;
        }
    

        //not found

        case ResponseCodes.CODE_202:
            return ResponseCodes.CODE_202;

        //Database entry is corrupt

        case ResponseCodes.CODE_403:
            return ResponseCodes.CODE_403;

            //no handshake

        case ResponseCodes.CODE_409:
            return ResponseCodes.CODE_409;
            
        default:
            return ResponseCodes.CODE_500;
    
    } 
}

The format of the data returned from the freedb "cddb query" command is as follows:

  • Code 200 - single exact match:

    code category discid Artist / Title

    For example: 200 rock 7a0b600a Kansas / Masque

  • Code 210 - multiple exact matches - Multiple lines are returned. The first line is as follows:

    210 Found exact matches, list follows (until terminating `.')

    Each line after the first line has the following format:

    category discid Artist / Title

    For example, one of the lines returned for Supertramp's Crisis? What Crisis? is:

    rock 7d0b1c0a Supertramp / Crisis? What Crisis?

The QueryResult class has a constructor that takes a string as input. This allows the class itself to parse its own data. This pattern is used for the other data classes; Site and CDEntry.

Here is the code snippet from above where the QueryResult object is created and the one line of data returned from the freedb server is passed into the constructor:

// single match

case ResponseCodes.CODE_200:
{
    queryResult = new QueryResult(coll[0]);
    return ResponseCodes.CODE_200;
}

// multi-match

case ResponseCodes.CODE_211:
case ResponseCodes.CODE_210:
{
    queryResultsColl = new QueryResultCollection();
    //remove the 210 or 211

    coll.RemoveAt(0);
    foreach (string line in coll)
    {
        QueryResult result = new QueryResult(line,true);
        queryResultsColl.Add(result);
    }

    return ResponseCodes.CODE_211;
}

The constructor of QueryResult is as follows. Note that since the query data returned is slightly different for a code 200 and a code 210 or 211, the constructor takes a boolean to indicate if this input is from a multi-match or not.

public QueryResult(string queryResult, bool multiMatchInput) {
if
    (!Parse(queryResult, multiMatchInput))
    {
        throw new Exception("Unable to Parse" + 
                 " QueryResult. Input: " + queryResult);
    }

}

The call to the freedb server is a POST to a specific URL. Here is the code for the private Call method that makes the actual call to the freedb server:

/// Call the Freedb server using the specified command and the specified url

/// The command should not include the cmd=  and hello and proto parameters. 

/// They will be added automatically 

/// returns StringCollection 

private StringCollection Call(string commandIn, string url)
{
    StreamReader reader = null;
    HttpWebResponse response = null;
    StringCollection coll = new StringCollection();
    
    try
    {

        //create our HttpWebRequest which we use to call the freedb server

        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
        req.ContentType = "text/plain";
        // we are using th POST method of calling the http

        // server. We could have also used the GET method

        req.Method="POST";
        //add the hello and proto commands to the request

        string command = BuildCommand(Commands.CMD + commandIn);
        //using Unicode

        byte[] byteArray = Encoding.UTF8.GetBytes(command);
        //get our request stream

        Stream newStream= req.GetRequestStream();
        //write our command data to it

        newStream.Write(byteArray,0,byteArray.Length);
        newStream.Close();
        //Make the call. Note this is a synchronous call

        response = (HttpWebResponse) req.GetResponse();
        //put the results into a StreamReader

        reader = new StreamReader(response.GetResponseStream(), 
                                        System.Text.Encoding.UTF8);
        // add each line to the StringCollection

        // until we get the terminator

        string line;
        while ((line = reader.ReadLine()) != null) 
        {
            if (line.StartsWith(Commands.CMD_TERMINATOR))
                break;
            else
                coll.Add(line);
        }
    }
    
    catch (Exception ex)
    {
        throw ex;
    }

    finally
    {
        if (response != null)
            response.Close();
        if (reader != null)
            reader.Close();
    }
    
    return coll;
}

The comments in the code are fairly self explanatory except for the purpose of the BuildCommand.

string command = BuildCommand(Commands.CMD + commandIn);

Commands.CMD is defined as:

public const string CMD    = "cmd="; 

When using the HTTP protocol, the query command we pass to the freedb server will look as follows:

cmd=cddb+query+discd+ntrks+trackoffset1+trackoffset2 .. 
               +seconds&hello=test+abc.company+FreedbDemo+1.0&proto=6

Every command you pass in has to have the hello and proto commands passed along with it. FreedbHelper does this using the BuildCommand method and the Hello and Proto helper methods:

/// Given a specific command add on the hello

/// and proto which are requied for an http call

private string BuildCommand(string command)
{
    StringBuilder builder = new StringBuilder(command);
    builder.Append("&");
    builder.Append(Hello());
    builder.Append("&");
    builder.Append(Proto());
    return builder.ToString();
}

/// Build the hello part of the command 

public string Hello()
{
    StringBuilder builder = new StringBuilder(Commands.CMD_HELLO);
    builder.Append("=");
    builder.Append(m_UserName);
    builder.Append("+");
    builder.Append(this.m_Hostname);
    builder.Append("+");
    builder.Append(this.ClientName);
    builder.Append("+");
    builder.Append(this.m_Version);
    return builder.ToString();
}

/// Build the Proto part of the command

public string Proto()
{
    StringBuilder builder = new StringBuilder(Commands.CMD_PROTO);
    builder.Append("=");
    builder.Append(m_ProtocolLevel );
    return builder.ToString();
}

Note that the username and hostname that are configurable in the Preferences dialog are used when creating the Hello command.

Points of Interest

When making the call to the Freedb server, I had to choose between GET and POST. The reason I chose POST was because it was what I first tried, and it worked just fine so I stuck with it.

To make the web request to the freedb server, I could have used a number of .NET classes including HTTPWebRequest and WebClient. I chose HTTPWebRequest because it gave me more control than WebClient did.

I seriously considered making the FreedbHelper class consist of a number of static methods instead of having to instantiate an instance of it to use it. I still go back and forth on that decision. I rejected that idea mainly because it needed the ability to have member variables for CurrentSite and Username and Hostname. I could have made those static as well but I didn't like that design.

History

  • First Release Freedb Library - Version 1.0.0.0 - June 13,2004.
  • Version 1.0.0.1 - July 20, 2004 : Added the ability to set a different main site in FreedbHelper.cs. Updated the demo to demonstrate setting the main site from a configuration file.

Enhancements

I have the following enhancements I would like to implement when I get a chance:

  • Ability to submit CD information to freedb for new entries or updating existing entries.
  • Provide an asynchronous version of the FreedbHelper.Call() method.
  • Suggestions?

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here