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

iTunes Style Music Browser using MSN Desktop Search API

0.00/5 (No votes)
29 Dec 2005 2  
Music browser app using the MSN Desktop Search query API.

Sample Image - MSNDesktopSearchQueryAPI.png

Introduction

Microsoft has included an indexing service in Windows from Windows 2000 and onwards. As part of the indexing service, they've included APIs to allow third parties to add indexing support for their own proprietary file formats by implementing IFilter components and APIs for third parties to perform queries against the index.

The MSN Desktop Search application is an evolutionary version of the standard Indexing Service that has been shipping as part of Windows. It also makes use of the same IFilter components to allow third parties to add indexing support for their file formats. However the initial release didn't provide support for an API for performing queries. The latest version to be released, version 2.05, now does include a query API.

To test out the query API, I decided to implement a music browser which allows you to browse for any music that has been indexed on your system. Instead of browsing your music collection using a tree control, I decided to use three lists for filtering based on genre, artist and album. In much the same way that Apple's iTunes does, or according to Contois Music Technology, the approach they invented for music browsing in the early 90s.

Query API Details

The query API is listed as being in beta and there isn't much documentation on it at all. In fact all there currently is, is an IDL file specifying the COM interface ISearchDesktop and a couple of comments.

interface ISearchDesktop : IUnknown
{
// perform a search given the SQL

HRESULT ExecuteSQLQuery([in] LPCWSTR lpcwstrSQL, 
                        [out, retval] _Recordset **ppiRs);

// perform a search given the query string

HRESULT ExecuteQuery([in] LPCWSTR lpcwstrQuery,               
                    [in] LPCWSTR lpcwstrColumn,              
                    [in] LPCWSTR lpcwstrSort,                
                    [in] LPCWSTR lpcwstrRestriction,         
                    [out, retval] _Recordset **ppiRs);       
}

// lpcwstrQuery - The query (like you'd type in to the query box)

// lpcwstrColumn - Columns to include (just comma-separated,

//                          it'll be put right into the SQL)

// lpcwstrRestriction - A "where" clause to be appended,

//                           e.g., "Contains(foo,'bar')"

// ppiRs - The resulting recordset

To get a feel for the types of queries you can enter in the MSN Desktop Search query window, take a look at the documentation on the advanced query syntax in the Help which is also provided on the internet.

Here is an example of an advanced query you can perform:

kind:music genre:rock artists:("Cowboy Junkies" OR Coldplay)

The MSN Desktop Search query results are returned as an ADO recordset with the columns defined by the column names that are passed into the ExecuteQuery() method or in the SQL statement that is passed to the ExecuteSQLQuery() method.

In order to make use of the ISearchDesktop interface, I ran the MIDL compiler on the supplied IDL file to generate a COM type library and then used TlbImp to create a .NET interop assembly in order to use the query API from a .NET application.

Subsequently after writing this initial article, Microsoft has released an updated SDK which includes pre-built interop assemblies for use by .NET applications. Check out the following blog entry from the MSN Desktop Search team for more details.

The following snippet of code shows how to execute a query using the .NET interop assembly:

SearchDesktopClass msnDS = new SearchDesktopClass();
_Recordset rs = msnDS.ExecuteQuery("kind:music genre:rock", 
          "MusicArtist, MusicAlbum, DocTitle", null, null);

If you inspect the properties of the recordset after executing the query, you'll notice that the Source property holds the equivalent SQL query.

