Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

Silver JukeBox with an Alternative Client

4.91/5 (10 votes)
8 Nov 2009CPOL15 min read 60.3K   949  
An article on how to connect Silverlight 2 with web services in order to create a music library application with browse, search and download features.
Client.png

Contents

Credit

First of all I would like to credit UncleRedz for making this article a reality. He was the one who originally created the Silver JukeBox, and was kind enough to share the code with me. Basically all code on the server is written by him, the only thing I've done is to change to a Northwind database, add the possibility to download album art and somewhat modify the interfaces in order to make them fit my client better. Kudos to UncleRedz for sharing and being the original creator of this application.

Abstract

I would like to state three facts that are true about my persona:

  1. I can't sing or play any sort of instrument (not even the triangle), but I love music
  2. Music has an influence on my mood
  3. If I could, I would like to have access to music wherever I am

The first and second facts are hard to affect (sure, I could take courses in learning to play the triangle, but I am of the opinion that certain things should be done by professionals), but the third one would be really cool to fix. The music I love and listen to is stored at my home computer; would it be possible to access that music indifferent of where I am? The answer is of course 'yes' and this article will demonstrate how it's done.

Introduction

The application consists of two parts:

  • A server running on my home computer, built on WPF and WCF, responsible for keeping track of my music
  • A client developed in Silverlight 2, featuring browsing, searching and downloading music files

I feel a need to emphasize that this isn't another media player. The client isn't responsible for the actual 'playing' of the music; what it merely does is to allow 'access' to it. On my computer Windows Media Player (from now on WMP) is the default music player. When selecting a track or an album in the client, WMP pops up and starts playing, i.e. the client is assigned for accessing the music and WMP is responsible for playing it.

I would also like to point out a difference in behavior of Internet Explorer (from now on IE) and Firefox, since it affects the user experience of the application. When selecting a track or an album in IE, it's instantly passed over to WMP, letting it buffer and start playing even though the file hasn't been downloaded yet. Firefox however downloads the complete file before passing it on to WMP, which may take some time depending on internet connection.

Prerequisites

If you've got Visual Studio 2008 installed you're fine, otherwise you need to install the following components:

  • .NET 3.5 Service Pack 1
  • Microsoft SQL Server 2005

The Client

I do most of my user interface programming in WPF and was very eager to get my hands dirty with this new web supported framework called Silverlight. The differences between WPF and Silverlight are of course very well documented, but the first thing that struck me when starting to write Silverlight code was the idea of creating my own classes acting as DataContext for my UI controls, since in Silverlight it isn't possible to data bind to dependency properties using the ElementName methodology. At first, it gave me ghastly headaches whenever I had to create a new control, since I knew I had to create a DataContext class at the same time (and yes Visual Studio, I think you automatically should create a DataContext class whenever I am adding a new UI control to the project). Surprisingly along the development of the client I mended with the idea and actually started to like it. The fact that you are separating data and UI, not only by having them in XAML and code behind as you do in WPF, but in fact have them in entirely different classes is something I might embrace also for WPF; even though the need for it doesn't really exist unless you are using MVVM or similar patterns.

Background

In the following chapters, I will discuss the design development, a brief history on why the application became as it is now.

Version 0.1

The very first version asked the server for all albums right up front when the client launched, the idea was to store all data on the client and then use the concept of paging albums, thus have a very fast paging since all data already existed on the client. The idea was also to feature instant searching, i.e. when you start typing text in the search field the search results practically updates on the fly, which would have been really cool.

But unfortunately the design was lousy for so many reasons I don't know where to start. First of all, the loading time will depend solely on the size of the music library, and that's not a good design when you start thinking in the terms of scalability. It took my 4 year old computer about 4-5 minutes to completely load the data, and you should note that the server and client were running on the very same machine, this of course means that over a network it would have been even slower.

Version 1.0

