Friday, July 31, 2009

Possible Extensions

The code I currently have is working fine for me. However, there may be a few extensions that can be helpful.

Configure locations

For now, some information is hard coded, for example the initial path that the playlists are to be written to. There should be some easy way of configuring this information and storing the information “somewhere”.

Check existence of files

It only makes sense to put files in the playlist if the files exist at the location that is put into the playlist. The Converter could check if the file actually exists and only include the file if it is already there.

This information could be checked at the time the playlist is displayed in the listbox. Not sure if the runtime of all that checking would be acceptable.

Copy the music files if required

The next step would be to have the Converter copy the file from the local path to the server location if the file is missing at the server location. That way, the file would be there whenever it is referenced in the playlist on the server.

Interested or other ideas?

Let me know if one of these or some other feature would be useful to you – just leave a comment or drop me an email. I will then see if I can put that in the code.

Main

Friday, July 24, 2009

Retrieving track information from the iTunes XML library file

In order to convert a playlist as described in the previous post, we need two pieces of information:

  1. the tracks (represented by their IDs) in a playlist
  2. the location of the file of a certain track

How to get this from the iTunes XML library file is described in the next sections.

Get Tracks in a playlist

Similar to the playlist member in the library, the trackIDs member of the playlist is built at the fist time it is accessed:

public List<String> getTrackIDs()
{   // lazy initialization
 if (_trackIDs == null)
 {
     _trackIDs = new List<String>();

     //Query XML for TrackIDs
     IEnumerable<XElement> tracks = (from element in _root.Descendants()
                                       where element.Value.Equals("Track ID")
                                       select element);
     foreach (XElement track in tracks)
     {
         _trackIDs.Add((track.NextNode as XElement).Value );
     }
 };
 return _trackIDs;
}

Again, the LinkToXML query retrieves the items with a certain value (“Track ID”) from the XML. The statement uses just the part representing the playlist (stored in the _root member). It then moves over to the NextNode to get the actual value and stores all of these in a member that the form can then iterate over.

Get filenames for a given TrackID

Once you know the IDs of all the tracks in the playlist, you can then retrieve the filename as follows:
internal string GetTrackLocation(string ptrackID)
{
 // get root of XML-Dictionary for this track
 XElement track = (from element in _root.Descendants().Elements("key")
                   where element.Value.Equals(ptrackID)
                   select element).First().NextNode as XElement;

 XElement location = (from element in track.Descendants()
                   where element.Value.Equals ("Location")
                   select element).First().NextNode as XElement;

 string fileName = HttpUtility.UrlDecode((string)(location  as XElement));

 return fileName;
}
This code is in the library class. I could have used a dedicated track class, but apart from this little piece of information, there wasn’t anything else needed – so that felt a little bit too small to actually build a class for that.

The code gets the location in these steps:

  1. Get the dict for this specific track (lines 4 to 6)
  2. Get the “Location” from within this specific dict (lines 8 to 10)
  3. Get the ‘proper’ formatting by URL-Decoding the value from the XML.
I’m sure that there are easier or more compact ways to achieve this, but this code is working fine and seems quite clear to me. Let me know if you have better suggestions ….

Main

Friday, July 17, 2009

Converting the iTunes Playlists into the M3U format

Once the playlists are displayed in the listbox, the user can select the playlists to be exported and then click the “Convert” button. Here’s the code that is triggered by the button click:

private void btnConvert_Click(object sender, EventArgs e)
{
 // check existence of psPath
 DirectoryInfo di = new DirectoryInfo(txtResultPath.Text+"\\");
 if (!di.Exists) {
     MessageBox.Show ("Path to write to does not exist", "Problem",
         MessageBoxButtons.OK,MessageBoxIcon.Error);
     return;
 }

 foreach (Playlist pl in lbPlaylists.SelectedItems)
 {
     WriteToFile(pl, Path.GetDirectoryName(txtResultPath.Text+"\\"));
 }

 MessageBox.Show("Playlists written","Info",MessageBoxButtons.OK,MessageBoxIcon.Information);
}
After some checking for the existence of the path to write the playlists to (lines 4 to 9), the code loops through all the selected playlists and writes out each playlist to a file by calling the auxiliary function “WriteToFile”.

Here’s this method:

