|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThis 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. BackgroundThe 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:
Using the codeThere are two class libraries included with the demo application.
Demo ApplicationTo 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:
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 private FreedbHelper m_freedb = new FreedbHelper();
In the constructor of the form, the m_freedb.UserName = "test";
m_freedb.Hostname = "abc.company";
m_freedb.ClientName = "FreedbDemo";
m_freedb.Version = "1.0";
According to freedb documentation, the
sites commandThe 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 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 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 General NoteAll of the The data from each call is returned via out parameters. All calls to There are a number of 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 commandIn the demo, to execute the lscat, select the Categories button:
Here is the event handler code for the Categories button. The 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 queryThe 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
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 As the source code comments indicate, the The A cddb readThe 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 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
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 ButtonThe "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 FreedbHelper classI wanted to go over a few of the methods in the The four methods in
Here is the entire 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:
The Here is the code snippet from above where the // 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 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 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 string command = BuildCommand(Commands.CMD + commandIn);
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. /// 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 InterestWhen 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 I seriously considered making the History
EnhancementsI have the following enhancements I would like to implement when I get a chance:
| ||||||||||||||||||||