If I wasn't able to have all data on the client, the instant search feature is impossible to implement since I wasn't at that point interested in implementing AJAX or similar techniques. Coming to terms with the fact that not all albums can exist on the client, I moved to the very far opposite end of the game field: what if the client was really stupid? What if all CPU and time consuming operations were to take place on the server? If you think about it, there's a very big chance that the server computer is more powerful than the client. What if the data displayed on the client was the only data available on the client? That would mean that not only paging would have to take place on the server, but also the searching. It was an intriguing idea that also sufficed the idea of scalability.

The consequence of this design is that data is requested from the server when:

  • The client is launching
  • A new page is displayed
  • Enter is pressed in the search field

These bullets results in the method GetAlbums(...) in interface IJukeBoxServer described in chapter Web service interfaces.

Debugging the Client

If you set debugPage.html as the start page, it will be possible to debug the client.

Silver JukeBox Client - Debugging the client

The Server

Let me first give you a quick tour of the server's user interface. The main window consists of two buttons. Library, if pressed, will open a dialog where you can configure the folders scanned for music, while Server lets you configure the server settings. There is also an activity log showing network requests made by the client, album art searches, etc.

Silver JukeBox Server - Image of the server

The library configuration is hardly worth mentioning, you simply add folders with music files to the list scanned by the server.

Silver JukeBox Server - Image of the library dialog on server

The server settings dialog on the contrary is more interesting. It exposes the following features:

  • The possibility to specify a port to be used by the server
  • Whether communication between server and client should be encrypted (further described in chapter Encrypted communication)
  • Whether access to the client should require NTLM authentication (further described in chapter Authentication )
Silver JukeBox Server - Image of the settings dialog on server

Web Service Interfaces

The server is exposing two interfaces: IFileServer and IJukeBoxServer. As the name states IFileServer is used when transporting files, e.g. the Silverlight client itself. IJukeBoxServer only contains one method responsible for transmitting albums between the server and the client. The method GetPolicy() solely exists since cross-domain calls should be allowed. More information about cross-domain can be found here.

C#
[ServiceContract]
public interface IFileServer
{
  /// <summary>
  /// Gets the Silverlight policy file.
  /// </summary>
  [OperationContract, WebGet(UriTemplate = "/crossdomain.xml")]
  Stream GetPolicy();

  /// <summary>
  /// Gets the HTML page holding the Silverlight client.
  /// </summary>
  [OperationContract, WebGet(UriTemplate = "/JukeBox.html")]
  Stream GetHtmlPage();

  /// <summary>
  /// Gets the Silverlight client.
  /// </summary>
  [OperationContract, WebGet(UriTemplate = "/SilverlightClient.xap")]
  Stream GetClient();

  /// <summary>
  /// Gets a playlist of a specific album.
  /// </summary>
  [OperationContract, WebGet(UriTemplate =
     "/albumplaylist.wax?id={albumId}&base={basePath}")]
  Stream GetAlbumPlaylist(int albumId, string basePath);

  /// <summary>
  /// Gets a specific track.
  /// </summary>
  [OperationContract, WebGet(UriTemplate = "/song?id={trackId}")]
  Stream GetTrack(int trackId);
}

The single method in IJukeBoxServer allows the client to fulfill its requirement to display albums to the user. The first argument searchText is specifying whether the user is searching for a specific artist, album or track. The second argument skipCount allows the client to page albums by skipping search result matches, and the third argument resultCount is a constant value set in the client describing the number of albums to return, i.e. the number of albums existing on each page in the client.

C#
[ServiceContract]
public interface IJukeBoxServer
{
  /// <summary>
  /// Gets albums matching a specified search text. The caller has also the
  /// option to specify the number of albums to skip and take.
  /// </summary>
  [OperationContract]
  SearchResultDto GetAlbums(string searchText, int skipCount, int resultCount);
}

Data Structure