private void WriteToFile(Playlist pl, String psPath)
{
 //create the file - overwrite if necessary
 FileInfo fi = new FileInfo (Path.Combine (psPath, pl.name() + ".m3u"));
 StreamWriter fs = new StreamWriter(fi.Open(FileMode.Create));
 // write stuff
 foreach (String trackID in pl.getTrackIDs())
 {
     String trackLocation = _library.GetTrackLocation(trackID);

     // now remove the "local" part of the location
     int pos = trackLocation.IndexOf("/iTunes Music/");
     string newFileName = trackLocation.Substring(pos+13);
     newFileName = "//Marvin/musik" + newFileName;
     newFileName = newFileName.Replace("/", "\\");

     fs.WriteLine(newFileName);
 }
 fs.Close();
}
This could be a member of the Playlists, but I’ve decided to leave it in the Converter class in order not to introduce additional dependencies in the Playlist class. This way, all the Playlist and Library classes do is read the contents of the iTunes library. This way the Converter class does not have to deal with any XML. The code iterates through all the tracks in the playlist (through their IDs, line 7). The tracks are retrieved in the order that they are in the playlist file (which is implicitly the order of the tracks in the playlist). In the loop, the file location of the track is retrieved (line 9). This location is then stripped of the “local part” (the part up to and including “/iTunes Music/”, lines 12&13), then the path for my server is added (line 14) and some proper formatting is applied (line 15) so the new file path is usable by the TVersity media server. Each constructed filename is written out to the file. The next post describes the additional functionality in the library class in order to retrieve the required track information from the XML.

Main | Next

Friday, July 10, 2009

Display iTunes Playlists in the listbox

In the previous post I described how the playlists in an iTunes library are read from the XML file and then added to the listbox.

In order for the listbox to properly display these objects, the ToString() member for each of these playlist objects is called. Here’s this method:

public override string ToString()
{
  return name() + " (ID: " + id() + ", " + getCount() + " titles)";
}
Basically, it just constructs a string that gives out some information about the playlist. Each of these pieces of information is a simple method that retrieves the required information from the part of the playlist from the XML similar to these examples:
public string id()
{
  return (string)((from element in _root.Descendants()
                   where element.Value.Equals("Playlist ID")
                   select element).First().NextNode as XElement);
}

public int getCount()
{
  return (from element in _root.Descendants()
          where element.Value.Equals("Track ID")
          select element).Count();
}
Each of these queries is quite simple once you have a look at the structure of the XML, but the combination is pretty powerful to display all the playlists in my iTunes library: image

The next post describes how the conversion of the playlists works. Main | Next

Friday, July 3, 2009

Reading the Playlists from the iTunes Library XML

Once you have selected an iTunes Library XML (or if the default location is working), the code reads through the XML file to figure out which playlists are in the file.

The playlist is stored in an instance variable (line 1) that is instantiated with the filename as a parameter (line 3):

private Library _library;
...
_library = new Library(txtLibrary.Text);

In the constructor of the Library class the XML file is read:

public Library (string pfileName) {
 // no other way to set the root
 _root = XElement.Load(pfileName);
 _playlists = null;
}

Line 3 is the first line of code that uses LinqToXML ... not much too it. For a production environment, some kind of error handling would probably have to be added. The instance variable _playlists is not initialized, it will be set up at the time it is accessed.

The fist access happens when the playlists are to be displayed in the list box. This code is from the Playlist Converter Form right after instantiating the _library instance variable:

foreach (Playlist pl in _library.getPlaylists()) {
 lbPlaylists.Items.Add(pl);
}

Getting the playlists requires some XML reading:

public List<Playlist> getPlaylists ()
{   // lazy initialization
 if (_playlists == null)
 {
     _playlists = new List<Playlist>();

     //Query XML for Playlists
     XElement rootList = (from element in _root.Descendants()
                          where element.Value.Equals("Playlists")
                          select element).First().NextNode as XElement;
     IEnumerable<XElement> listIDs = (from listFields in rootList.Descendants()
                                        where listFields.Value.Equals("Playlist Persistent ID")
                                        select listFields);
     foreach (XElement list in listIDs)
     {
         _playlists.Add(new Playlist(list.Parent as XElement));
     }
 };
 return _playlists;
}

The method returns a list of playlists. If the instance variable is already initialized, it is just returned in a getter fashion. Otherwise we build the list in a few simple steps.

First, we find the root element for the playlists in the XML file with the query in lines 8 to 10. We look for a node that has a value of “Playlists”. There is only one in the file, so “.First” returns this single node. The array of playlists is the next node after that, so “.NextNode” returns this array.

Once we have that array, we can then query in it for the individual playlists. Lines 11 to 13 looks for all the “Playlist Persistent ID” nodes in this array. Then we can iterate through these elements and build Playlist objects with the dict for an individual playlist (line 16).

After each of these new playlist objects has been instantiated, they can just be added as an item to the listbox. In order for the listbox to properly display these items in a list, the ToString() member of the playlist will be called. This will be described in the next post …

Main | Next