Click here to Skip to main content
14,382,347 members

MP3-CMS Project

Rate this:
3.42 (20 votes)
Please Sign up or sign in to vote.
3.42 (20 votes)
27 Sep 2010CPOL
Update to the MP3-CMS project.

Image 1

The MP3CMS entry page



Even in 2008, managing digital music libraries remains an enduring problem in a distributed environment. Most of us have personal computers or PC appliances in just about every room in the house. Making sure all of these devices all have access to the family's digital music library should be as simple as creating a file server. However, as the features included in these standalone players increase, so do application footprints. Each application needs to spend countless hours scanning the file server for music, updating its database, and then there's the problem of synchronization or having to make sure that all these applications have the newest music in their databases. Ultimately, it becomes quite an administrative task.

What is needed is a single interface, with common features that run entirely from a web server. It needs to be intelligent. Specifically, it should allow consumers to create playlists, rate tracks, comment on albums, generate smart playlists based on criteria such as star ratings, genres, newest tracks, and most importantly, it should remember which tracks have been played and how often. Based on such analysis, the system should recommend music, and this system should be self-contained with as little administration as possible. If users add or delete music from the server, the system should update itself appropriately.

The MP3-CMS is a server based music management solution that addresses these traditional constraints. It provides a clean user interface for cataloging and listening to your music from a web browser. The system is composed of two components: a web site and a scanner. The scanner application is embedded in the web site and runs concurrently. The scanner identifies the path it should scan from the site's configuration files, and it runs in the background behind the scenes and watches the path for changes. As you go about your business adding new MP3s or deleting them, the scanner will automatically detect this activity and update the embedded SQLite database accordingly.

Obviously, the CMS is huge, and has a degree of complexity that cannot be covered in one article, so this article will focus on some of the interesting controls and mechanisms created as part of the project.


MP3CMS Editions

The MP3 CMS consists of two editions: the enterprise server edition and the local workstation edition. The code base is the same, but the distribution mechanisms are different. The desktop edition comes packaged in a professional NSIS installer, and sets up and configures a local Cassini ASP.NET web server for the user. The desktop edition is designed for anyone to start using and enjoying the MP3CMS product. The enterprise server edition is designed for engineers and web administrators who have access to enterprise infrastructure and intend on deploying a distributed MP3CMS installation across a private intranet, corporate cloud, or small business.

Automated Installers

Installing enterprise web solutions can be quite a challenge since they typically require the administrator to have a comprehensive understanding of web server technology and enterprise best practice. This typically includes creating service accounts, privileged application pools, and setting file and share permissions to grant the application the appropriate level of access.

For this reason, it is absolutely essential to deliver an automated installation wizard of some kind to assist the customer with the installation. The following sub-section will document the various installation technologies we investigated and delivered with the MP3CMS product.

Desktop Edition

MP3CMS Desktop Edition Installer

The MP3CMS desktop edition installer

For the desktop edition, we decided on the NSIS (NullSoft Installation System) for distributing the MP3CMS Desktop Edition product. This was mostly due to flexibility and ease of use of NSIS. The installer offers three components for the user to install: Application Files, Web Server, and Auto-Configuration. Below you can see these NSIS sections:

Section "Applicaton Files (Required)" APPFILES

    # Copy Files/Folders
    SetOutPath $INSTDIR
    File /r "site\*.*"

    # Create uninstaller
    WriteUninstaller "$INSTDIR\uninst.exe"


Section "Web Server" APPSRV

    nsExec::ExecToLog '"$SYSDIR\cscript.exe" "/nologo" 
              "$INSTDIR\installer\InstServer.js" "$INSTDIR"'


Section "Auto-Configuration" ACWS

    # Get Install Options dialog user input
    ReadINIStr ${SETTING_MUSIC_PATH} "$PLUGINSDIR\installer.ini" "Field 3" "State"
    # Run installer script
    nsExec::ExecToLog '"$SYSDIR\cscript.exe" "/nologo" 
       "$INSTDIR\installer\config.js" "$INSTDIR" "${SETTING_MUSIC_PATH}"'


You can see from the NSIS sections above that our installation consists of three core behaviors:

  1. Copy product files to the installation directory.
  2. Run our web server installation JavaScript program.
  3. Run our site configuration JavaScript program.

In order for our product to run on desktop computers, we need to deliver a web server with the product (as well as configure that web server to host our application), and this action needs to be transparent to the user. To accomplish this feat, we install the Cassini Web Server as part of our installation. The APPSRV section of the install script executes this for us.

