Way back in October 2007, I wrote up a few posts (here and here) on my experiments with the Windows Media Player object model. The problem I was trying to solve was that, when I had a playlist set to “shuffle,” WMP would break up songs that should always play together (for example, Jackson Browne’s “The Load-Out” and “Stay”, or Pink Floyd’s “Brain Damage” and “Total Eclipse”). In those posts, I worked around this by building my own shuffler which would emit a new randomized playlist which preserved those links, and then playing that playlist. A few months later, after I bought my Zune 80, I updated the program to copy the playlist to a ZPL format, since the format was essentially identical to WPL.
Although this was a good investigation into the WMP object model, I’ve never been happy with the program. First of all, for the program to work, I had to add metadata to the actual music tracks, and anything could come along and blow that metadata away. Second, the tracks had to already be adjacent in the parent playlist for it to work at all. Third, I had to bring up the WMP just to shuffle a playlist, which is pretty heavyweight. Finally, since I mostly care about Zune playlists these days and not WMP at all, it seemed the height of hackery to bring up the WMP to generate something for Zune (since the Zune object model is not public).
So, last week, after seeing my metadata blown away again when I carelessly updated my tracks after upgrading to Windows 7, I decided to work on a more permanent solution. Why bring up the WMP at all (or Zune, for that matter), when the playlists themselves are just XML, and the track just entries into those?
After some experiments with parsing the XML by following the parent/child chain, which seemed kludge to me, I came across a post by Avner in which he discusses porting iTunes playlists to Zune, leveraging LinQ to help with the data parsing. I realized right away that I could do something very similar in this case, and came up with what I think is a pretty elegant solution.
So here’s the plan: the application will:
(1) Load in a playlist that the user chooses
(2) Allow the user to rearrange the playlist
(3) Allow the user to link certain tracks together (and unlink them)
(4) Shuffle the playlist on command, preserving the linkages
(5) Allow the user to change the title of the playlist
(6) Allow the user to cave the playlist (or a copy of it)
(7) And, most important, persist the linkage data between sessions in such a way that the information won’t get blown away.
Let’s get started!
The form
Create a Windows Form Application (about 430 x 300) containing the following:
(1) A ListBox, about 290 x 180 and positioned on the left side, with the SelectionMode property set to “MultiExtended” and the DrawMode set to “OwnerDrawFixed.”
(2) Above that, a Label and EditBox, with the Label text set to “&Title”.
(3) To the right of those, five Buttons going down the right side, labeled “Link”, “Unlink”, “Move Up”, “Move Down”, and “Shuffle”.
(4) A MenuStrip. Select it, and on the right side of the strip, click the tiny right-pointing triangle icon and choose “Insert Standard Items” from the resulting popup. Now, go back and remove all of the items except File (Open, Close, Save, Save As, Exit) and Help (About). The Edit and Tools menu can be completely removed; we won’t be using those.
(5) An OpenFileDialog and a SaveFileDialog. The Filter property of both should be set to “Zune playlists|*.zpl” (without the quotes).
In my example, I also disabled the Maximize box in the form, added an icon and title to the form and application, etc., but those aren’t strictly necessary for the exercise.
The Zune playlist
The format of a Zune playlist (version 2.0) looks like this (memorize this, it will be useful later):
<smil>
<head>
<guid> (some guid)</guid>
<meta name=”creatorId” content=”(some guid)“ />
<meta name=”Subtitle” />
<meta name=”ContentPartnerName” />
<meta name=”ContentPartnerNameType” />
<meta name=”ContentPartnerListID” />
<meta name=”ItemCount” content=”(number of tracks)” />
<meta name=”TotalDuration” content=” (total duration) ” />
<meta name=”AverageRating” content=”(whatever the average rating is)” />
<meta name=”Generator” content=”Zune — 4.0.740.0″ />
<title> (some title) </title>
</head>
<body>
<seq>
<media src=”(file path of the track on disk)” serviceId=”{BF0A0E00-0100-11DB-89CA-0019B92A3933}” albumTitle=”(album title)” albumArtist=”(artist)” trackTitle=”(name of the track)” trackArtist=”(track artist)” duration=”(track duration)” />
<media … /> (etc… one for each track)
</seq>
</body>
</smil>
Code for the controls
The rest of this post will be organized based on the controls, starting from the top.
The Form
The only code associated with the form itself is the event handler for Load, a few member variables, and a helper class. Let’s take those in reverse order:
The helper class, xmlMediaEntry
We’ll be reading the Zune playlist using the XDocument class, and then we’ll be using LinQ to retrieve individual <media>…</media> tag pairs representing each track. These will be handed to us as XElements, and as we will want to add each of the tracks to the listbox, we’ll want to wrap these in an object that will expose the proper information to the ListBox – namely, a human-readable string. We will also want to have a way to link a track to another track, and we’ll be using a doubly-linked list to do this. So:
Class xmlMediaEntry
Public xmlElem As XElement
Public PrevLinkedElement As xmlMediaEntry = Nothing
Public NextLinkedElement As xmlMediaEntry = Nothing
That code allows us to cache the XElement and also point to other elements of this class backwards and forward. The header node for a chain of linked songs will have PrevLinkedElement remaining as Nothing; the tail node will have NextLinkedElement remain as Nothing; songs in the middle of the chain will have both of those set to some other node.
The following constructor allows us to initialize the object with the XElement to be cached.
Sub New(ByVal xelem As XElement)
xmlElem = xelem
End Sub
This code, exposed publically as a shared method, allows us to create a canonical (and readable) way to refer to a track in an XElement. To use one of my previous examples, the resulting value might be “The Load-Out (Jackson Browne)”:
Public Shared Function CreateMediaString(ByVal elem As XElement) As String
Return elem.Attribute(“trackTitle”).Value & _
” (“ & elem.Attribute(“trackArtist”).Value & “)”
End Function
I can then leverage that function to override the ToString for the xmlMediaEntry:
Public Overrides Function ToString() As String
Return CreateMediaString(xmlElem)
End Function
End Class
And that’s all I need in that class. Once I add an instance of it to the ListBox, it will show up with nice readable text, and I’ll be able to navigate between it and any other linked songs (if any).
Member Variables
I’ll need a few variables to keep track of the state of the file, and these should all be self-explanatory:
Private FileLoaded As Boolean = False ‘ Do we have a file loaded into the listbox?
Private FileChanged As Boolean = False ‘ Has the playlist changed since it was last loaded/saved?
Private FilePath As String ‘ Where was the file last loaded from/saved to?
Private FileDirectory As String ‘ What directory was the file last loaded from/saved to?
We’ll also need to keep track of some of the playlist’s metadata. Each playlist has a GUID associated with it (if you don’t know what a GUID is, don’t worry about it – think of it as a unique identifier); if we do a “Save As” to a different file, we’ll want a different GUID. The user might also change the title of the playlist, and we should cache our creatorId format since it might be unique to the user’s situation:
Private playlistGuid As String ‘ What is the GUID of this playlist?
Private playlistTitle As String ‘ What is the title of this playlist?
Private playlistCreatorId As XElement ‘ What is the creator information of this playlist?
Events
The Form only has two events to handle that we care about – the Load event, and the FormClosing event. For the Load event, we’ll want to make sure that our menu items and buttons are enabled appropriately, and that we appropriately generate random numbers later on:
Private Sub VBShuffleForm_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
Microsoft.VisualBasic.Randomize()
0 comments
Be the first to start the discussion.