SELECT MusicArtist, MusicAlbum, DocTitle FROM \"MyIndex\"..scope() 
WHERE ((Contains(PerceivedType,'music') AND CONTAINS(MusicGenre,'\"Rock\"',1033))

In order to populate the lists that we're planning on using for filtering, i.e. genre, artist, album, we'd ideally like to execute a query along the following lines:

SELECT DISTINCT MusicGenre FROM MyIndex

Unfortunately the MSN Desktop Search query engine doesn't support the DISTINCT SQL keyword. Which means that in order to get a list of all the different genres we have in our music library, we need to process a resultset which has a MusicGenre column but that has a row for every music track. So we need to post process the results to retrieve the distinct set of genres.

In order to databind our results to data aware WinForms controls like list boxes, data grids etc., we need to convert the ADO recordset into a .NET DataSet. The OleDbDataAdapter class provides the functionality needed to convert an OLE DB recordset into a .NET DataSet via it's Fill() method.

The following snippet of code shows how to convert the ADO recordset returned from the query into a .NET DataSet and then bind that to a data-aware control to display the results.

OleDbDataAdapter dbAdapter = new OleDbDataAdapter();
DataSet ds = new DataSet();
dbAdapter.Fill(ds, rs, "MyIndex");
list.DataSource = ds.Tables[0];

It turns out that you can set a primary key on a table in a DataSet and then have the OleDbDataAdapter class only insert unique entries based on the primary key into the table while filling the DataTable from the recordset.

ds.Tables[0].PrimaryKey = 
  new DataColumn[] { ds.Tables[0].Columns[primaryKeyColumn] };
dbAdapter.FillLoadOption = LoadOption.Upsert;

There are two main methods that perform the bulk of the implementation.

private string BuildQuery(string[] categories, DataGridView[] listFilters)
private void LoadCategory(string categorySearch, 
        string[] columns, int primaryKeyColumn, DataGridView list)

The BuildQuery() method takes an array of category strings and an array of DataGridView controls that display the categories to be queried on and have a list of the user's current selections in those categories.

For example, to build the necessary query to populate the list of albums, take a look at the implementation of the LoadAlbums() method.

private void LoadAlbums()
{
    string query = BuildQuery(new String[] { "genre", "artist" }, 
                   new DataGridView[] { listGenres, listArtists });
    LoadCategory(query, new String[] { "MusicAlbum" }, 0, listAlbums);
}

The BuildQuery() method in this case would return a query string similar to the following example:

kind:music genre:("rock") artists:("Cowboy Junkies" OR "Coldplay")

The LoadCategory() method then executes the query returned from BuildQuery() and populates a DataSet using the supplied column names and binds it to the supplied DataGridView control.

private void LoadCategory(string categorySearch, 
        string[] columns, int primaryKeyColumn, DataGridView list)
{
    StringBuilder columnList = new StringBuilder();
    for (int iColumns = 0; iColumns < columns.Length; iColumns++)
    {
        if (iColumns > 0)
            columnList.Append(",");
        columnList.Append(columns[iColumns]);
    }

    _Recordset rs = msnDS.ExecuteQuery(categorySearch, 
                     columnList.ToString(), null, null);
    OleDbDataAdapter dbAdapter = new OleDbDataAdapter();

    DataSet ds = new DataSet();
    DataTable table = new DataTable("MyIndex");

    foreach (string column in columns)
    {
        table.Columns.Add(column, typeof(string));
    }
    ds.Tables.Add(table);

    if (primaryKeyColumn != -1)
    {
        ds.Tables[0].PrimaryKey = 
           new DataColumn[] { ds.Tables[0].Columns[primaryKeyColumn] };
        dbAdapter.FillLoadOption = LoadOption.Upsert;
    }

    dbAdapter.Fill(ds, rs, "MyIndex");
    list.DataSource = ds.Tables[0];
}

The last bit of code that needs to be implemented then are event handlers for handling selection change events fired by the lists and which then need to refresh the category lists and the music track list.

private void listArtists_SelectionChanged(object sender, EventArgs e)
{
    HandleSelectionChange((DataGridView)sender, delegate { LoadAlbums(); });
}

private void HandleSelectionChange(DataGridView list, ExecuteMe loader)
{
    if (bLoaded && list.SelectedCells.Count > 0)
    {
        TimeMe(loader);
    }
}

private void TimeMe(ExecuteMe exec)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();
    exec.Invoke();
    sw.Stop();
    DisplayQueryTime(sw.ElapsedMilliseconds);
}

The total implementation including white space is only 170 lines of code. The current implementation is built using Visual Studio 2005 Beta 2 and therefore requires the .NET 2.0 Beta 2 runtime. However there is no specific dependency on the .NET 2.0 release and this application could be implemented using any language and environment that can use a COM component.

Performance

In order to get a feel for the query performance, I've included code to measure the time it takes to query the index when the user makes filtering selections. The query time is displayed in the status bar after each set of queries. On my system which contains a music library of 120 music albums and 1,700 individual tracks running on a 3GHz P4 PC, it takes just over a second to initially populate all the controls including the full list of all the music tracks.

Subsequent selections, for example changing the genre, take on the order of 100-300ms and changing the artist is on the order of 20-50ms.

To Do

Currently this application is just a test bed for testing the MSN Desktop Search query API and not a fully functioning music browser/player. If you want to turn it into a music player then one approach is to host the Windows Media Player control and use the embedded control to perform the music playback for selected songs, or you could generate a playlist based on the selected songs and launch Media Player as an external process to play the supplied playlist.

In addition, you'd want to polish the UI and include an 'All' option in each of the lists used for filtering. The other option I'm considering is implementing this as a Windows Media Center add-in with a 10 foot UI to provide an alternative way to browse and select music within Windows Media Center.

Conclusion

With the inclusion of a query API now which is fairly easy to use, you may find third party applications offering a search facility within their own UI based on the MSN Desktop Search index. In addition to presenting a search and results UI which mimics the existing MSN UI, the query API does allow you to create search and results UIs that are better tailored for the application's specific domain.

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