nsExec::ExecToLog '"$SYSDIR\cscript.exe" "/nologo" 
             "$INSTDIR\installer\InstServer.js" "$INSTDIR"'

The NSIS installer calls our InstServer.js JavaScript program to install Cassini.

Installing Cassini consists of three steps:

  1. Install CassiniExplorer.
  2. Install CassiniServer2.
  3. Register the MP3CMS application with Cassini.
function CassiniSetup() {
    _objShell.Run("msiexec.exe /passive /i " + "\"" + _dir + 
                  "\\CassiniExplorerSetup.msi\"", 1, true);
    _objShell.Run("msiexec.exe /passive /i " + "\"" + _dir + 
                  "\\CassiniServer2Setup.msi\"", 1, true);
    _objShell.Run("\"%ProgramFiles%\\UltiDev\\Cassini Web Server " + 
         "for ASP.NET 2.0\\UltiDevCassinWebServer2.exe\" /register \""
         + _strInstallPath + 
         "\" 8FE19AD1-93F5-4366-8AF9-2749BA26C4EF Default.aspx");

Using NSIS, we can deliver an enterprise web product to the desktop as if it were a desktop application and capture the widest possible user base.


Server Edition

MP3CMS Server Edition Installer

The MP3CMS server edition installer

Creating an automated MP3CMS Server Edition installer has always been a difficult proposition for us, given the complexities of enterprise architectures. At the very least, even a simple MP3CMS installation requires a privileged IIS Application Pool, a local/domain service account, and specific permissions in order to function correctly. The recommended MP3CMS design calls for a NAS/SAN file server to host the consolidated collection of music files, a web server to host MP3CMS, and a service account with permission to read and write to the remote file server storing the consolidated music collection. Of course, it's possible to host the music files on the same server as IIS, but we don't recommend a design of that nature.

In order to deliver an automated server installation wizard, we decided to leverage HTA (HTML Application) technology. This gives us the ability to address the most common installation cases.

MP3CMS Server Edition installation tasks:

  1. Create service account.
  2. Assign service account to IIS Worker process.
  3. Assign database permissions.
  4. Create IIS application pool.
  5. Create IIS virtual directory.
  6. Link IIS virtual directory to IIS application pool.
  7. Assign IIS ASP.NET version.
  8. Restart IIS Services.
  9. Kickoff MP3CMS MP3-Scanner queuing thread.

The core JavaScript functions that perform these tasks can be seen below:

function CreateNewUser() {
    var container = GetObject("WinNT://" + _wshNet.ComputerName);
    var account = container.Create("user", _userName);

function AddUserToIIS_WPG() {
    var iisWPG = GetObject("WinNT://" + 
           _wshNet.ComputerName + "/IIS_WPG,group");
    var account = GetObject("WinNT://" + 
          _wshNet.ComputerName + "/" + _userName + ",user");
    if (!(iisWPG.IsMember(account.AdsPath))) {

function SetFolderPerms(user, folder, parms, ACCESS_FLAG) {
    var oExec = _oShell.Exec("cacls \"" + folder + "\" " + 
                   parms + " "  + user + ":" + ACCESS_FLAG);
    var oResponse = oExec.StdOut;
    while(oExec.Status == 0) {
    while(!oResponse.AtEndOfStream) {
        var sLine = oResponse.ReadLine();
        if (sLine.indexOf("processed") > -1) {
            return true;
        else {
            return false;

function CreateAppPool() {
    var appPoolRoot = GetObject("IIS://localhost/w3svc/AppPools");
    var newAppPool = 
        appPoolRoot.Create("IIsApplicationPool", _appPoolName);
    newAppPool.WamUserName = _userName;
    newAppPool.WamUserPass = _password;
    newAppPool.LogonMethod = 1;
    newAppPool.AppPoolIdentityType = 3;

function CreateVirtualDirectory() {
    var oRoot = GetObject(_iisRoot);
        var oVirDir = oRoot.Create("IIsWebVirtualDir", _vdirName);
        oVirDir.AccessRead = true;
        oVirDir.AccessScript = true;
        oVirDir.AccessExecute = true;
        oVirDir.DefaultDoc = "default.aspx";
    oVirDir.Put("Path", _vdirPhysicalPath);

function AssignAppPool() {
        var oVirDir = GetObject(_iisRoot + "/" + _vdirName);
        oVirDir.AppPoolId = _appPoolName;

function SetDefaultNetFX() {
        var sys = _oShell.ExpandEnvironmentStrings("%windir%");
        var fx = _oFso.BuildPath(sys, "Microsoft.NET\\Framework\\v2.0.50727");
        var regiis = _oFso.BuildPath(fx, "aspnet_regiis.exe");
        _oShell.Run(regiis + " -sn W3SVC/1/Root/\"" + _vdirName + "\"", 0, true);

function RestartIIS() {
        _oShell.Run("iisreset /restart", 0, true);

Delivering an automated installer for use with the MP3CMS Server Edition is difficult, but for most users, the installer is sufficient in eliminating the usual (difficult) IIS/Web Server product installation.


The SQLite Database

A few revisions ago, we migrated the old MS-SQL 2005 data model to an OR/M friendly SQLite data model. This was mostly to allow us to easily change databases and to not constrain users who have no access to expensive Microsoft servers. Core business logic was relocated into a business layer in .NET repositories, and data abstraction was provided by NHibernate domain driven development. Below you can see a simple view of the data model.

Image 4

You can see how the model maps to the .NET domain entities here.

The MP3Scanner Component

Before we can provide a slick user interface or playback music files, we first have to get them into our database. This task is accomplished by the MP3Scanner component. The scanner objects are called from the web application directly, and execute in a (lowest priority) thread within the MP3CMS application thread pool.

The MP3Scanner Object Model

Refer to the object diagram below:

Image 5

The scanner consists of four primary entities:

  • Mp3Scanner: the scanning engine.
  • ScannerConfiguration: the user defined settings for scanning behavior.
  • QueueItem: an item in the queue.
  • QueueItemProcessor: the object that takes action on items in the queue.

Embedded Scanner Detail

In earlier iterations, we used an NTService component to handle scanning. This design proved to be cumbersome for many users, and made it extremely difficult to allow the web site and the scanner to communicate efficiently with one another. Eventually, we moved the scanner into the website.


We created a custom HttpModule which allows us to run some checks each time a request is received (or ended). In the OnBeginRequest method, we added logic to setup the user and system parameters:

Custom HttpModule tasks:

  1. Set admin-defined album art path.
  2. Create album art path if non-existent.
  3. Create new database and load schema if the database is non-existent.
  4. Enable/Start or disable/dispose scanner based on admin-defined setting.

Reference: DataManagementHttpModule.cs.

Scanner Workflow

Basic scanner execution:

  1. Create FileSystemWatchers (on scan path setting).
  2. Find and remove stale/obsolete records.
  3. Find new files:
    • For each folder in scan path:
      1. Add new MP3 files into queue.
      2. Process queued items (Add/Update/Delete).
      3. Delete/scrape obsolete Artists, Albums, and Genres.
      4. Cache album cover artwork (folder.jpg or embedded Id3Tag frame).
      5. Update process, collection statistics.
  4. Process any items in the queue.

Resizing cache images with PictureMagic

In stress testing earlier revisions of MP3CMS, we determined that the amount of data being sent to the browser was unusually high. One of the reasons for this was the cover art images we were caching on the web server. Some albums covers stored on remote shares (or embedded in Id3Tags) were enormous. To address this issue, we implemented an image resizer type which limited each cached album cover to a specified dimension range.

namespace baileysoft.mp3
    public class PictureMagic
        protected PictureMagic() { }

        public static void ResizeImage(string OriginalFile, string NewFile, 
               int NewWidth, int MaxHeight, bool OnlyResizeIfWider)
            Image FullsizeImage = Image.FromFile(OriginalFile);

            if (OnlyResizeIfWider)
                if (FullsizeImage.Width <= NewWidth)
                    NewWidth = FullsizeImage.Width;

            int NewHeight = FullsizeImage.Height * NewWidth / FullsizeImage.Width;
            if (NewHeight > MaxHeight)
                NewWidth = FullsizeImage.Width * MaxHeight / FullsizeImage.Height;
                NewHeight = MaxHeight;

            Image NewImage = FullsizeImage.GetThumbnailImage(NewWidth, 
                                           NewHeight, null, IntPtr.Zero);

Reference: PictureMagic.cs.

Reading ID3Tags

To read ID3Tags, we used the library UltraID3Lib from HundredMilesSoftware.

The process is pretty straightforward:

public virtual void SetPropertiesFromTags()
    UltraID3 id3Tags = new UltraID3();

    _title = id3Tags.Title;
    _trackNumber = id3Tags.TrackNum.HasValue ? id3Tags.TrackNum.Value : (short)0;
    _bitRate = id3Tags.FirstMPEGFrameInfo.Bitrate;
    if (id3Tags.FirstMPEGFrameInfo.VBRInfo.FoundFlag && 
        _duration = id3Tags.FirstMPEGFrameInfo.VBRInfo.Duration.HasValue ? 
                    id3Tags.FirstMPEGFrameInfo.VBRInfo.Duration.Value : 
                    new TimeSpan(0);
        _duration = id3Tags.FirstMPEGFrameInfo.Duration;

        //correcting tags: Fahrain (07/09/09)
        TagConvertHelper helper = new TagConvertHelper();
        string artist_str = helper.ConvertArtist(id3Tags.Artist);
        string album_str = helper.ConvertAlbum(id3Tags.Album);

        _artist = GetArtist(artist_str);
        _album = GetAlbum(album_str, artist_str, Mp3File.Folder);
    _genre = GetGenre(id3Tags.Genre);
    _year = GetYear(id3Tags.Year);


Website User Interface Component

The goal all along with MP3CMS was to provide a desktop-application like user experience from the web. There were some critical features that I was not willing to budge on that I felt were absolutely necessary for MP3CMS to compete with desktop players like Songbird and Amarok.

Required features:

  • Smart playlists: Newest songs, most played, etc.
  • Star ratings: Ability to rate songs and have them mean something (smart star rating playlists, etc.).
  • Album reviews: Ability to save reviews, bios, and lyrics.
  • Playback history: Ability to store and do things with playback information.

Of course, for all my grandiose thinking, I had to first conquer a few hurdles like, for example, figuring out how to stream music files on a remote network share to a user's web browser in ASP.NET!

MP3 Playback from the Browser

This was a real challenge, especially when MP3CMS first started five years or so ago. After exhaustive analysis, we decided to go with Macromedia Flash since it was browser agnostic and there was a fantastic reusable component out there that everyone on the web was using (at the time). The player control from Jeroen Wijering worked especially well for our purpose.

Given the potential popularity of this control, we decided to migrate our original User-Control to a Visual Studio toolbox control, which can be dragged and dropped directly onto any ASPX page.

MP3 Player Server Control (Redistributable DLL)

Image 6

In order to create a toolbox aware control, we first had to make the control dependencies embedded resources.

Image 7

Once the files are identified as embedded resources, we need to create a link to them so they can be leveraged later in code. To do this, we have to edit the AssemblyInfo.cs file.

// Embedded Resources
[assembly: System.Web.UI.WebResource("baileysoft.mp3.Resources.swfobject.js", 
[assembly: System.Web.UI.WebResource("baileysoft.mp3.Resources.mp3player.swf", 

// Design Time Support
[assembly: System.Web.UI.TagPrefix("baileysoft.mp3", "custom")]

Once the resources were linked up, we created a new WebControl project which gives you the core skeleton of the control.

Default Properties

private string _playlist = null;
private bool _shuffle = true;
private bool _autostart = false;
private string _movie = "baileysoft.mp3.Resources.mp3player.swf";
private string _scriptPath = "baileysoft.mp3.Resources.swfobject.js";
private string _width = "500";
private string _height = "400";
private string _bgcolor = "#FFFFFF";
private string _lightcolor = "0x0C0C0C";
private string _forecolor = "0x000000";
private string _backcolor = "0xFFFFFF";
private string _displayheight = "200";
private bool _repeat = true;
private bool _displaythumbs = true;
private bool _overstretch = false;
private bool _autoscroll = false;
private string _callbackPath = "../scripts/SetPlayCount.aspx";

Build and Render the Control

protected override void RenderContents(HtmlTextWriter output)

/// <summary>
/// Gets the HTML output for the player.
/// </summary>
/// <returns></returns>
public string GetHtml()
    StringBuilder builder = new StringBuilder();
    string mp3PlayerScript = "mp3PlayerScript";

    ClientScriptManager cs = Page.ClientScript;
    if (!cs.IsClientScriptBlockRegistered(this.GetType(), mp3PlayerScript))
        // JavaScript Code
        builder.AppendFormat("<script type=\"text/javascript\" src=\"{0}\"></script>\n", 
                             cs.GetWebResourceUrl(this.GetType(), _scriptPath));
        builder.Append("<p id=\"player\"><a href=\"" + 
                       "go/getflashplayer\">Get the Flash Player</a> " + 
                       "to see this player.</p>\n");
        builder.Append("<script type=\"text/javascript\">");
        builder.AppendFormat("var so = new SWFObject('{0}','mpl','{1}','{2}','7');\n", 
                             cs.GetWebResourceUrl(this.GetType(), _movie), _width, _height);
        builder.AppendFormat("so.addVariable(\"height\",\"{0}\");\n", _height);
        builder.AppendFormat("so.addVariable(\"width\",\"{0}\");\n", _width);
        builder.AppendFormat("so.addVariable(\"displayheight\",\"{0}\");\n", _displayheight);
        builder.AppendFormat("so.addVariable(\"lightcolor\",\"{0}\");\n", _lightcolor);
        builder.AppendFormat("so.addVariable(\"backcolor\",\"{0}\");\n", _backcolor);
        builder.AppendFormat("so.addVariable(\"frontcolor\",\"{0}\");\n", _forecolor);

    cs.RegisterClientScriptBlock(this.GetType(), mp3PlayerScript, builder.ToString());
    return builder.ToString();

Reference: ASP.NET MP3 Player (Mp3Player.ascx.cs).

Generating Dynamic Playlists

In order to use the Flash control above, we have to feed in a playlist in the XSPF XML format. The original design had us using the scanner to generate artist and album playlists at scan time, but with some expert assistance from another developer, that model was scrapped in favor of a dynamic generation model. The new model stores playlist track IDs in a user cookie, and a script reads from the cookie and generates the necessary compliant playlist file.

<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
        string playlistTrackIds = Convert.ToString(
        string[] trackIds = playlistTrackIds.Split(new char[] { ',' },

        List<int> trackIdList = new List<int>();

        foreach (string trackId in trackIds)

        IList<Track> tracks = TrackRepository.GetTracksById(trackIdList);
        List<Track> sortedTracks = new List<Track>();

        foreach (int trackId in trackIdList)
            foreach (Track track in tracks)
                if (track.Id == trackId)

        StreamWriter writer = new StreamWriter(Response.OutputStream);
        string playlistXml = Playlist.GenerateXml(sortedTracks);

Reference: GetPlaylist script (GetPlaylist.aspx).

Essentially, this is the core engine of the product by which everything else is built around. As the user uses the system, the code is generating XSPF compliant playlists based on cookie data and then feeding that data into the Mp3Player control.


We've recently released v2.1.0.0 to the public, and it's getting a positive response so far. Please feel free to download, modify, improve, and participate in the project! A lot of people spent a lot of time on this project, and I hope that everyone out there really enjoys it.


  • Originally submitted (03/30/2007).
  • Online demo is now up (04/01/2007).
  • Updated article (06/14/2007).
  • Updated article; entire code base re-written. Article depreciated (03/04/08).
  • Dumped the revised article and started all over again for v2.1.0.0 (08/04/10).
  • Fixed broken links, added info for MP3Player Toolbox control (09/24/2010).


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


About the Author

Software Developer
United States United States
I'm a typical 30 year old generation X guy that likes video games, NFL football, and comic style art. I have an insatiable passion for programming and doing what ever it takes to become a better programmer.

Comments and Discussions

GeneralUnable to run Scanner Pin
qumer1015-Apr-07 2:57
memberqumer1015-Apr-07 2:57 
GeneralRe: Unable to run Scanner Pin
qumer1015-Apr-07 3:04
memberqumer1015-Apr-07 3:04 
GeneralRe: Unable to run Scanner Pin
thund3rstruck5-Apr-07 4:36
memberthund3rstruck5-Apr-07 4:36 
GeneralRe: Unable to run Scanner Pin
thund3rstruck5-Apr-07 4:25
memberthund3rstruck5-Apr-07 4:25 
GeneralRe: Unable to run Scanner Pin
thund3rstruck5-Apr-07 6:34
memberthund3rstruck5-Apr-07 6:34 
GeneralBroken Links Pin
Dewey4-Apr-07 12:12
memberDewey4-Apr-07 12:12 
GeneralRe: Broken Links Pin
thund3rstruck4-Apr-07 14:20
memberthund3rstruck4-Apr-07 14:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Posted 30 Mar 2007


110 bookmarked