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.
- Freedb - classes for retrieving CD database information from freedb servers. This is the library that I created for this article.
- 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";
public const string CODE_200 = "200";
public const string CODE_202 = "202";
...
}
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)
{
if (m_drive == null)
LoadCD();
if (m_drive.IsOpened == false || m_drive.IsCDReady() == false)
{
MessageBox.Show("CD Drive is not ready.");
return;
}
string query;
try
{
query = m_drive.GetCDDBQuery();
}
catch (Exception ex)
{
MessageBox.Show("CD Drive is not ready" +
" or cannot read cd. Details: " + ex.Message);
return;
}
QueryResult queryResult;
QueryResultCollection coll;
string code = m_freedb.Query(query,out queryResult,out coll);
LBResults.Items.Clear();
if (code == FreedbHelper.ResponseCodes.CODE_200)
this.LBResults.Items.Add(queryResult);
else if (code == FreedbHelper.ResponseCodes.CODE_210
|| code == FreedbHelper.ResponseCodes.CODE_211 )
{
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)
{
if (LBResults.SelectedIndex == -1)
{
MessageBox.Show("No item selected");
return;
}
else if (!(LBResults.SelectedItem is QueryResult))
{
MessageBox.Show("Please perform a query and the select the item");
return;
}
CDEntry cd;
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:
- Build the command.
- Make the call.
- Retrieve the result code from the first string in the
StringCollection
that was returned.
- Perform a switch on the result code.
- If successful, create the appropriate result objects.
- 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;
StringBuilder builder =
new StringBuilder(FreedbHelper.Commands.CMD_QUERY);
builder.Append("+");
builder.Append(querystring);
try
{
coll = Call(builder.ToString());
}
catch (Exception ex)
{
string msg = "Unable to perform cddb query.";
Exception newex = new Exception(msg,ex);
throw newex ;
}
if (coll.Count < 0)
{
string msg = "No results returned from cddb query.";
Exception ex = new Exception(msg,null);
throw ex;
}
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;
}
switch (code)
{
case ResponseCodes.CODE_500:
return ResponseCodes.CODE_500;
case ResponseCodes.CODE_211:
case ResponseCodes.CODE_210:
{
queryResultsColl = new QueryResultCollection();
coll.RemoveAt(0);
foreach (string line in coll)
{
QueryResult result = new QueryResult(line,true);
queryResultsColl.Add(result);
}
return ResponseCodes.CODE_211;
}
case ResponseCodes.CODE_200:
{
queryResult = new QueryResult(coll[0]);
return ResponseCodes.CODE_200;
}
case ResponseCodes.CODE_202:
return ResponseCodes.CODE_202;
case ResponseCodes.CODE_403:
return ResponseCodes.CODE_403;
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:
case ResponseCodes.CODE_200:
{
queryResult = new QueryResult(coll[0]);
return ResponseCodes.CODE_200;
}
case ResponseCodes.CODE_211:
case ResponseCodes.CODE_210:
{
queryResultsColl = new QueryResultCollection();
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:
private StringCollection Call(string commandIn, string url)
{
StreamReader reader = null;
HttpWebResponse response = null;
StringCollection coll = new StringCollection();
try
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
req.ContentType = "text/plain";
req.Method="POST";
string command = BuildCommand(Commands.CMD + commandIn);
byte[] byteArray = Encoding.UTF8.GetBytes(command);
Stream newStream= req.GetRequestStream();
newStream.Write(byteArray,0,byteArray.Length);
newStream.Close();
response = (HttpWebResponse) req.GetResponse();
reader = new StreamReader(response.GetResponseStream(),
System.Text.Encoding.UTF8);
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:
private string BuildCommand(string command)
{
StringBuilder builder = new StringBuilder(command);
builder.Append("&");
builder.Append(Hello());
builder.Append("&");
builder.Append(Proto());
return builder.ToString();
}
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();
}
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?