The server is storing media related data in a Northwind database and accesses it using LINQ to SQL. LINQ has become my favorite among the features of the .NET Framework; it's so clean and elegant to write! The reason for using a Northwind database instead of a SQL Server Compact database, which you can add to a project as easily as you add anything else in Visual Studio, is because of LINQ to SQL. The current version of the Compact edition doesn't support LINQ to SQL, and since I am a huge fan of LINQ, the Compact edition wasn't really any option. I also suspect that Northwind is faster than Compact, but please don't flame me about this because it is my own personal opinion and isn't backed up by any scientific data at all. You are however free to prove me wrong whenever you want. Wait a minute, was that a thrown gauntlet? I guess it was...

Silver JukeBox Server - Database structure

The database contains four tables:

  • The Track table is a representation of the media files found on disk. One entry in this table represents one file on disk.
  • A track does not contain an artist; it is instead referencing one in the Artist table using a foreign key relationship called ArtistId. By having this relationship we are instructing the database to manage the tracks depending on performer.
  • We are separating albums in the same way we are separating artists, by having a foreign key relationship called AlbumId pointing from a track to an entry in the Album table. This way the database is managing what tracks a certain album contains.
  • The AlbumArtist table connects artists to albums by defining the two foreign key relationships AlbumId and ArtistId. A query to get the artists on a specific album is because of these relationships very fast.

Using Amazon Associates Web Service for Getting Album Art

The display feature of album art in the client was crucial. Quite a lot of effort was put into the investigation of a reliable and high-percentage search hit service. Most tips on the internet pointed at a solution where the Yahoo image search engine was the best way to go, but I felt that there had to be better solutions. Finally I decided that Amazon Associates Web Service, formerly named Amazon E-Commerce Service or ECS, was the best solution simply because it provides a very mature API where I actually can state that I am searching for images. They also have a very, VERY good documentation of the API. I started implementing documented URL requests until I stumbled upon the Sample Code & Libraries section where you actually can download a .NET library that implements the URL requests for you. You simply describe what you're searching for by writing:

C#
public static Uri SearchForAlbumArt(string artistName, string albumName)
{
  // Create the query
  AmazonECSQuery query = new AmazonECSQuery("MyAccessKeyId", null, AmazonECSLocale.US);

  // Create the request
  ItemSearchRequest request = new ItemSearchRequest
  {
    Artist = artistName,
    Title = albumName,
    SearchIndex = "Music",
    ResponseGroup = new List<string> { "Images" }
  };

  try
  {
    // Perform search on Amazon
    ItemSearchResponse response = query.ItemSearch(request);

    // Validate the response
    if (
      response.Items.Count > 0 &&
      response.Items[0].Item.Count > 0 &&
      response.Items[0].Item[0].ImageSets.Count > 0 &&
      response.Items[0].Item[0].ImageSets[0].ImageSet.Count > 0)
    {
      return new Uri(response.Items[0].Item[0].ImageSets[0].ImageSet[0].MediumImage.URL);
    }
  }
  catch (AmazonECSException e)
  {
    // Do something about the exception
  }

  return null;
}

As you can see, the only negative part of using the Amazon web service is that you are required to sign up for a free Associates Web Service account to receive a Amazon Access Key Id, that you specify when creating the query. The server is storing the ID as an application setting, making it possible to set it in the downloadable binaries.

One other thing that's pretty cool is that you in your query can specify which locale to use, i.e. depending on where you live in the world you can optimize your requests by choosing a locale near you. I live in Sweden and can then choose between UK, DE (Germany) and FR (France) instead of e.g. US to speed up my requests. The locale is also defined as an application setting, making it possible to set in the downloadable binaries.

Encrypted Communication

The communication between server and client is possible to encrypt by enabling HTTPS in the server configuration dialog. The BasicHttpBinding and WebHttpBinding are created in a way that support encrypted communication, but by doing so they rely on an installed certificate on the computer. Instructions of how to create and install a certificate is described in the next chapter.

