Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Automatic music categorization with .NET

DZone's Guide to

Automatic music categorization with .NET

·
Free Resource

If you have used a music player on your computer, you probably know that there is a future called “folder watch” – you can set a folder for the player to watch, and once new files are dropped into that folder, those get automatically sorted in various folders based on the artist and albums, assigned to that artist.

In this article, I am going to show how to implement similar functionality with shell interactions and FileSystemWatcher.

First of all, I am working with a sample console application (it really doesn’t matter what application type this is at the moment).

Add a reference to shell32.dll to your project. It can be found in the System32 subfolder in your Windows folder. I will be using this DLL to get the MP3 information. I am only using MP3 files in this article. The ID3 data is read with the help of file columns that can be read via the Folder class in Shell32.

First of all, let’s create an instance of FileSystemWatcher that will be watching the folder for changes. It works continuously (unless you explicitly disable it) while the application is running. Once the application is closed, it is no longer active, so if you want to have a somewhat permanent folder watch, you might want consider developing a service instead.

Here is what I used to create my instance of FileSystemWatcher:

FileSystemWatcher watcher = new FileSystemWatcher();

watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite;
watcher.Filter = "*.mp3";
watcher.Created += new FileSystemEventHandler(watcher_Created);
watcher.Path = @"D:\Temporary";
watcher.EnableRaisingEvents = true;

The NotifyFilter sets the type of changes to watch for. In our case it will track down new files that are dropped in the folder (existing files will remain). The Filter property sets the file format to watch, so at the moment it only applies to MP3 files. Created points to a new event handler that will be triggered once the watcher is notified of a change that needs to be handled. Path sets the path that needs to be watched and EnableRaisingEvents allows the watcher to trigger its events when necessary. Of course, you can set a different folder to watch than the one I indicated here.

The folder is being watched now, if you launch the application. But this is not the only purpose of the application. We need to move new files to folders, categorizing those by artist name and album name. To do this, I created a separate method called OrganizeFiles:

void OrganizeFiles(string watchPath, string path, string name)
{
string artist;
string album;

try
{
Shell shell = new Shell();
Folder folder = shell.NameSpace(watchPath);
FolderItem folderItem = folder.ParseName(name);
artist = folder.GetDetailsOf(folderItem, 13);
album = folder.GetDetailsOf(folderItem, 14);

if ((!string.IsNullOrWhiteSpace(album)) && (!string.IsNullOrWhiteSpace(artist)))
{
string perspectiveDir = watchPath + @"\" + artist + @"\" + album;
if (Directory.Exists(watchPath + @"\" + artist))
{
if (Directory.Exists(perspectiveDir))
{
File.Move(path, perspectiveDir + "\\" + name);
}
else
{
Directory.CreateDirectory(perspectiveDir);
File.Move(path, perspectiveDir + "\\" + name);
}
}
else
{
Directory.CreateDirectory(perspectiveDir);
File.Move(path, perspectiveDir + "\\" + name);
}
}
}
catch (Exception ex)
{
Debug.Print(ex.Message);
}

}

First of all, make sure you have Shell32 referenced in your project, as well as the System.IO namespace, before using this method.

The instance of Folder sets the main folder where files that contain necessary data are placed. In this case, it will be the same folder used by the FileSystemWatcher instance, therefore I am passing it via watchPath. The instance of FolderItem handles the file passed to it, so I am passing it the MP3 file name via name.

The interesting part comes when I am trying to use Folder.GetDetailsOf(FolderItem,ColumnID). For MP3 files, not only the artist and album data is exposed. In case you want to implement a different type of categorization (for example, based on rating), take a look at this graphic showing the column IDs and the data that is stored in that specific column for the specified file. In this case it applies to MP3 files, so it might be different for other file types.


 
Following this structure, I can easily select the needed columns to obtain various information about a track. If you look at the method above, you can see that I am only selecting the artist and album, since this is what my categorization process relies on.

After I tried to obtain the artist and album name, I am checking whether those are not empty. I do not want to categorize files without proper ID3 data, so I am only putting the file in a different category folder if it has both the artist and the album set.

If the folder already exists (artist/album), the file is simply moved. If not, the proper folders are created and then the file is moved.

But at this moment, the application doesn’t do much. We need to actually create the event handler that will be triggered once a proper file is dropped. To do this, we already declared that there is watcher_Created that handles the process. It looks like this:

void watcher_Created(object sender, FileSystemEventArgs e)
{
var fs = (FileSystemWatcher)sender;

OrganizeFiles(fs.Path,e.FullPath, e.Name);
}

Here, I am getting the sender and I am using it as a FileSystemWatcher -  to read the path that is passed to the OrganizeFiles method as the watch patch. Then, I am getting the full path for the file dropped in the folder as well as its name, also passed to the OrganizeFiles method.

If you run the application and create a new MP3 file inside the folder that you set to be watched over, you will see that the file will be moved to an artist/album folder, if the file has proper ID3 information.

Topics:

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}