Converting iTunes playlists to Zune playlists using VB 9.0
It has been a while since my last post and I apologize for that. There was a lot of work going on in getting the xml integration in VB into production quality. In this blog I would like to share with you a fun little demo I wrote for Xml 2006 conference. This demo shows some of the new power of language integration of XML in VB and the LINQ to XML API. This technology will be available for you to try on your own in the next CTP for Visual Studio “Orcas” but in the meantime, I’d love some feedback on how the code looks.
Application Description
The scenario for the demo is that my wife and I are sharing the same PC at home. She has iPod and I just recently got myself a Zune player. My wife likes creating playlists in iTunes from our shared library of MP3s that I would like to listen to in my Zune. So I decided to write a little app that converts iTunes playlist to a Zune playlist.
It turns out that doing so is a pretty straightforward Xml transformation since iTunes keeps its library information in Xml file " iTunes Music Library.xml" located in the iTunes music folder (defined in the user preferences). By default this file is located at "C:\Documents and Settings\YOUR_USER_NAME\My Documents\My Music\iTunes\". As a side note, I could have also used the ITunes API but it wouldn't be that much fun :)
Before we start, this demo uses the current VB 9.0 compiler that is not public yet, so unfortunately to try it out you will need to wait for the next Orcas CTP. You will also need to install iTunes and Zune client software (you do not need to own either of these players). And finally this demo shows how to convert simple playlists but will not work for smart playlists since these are rule based.
Here is how the application looks like, the user selects one of the iTunes playlist displayed in the drop down box, then provide a name for the Zune play list and clicks the "Create Zune playlist" button:
Populating iTunes playlists dropdown box
The implementation begins with creating global variables for the paths of the iTunes library file, the location of the Zune playlists and a XElement object that hold the in-memory iTunes library file:
Dim myMusicPath As String = "C:\Documents and Settings\YOUR_USER_NAME\My Documents\My Music\"
Dim itunesLibLocation As String = myMusicPath & "iTunes\iTunes Music Library.xml"
Dim zunePlaylistsLocation As String = myMusicPath & "Zune\My Playlists\"
Dim itunesLib As XElement = XElement.Load(itunesLibLocation)
I populate the drop down list of ITunes playlists in the Form's load event using LINQ query and the Xml properties in VB 9.0. Before I show the query, please take a look at a sample section of the iTunes library file:
<dict>
<key>Name</key>
<string>Oli</string>
<key>Playlist ID</key>
<integer>1257</integer>
</dict>
What the query needs to do is to find the "Playlist ID" keys and then grab the element called "string" which is before that "key" element:
Dim itunesPlaylists = From key In itunesLib...<key> _
Where key.Value = "Playlist ID" _
Select key.ElementsBeforeSelf("string").Value
ComboBox1.DataSource = itunesPlaylists.ToList()
This is a great example of how seamless is the LINQ to Xml integration in VB 9.0, in the "From" clause I use the VB xml descendants axis to get all the "key" elements, for each element I use the "Value" property on XElement to filter only the playlist ID keys. Finally I use the "Value" extension property in VB 9.0 to select the value of the first "string" element before that "key" element. Moving between XLinq API and VB 9.0 Xml properties is very natural and intuitive.
Creating Zune playlist using Xml literals
The next step is to create the Zune playlist in the click event of button; here I'm using another cool feature of VB 9.0 called "relaxed delegates" to implement the button's click event with a sub procedure that takes no arguments although the button click event expects a method that takes two arguments (the sender and the event args). In this sub procedure I pasted existing Zune playlist and added a couple of embedded expressions to create the new playlist with updated title and media information, this playlist is saved to the Zune playlists folder:
Private Sub Button1_Click() Handles Button1.Click
Dim playlist = <?xml version="1.0"?>
<?zpl version="1.0"?>
<smil>
<head>
<meta name="Generator" content="Zune -- 1.0.5341.0"/>
<meta name="AverageRating" content="50"/>
<meta name="TotalDuration" content="7078"/>
<meta name="ItemCount" content="2"/>
<meta name="ContentPartnerListID"/>
<meta name="ContentPartnerNameType"/>
<meta name="ContentPartnerName"/>
<meta name="Subtitle"/>
<author/>
<title><%= TextBox1.Text %></title>
</head>
<body>
<seq>
<%= GetMediaElements(ComboBox1.SelectedItem) %>
</seq>
</body>
</smil>
playlist.Save(zunePlaylistsLocation + TextBox1.Text + ".zpl")
MsgBox("Done", MsgBoxStyle.Information)
End Sub
Finally I implemented GetMediaElements() function to get the media information from the iTunes playlist. The first step is to get the "Dict" element that contains the playlist information by querying the iTunes library for the selected drop down box value. Since I'm always expecting the return one dictionary "Dict" element, I'm using the extension indexer available in VB 9.0 to select the first element from the query:
Dim query = From key In itunesLib...<key> _
Where key.Value = "Playlist ID" _
AndAlso key.ElementsBeforeSelf("string").Value = ComboBox1.SelectedItem _
Select key.Parent
Dim playListdict = query(0)
The extension indexer binds to the ElementsAtOrDefault() method in LINQ sequence operators library.
Before we go to the next step of finding the file location of each MP3 in the playlist, lets take a look at how this information is stored in the library file. Each playlist has a list of the MP3 file ID in an "array" element. The following is an example of the "Oli" playlist that is stored in the "dict" element:
<dict>
<key>Name</key>
<string>Oli</string>
<key>Playlist ID</key>
<integer>1257</integer>
<key>Playlist Items</key>
<array>
<dict>
<key>Track ID</key>
<integer>384</integer>
</dict>
<dict>
<key>Track ID</key>
<integer>385</integer>
</dict>
</array>
</dict>
Each MP3 in the "array" element has a matching "dict" element somewhere else in the file, the way to find it is by locating the "key" element with the MP3 ID. For example here is the section of the file that provides details about MP3 with ID of 384:
<key>384</key>
<dict>
<key>Track ID</key>
<integer>384</integer>
<key>Name</key>
<string>Romeo And Juliet</string>
<key>Artist</key>
<string>Dire Straits</string>
<key>Album</key>
<string>Alchemy (Disc 1)</string>
<key>Location</key>
<string>file://localhost/D:/MyDocuments/My%20Music/iTunes/iTunes%20Music/Dire%20Straits/Alchemy%20(Disc%201)/1-03%20Romeo%20And%20Juliet.mp3</string>
</dict>
So the next step is to get a list of the "dict" elements for the MP3 files in the playlist so I can extract the location of each file. The query joins between the ID of the MP3 that is in the playlist "array" element and the "key" element with that value. Note that the playListdict source is pointing to the first Xml fragment above and the itunesLib.<dict>.<dict>.<key> points to the second Xml fragment:
Dim mediaList = From mediaID In playListdict.<array>...<integer>, _
key In itunesLib.<dict>.<dict>.<key> _
Where mediaID.Value = key.Value _
Select key.ElementsAfterSelf("dict")(0)
Creating variables with computed values in VB LINQ query
The last step is to return a collection of "media" elements for the Zune playlist Xml file. The mediaList variable contains a collection of the "dict" elements with each MP3 file information, the Xml property "<key>" returns all the key elements in all of the "dict" elements in the collection. The query finds the "Location" keys for all files in the playlist. The second part of the query is a string manipulation to convert the the path format that an iTunes uses to a format that is compatible with Zune. Here you can see the VB syntax for creating intermediate variables in queries:
Return From key In mediaList.<key> _
Where key.Value = "Location" _
From location = key.ElementsAfterSelf("string").Value _
From index = location.IndexOf("D:") _
From newLocation = location.Substring(index) _
From file = New Uri(Uri.UnescapeDataString(newLocation)).LocalPath _
Select <media src=<%= file %> tid=<%= Guid.NewGuid() %>/>
The query above highlights a design point that the VB language team is currently debating. You’ll note that there are several “From” clauses that introduce range variables with computed values, namely location, index, newLocation, and file. This implies that we could have rewritten the query as:
Return From key In mediaList.<key> _
Where key.Value = "Location" _
From location = key.ElementsAfterSelf("string").Value,_
index = location.IndexOf("D:"), _
newLocation = location.Substring(index), _
file = New Uri(Uri.UnescapeDataString(newLocation)).LocalPath _
Select <media src=<%= file %> tid=<%= Guid.NewGuid() %>/>
The design point is whether or not creating computed values should be a modification of the From clause or should be another clause unto itself. The argument goes that the "From" clause always introduces variables and introducing a computed value is just a special case of that. On the other end of the ring, the "From" clause with “in” suggests that another iteration or loop is introduced with the introduction of that variable. If it is a computed value, then no iteration is introduced – just a single value.
If you think that having another clause would be better, what would be good term to use? The C# language uses “Let”. There are a couple of concerns related to using “Let”:
1) Historically, VB has legacy semantics with Let. In VB6, Let was used to mean value assignment. This is not that – it has VB.NET property semantics. Using the term “Let” could confuse.
2) We’d like to keep “Let” on reserve to possibly improve compat with VB6.
Here is how the same query would be written in C#:
return from key in mediaList.Elements("key")
where key.Value == "Location"
let location = key.ElementsAfterSelf("string").Value
let index = location.IndexOf("D:")
let newLocation = location.Substring(index)
let file = new Uri(Uri.UnescapeDataString(newLocation)).LocalPath
select new XElement("media",
new XAttribute("src", file),
new XAttribute("tid", Guid.NewGuid()));
This is the full application, I hope you enjoy reading it and I hope you’ll give us some feedback on the design decision and the overall experience.
Avner
Comments
Anonymous
December 19, 2006
Pues la gente de XMLAnonymous
December 19, 2006
Pues la gente de XML ha hecho una gran demo usando los bits de Orcas Post cruzado desde cfong en wmugper...Anonymous
December 19, 2006
Pues la gente de XML ha hecho una gran demo usando los bits de Orcas Mas info en www.cesarfong.infoAnonymous
December 19, 2006
What about just using Dim instead of From? I don't really like using From again-- it makes the query look confusing to me. Return From key In mediaList.<key> _ Where key.Value = "Location" _ Dim location = key.ElementsAfterSelf("string").Value _ Dim index = location.IndexOf("D:") _ Dim newLocation = location.Substring(index) _ Dim file = New Uri(Uri.UnescapeDataString(newLocation)).LocalPath _ Select <media src=<%= file %> tid=<%= Guid.NewGuid() %>/>Anonymous
January 29, 2008
Would you like to have your iTunes lists play on the Zune? Avner Aharoni wrote an application in VisualAnonymous
January 29, 2008
Would you like to have your iTunes lists play on the Zune? Avner Aharoni wrote an application in VisualAnonymous
January 25, 2009
Hey guys, I found a VB 9.0 sample on using Linq to read the iTunes XML library and I am trying to convert