C#
private bool StartWebService()
{
  serviceHost = new ServiceHost(typeof(Session), ServiceUri);

  // Create and configure the bindings, RPC and web/file delivery
  BasicHttpBinding basicBinding = CreateBasicHttpBinding();
  WebHttpBinding webBinding = CreateWebHttpBinding();

  serviceHost.AddServiceEndpoint(typeof(IJukeBoxServer), basicBinding,
      "SilverJukeBoxServer");
  serviceHost.AddServiceEndpoint(typeof(IFileServer),
      webBinding, "").Behaviors.Add(new WebHttpBehavior());

  EnableMetadata(serviceHost);

  try
  {
    serviceHost.Open();

    // Web service successfully started
    return true;
  }
  catch (AddressAlreadyInUseException)
  {
    // Unable to start the web service
    return false;
  }
}

private BasicHttpBinding CreateBasicHttpBinding()
{
  BasicHttpBinding basicBinding;

  // If we use HTTPS we need to enabled transport security
  if (IsHttpsEnabled)
  {
    basicBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
  }
  else
  {
    // If we don't use HTTPS we need to enabled credential security if
    // authentication is required.
    basicBinding = new BasicHttpBinding(IsRequiringAuthentication ?
      BasicHttpSecurityMode.TransportCredentialOnly : BasicHttpSecurityMode.None);
  }

  // Use standard windows authentication if enabled (Ntlm works with FireFox 2.0,
  // Windows doesn't)
  basicBinding.Security.Transport.ClientCredentialType = IsRequiringAuthentication ?
    HttpClientCredentialType.Ntlm : HttpClientCredentialType.None;

  // This should be better configured to match realistic worst case values.
  basicBinding.ReceiveTimeout = TimeSpan.FromHours(2);
  basicBinding.ReaderQuotas.MaxArrayLength = int.MaxValue;
  basicBinding.ReaderQuotas.MaxBytesPerRead = int.MaxValue;
  basicBinding.ReaderQuotas.MaxDepth = int.MaxValue;
  basicBinding.ReaderQuotas.MaxNameTableCharCount = int.MaxValue;
  basicBinding.ReaderQuotas.MaxStringContentLength = int.MaxValue;

  return basicBinding;
}

private WebHttpBinding CreateWebHttpBinding()
{
  WebHttpBinding webBinding;

  // If we use HTTPS we need to enabled transport security
  if (IsHttpsEnabled)
  {
    webBinding = new WebHttpBinding(WebHttpSecurityMode.Transport);
  }
  else
  {
    // If we don't use HTTPS we need to enabled credential security if authentication
    // is required.
    webBinding = new WebHttpBinding(IsRequiringAuthentication ?
      WebHttpSecurityMode.TransportCredentialOnly : WebHttpSecurityMode.None);
  }

  // Use standard windows authentication if enabled
  // (Ntlm works with FireFox 2.0, Windows doesn't)
  webBinding.Security.Transport.ClientCredentialType = IsRequiringAuthentication ?
    HttpClientCredentialType.Ntlm : HttpClientCredentialType.None;

  return webBinding;
}

Creating and Installing a Certificate

Creating and installing certificates can be a real headache but I've created a couple of files helping you on the way. These files are located in the Thirdparty folder of the downloadable source, i.e. NOT part of the binaries.

  • Generate_certificate.bat contains instructions of how to create and install a certificate (Win32 OpenSSL Light is a prerequisite)
  • httpcfg_add.bat is referred in Generate_certificate.bat, and contains instructions of how to map a certain certificate to a port on the computer
  • httpcfg_remove.bat is the opposite to httpcfg_add.bat, i.e. it will remove a certificate mapping from a certain computer port

