Introduction
This project is born out of pure frustration with Microsoft Mediaplayer and Itunes. The thing I needed was a simple player for my audio books that remembers the last position (file index and position).
Some of my audio books are a composed of several MP3 files (not all containing full meta information). So it would be nice to get a cover image if none is provided and to guess the author and title. The application is a hobby project, not a commercial grade application. So don't expect any unit tests or any other guaranties, it works on my machine (A Windows 7 64-bit laptop). It’s placed on CodeProject to give something back to everybody posting nice articles on the site. May you find the application interesting or the code useful. It contains the full source code and a setup project for the executable.
Application Requirements
Remember Position
There is only 1 playlist (the current). It will be written to the application data directory when the application closes and read at application startup. The playlist (class AudiobookList.cs) contains the names of the files, the cover image and the current playing position.
Easy to Create New Playlist
Support for drag-and-drop. All files dropped on the application will form a new playlist and start playing. The files can also be added to the commandline startup parameters.
Read Meta-Info
The taglib is used to read the meta information from a media file.
Guess meta-info Based on Filename
Use the Amazon webstore to check artist and title and get the cover image. You will need to supply your own account key. This key can be obtained without cost from Amazon (http://aws.amazon.com/).
Implementation Details
UI
The application is built using WPF and is a border-less application. The border can be hidden by adding the following to the declaration of the window:
<window ResizeMode="NoResize" WindowStyle="None" AllowsTransparency="True"
Background="Transparent">
Adding a leftmouse down eventhandler
adds support for moving the window around on the screen when the left mouse button is held, while moving the mouse.
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender == this)
{
DragMove();
}
}
The main window is still called Window1
because I am a bit lazy. ;-) The code is straight-forward without any tricks, other than some opacity changes on bitmaps.
Playlist
The playlist is stored as an instance of AudiobookList
. The class itself contains all the code needed to create, load and store the list. Based on a list of files or a directory, a new instance can be created. The directories will be scanned recursively and each file will be added if it’s an audio file. This is done by getting the mime-type from the registry based on the file extension. If it starts with “audio”, we assume it’s an audio file.
The filenames are sorted based on the numbers in the file and the containing directory. Every number is added, multiplying the total with 1000 for each number found.
public static FileType DetermineFileType( string filename )
{
string mime = MimeType(filename);
if (mime.StartsWith("image"))
{
return FileType.Image;
}
else if (mime.StartsWith("audio"))
{
return FileType.Audio;
}
return FileType.Other;
}
private static string MimeType(string filename)
{
string mime = "application/octetstream";
string ext = System.IO.Path.GetExtension(filename).ToLower();
Microsoft.Win32.RegistryKey rk =
Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(ext);
if (rk != null && rk.GetValue("Content Type") != null)
{
mime = rk.GetValue("Content Type").ToString();
}
return mime;
}
Meta-info
The class DetectMediaInfo
will retrieve the meta info from a file. It will try the MP3 tags first using the open-source TagLib
library. If no information is provided in the meta-info, we try to extract the author and title from the filename. We use the Amazon web store to check them. The parameters for Amazon are added to a URL and hashed using the access and secret key (not provided).The access and secret key can be entered using the config page and will be stored in the application config file. The parameters for the Amazon WS are hashed and added to the URL. The response is plain XML. The code itself was provided by Amazon. The only thing I added was the processing of the resulting XML with LINQ.
public void FindBookByTitle(string searchTitle, string searchAuthor,
out string bookTitle, out string bookArtist, out string imageUrl)
{
bookTitle = string.Empty;
bookArtist = string.Empty;
imageUrl = string.Empty;
string url = BuildRequestSearchBookTitle(searchTitle, searchAuthor);
string xml = CallAmazonWS(url);
ProcessXmlBookResult(xml, ref bookTitle, ref bookArtist, ref imageUrl);
}
private string BuildRequestSearchBookTitle(string searchTitle,
string searchAuthor)
{
IDictionary parameters = new Dictionary();
parameters["Service"] = "AWSECommerceService";
parameters["Version"] = "2009-03-31";
parameters["Operation"] = "ItemSearch";
parameters["SearchIndex"] = "Books";
parameters["Title"] = searchTitle;
if (string.IsNullOrEmpty(searchAuthor) == false)
{
parameters["AuthorName"] = searchAuthor;
}
parameters["ResponseGroup"] = "ItemAttributes,Images";
string requestUrl = Sign(parameters);
return requestUrl;
}
private static string CallAmazonWS(string url)
{
WebRequest request = HttpWebRequest.Create(url);
WebResponse response = request.GetResponse();
StreamReader reader = new StreamReader( response.GetResponseStream( ) );
return reader.ReadToEnd( );
}
private static void ProcessXmlBookResult(string xml, ref string bookTitle,
ref string bookArtist, ref string imageUrl)
{
XNamespace ns = "http://webservices.amazon.com/AWSECommerceService/2009-03-31";
XElement root = XElement.Parse( xml );
bool isValid = RequestIsValid(root, ns);
int resultCount = GetResultCount(root, ns);
if ( isValid && (resultCount > 0))
{
XElement item = GetFirstItem(root, ns);
GetAuthorAndTitle(item, ns, ref bookTitle, ref bookArtist);
imageUrl = GetItemImage( item, ns );
}
}
The Player
The player is a COM interface of Microsoft Mediaplayer, hence the WMPLIB
interop library. The MyPlayer
class is an abstraction of the COM instance. We cast the interface to a WindowsMediaPlayerClass
instance. This allows us to receive PlayStateChange
event and set the Autostart
property to false
. There is no need to do anything fancy. The UI has a background timer that is run every second. It checks if the player is playing and then prints the current duration as a string
. A duration string
is provided if the Mediaplayer is not yet playing, otherwise we get an empty string
.
Ideas to Improve the Application
- The application assumes there is only one author and title for all the files. If you add different titles to the list, only the first will be shown.
- The XML result from Amazon only processes the first item. Maybe the other items may also contain a valid result.
- Add some more checks and add some robustness. Currently only the real bare minimum of the exceptions are trapped with a
try
/catch
.
- Add support for formats other than MP3. It may work, but none is tested.
- Use the cloud as a storage for audio files and
AudiobookList
. So play may continue on every computer used.
Have fun!!
History
- 7th March, 2010: Initial post