The following instructions in Generate_certificate.bat will install a certificate for you, but since you created and signed the certificate yourself it won't be considered as valid according to IE, Firefox or whatever browser you are using. A certificate is only valid if all of the following statements are true:

  • Certificate hasn't expired
  • Certificate's common name is matching URL
  • Certificate is issued by a certified authority, e.g. VeriSign

The certificate created by Generate_certificate.bat will fulfill the first two statements assuming the correct common name was entered, but nevertheless the third statement will fail. This will manifest itself as a big red warning in your browser, which you of course can ignore and nevertheless browse to the client's user interface.

Silver JukeBox Client - Invalid certificate warning in Internet Explorer

Unfortunately you won't get that option in WMP, which instead will show you a cryptic message and prevents you from playing music acquired from the client.

Silver JukeBox Client - Windows Media Player error

What you will have to do is to import the certificate into Windows 'Trusted Root Certification Authorities' store. The easiest way to do so is by browsing to the client using IE, ignore the certificate warning, and when view the certificate properties by clicking the red shield icon next to the address field.

Silver JukeBox Client - Invalid certificate shield in Internet Explorer

There is a button within the certificate properties dialog that allows you to install the certificate, assuming you are running as an administrator. Make sure you place the certificate in the Trusted Root Certification Authorities, and then restart IE.

Authentication

Access to the client can be restricted using NTLM authentication, i.e. the same user management Windows is using. I myself created a new standard user called 'JukeBox,' and am using it when accessing the client. There are, however, a couple of issues you might run into before getting it to work. First of all, the user account you are using to access the client has to have a password, users without password will instantly be denied access. Furthermore, you have to disable ForceGuest if you are running a computer with Windows XP that isn't joined to a domain. And what is ForceGuest you might wonder, well Microsoft says it best:

"If Simple File Sharing is enabled on a Microsoft Windows XP system that is not joined to a domain, then all users who access this system through the network are forced to use the Guest account. This is the "Network access: Sharing and security model for local accounts" security policy setting, and is also known as ForceGuest."

This means that even though you created a new user to access the client with, the guest account is used nevertheless, which of course will fail. Follow these steps to disable the Simple File Sharing feature:

  1. Double-click the My Computer icon on the desktop, or click Start - My Computer
  2. Select Tools - Folder Options
  3. Select the View tab
  4. Clear the Use simple file sharing (Recommended) check box

You might have considerations for creating new users on your computer, for instance you might dislike that the new user is shown in the Windows login screen, but the following chapters will fix those issues for you.

Fix: Remove User Account from the Login Screen

A user account for the Silver JukeBox shouldn't be visible on the Windows login screen. Furthermore, if you by mistake log into Windows with that user a bunch of new files and folders will be created on your computer, something that's really annoying since the purpose of the user never was to actually log into Windows. A guide of how to removing a user from the login screen can be found here.

Fix: Enable Automatically Login

If you only have one user on your computer, and that user doesn't have a password, it means that Windows automatically will logon that user when you start the computer, and you have probably never seen the Windows login screen. As soon as you create a new user, you won't automatically be logged in anymore. If you feel that the login screen is the most annoying thing in the world, feel free to follow Windows XP or Windows Vista guide on how to automatically login to Windows.

Mail-Delivered Exception Logging

As it happens to be, I'm not anywhere near a perfect software engineer, thus I would like to know when the application crashes. The server is configured to send application exceptions using the SmtpAppender supported by log4net when a crash occurs, 'if' the user wishes so. The mail includes the last 1024 log rows along with information about the hosting computer. A typical mail looks like this:

Silver JukeBox Server - Exception logging sample

I know this is a sensitive issue, but my only intension is to improve the quality of the application, nothing more. If you don't wish to participate in improving the quality, feel free to deny when asked by the application.

History

  • 7 November, 2009
    • Updated source code
  • 1.0.1.0 (20 June 2009)
    • Changed host address due to mail-delivered exception logs
  • 1.0.0.0 (14 Jan 2009)
    • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)