19 August, 2010

Now Playing with MVVM – Fetching Sample Album / Artist / Song Data

This is part 5 of a series of posts in which I attempt to reproduce the Zune Now Playing interface using the MVVM pattern within WPF. An introduction to the series and table of contents can be found in the first post of this series.

To this point in my Now Playing with MVVM series I’ve been relying on static sample data that I’ve hardcoded into my application. It served me well while I was just working with half a dozen album covers, but now that I’m getting closer to implementing an interface that needs to stretch full screen, I’ll need data that is more dynamic and more interesting to look at.

I don’t use them myself, but a few of my friends and family use the Rhapsody service. Some Binging lead me to a page on the Rhapsody site where they offer up a selection of RSS and OPML feeds of top albums, artists, tracks, new releases, and more. This is a great solution for me, not only because of the amount and depth of data, but because by retrieving it over the Internet, I’ll be forced to deal with binding to data that is not immediately available.

The service I’ve chosen to work with is the New Releases feed. This should be a good source of ever-changing data that pulls from a different genres and album types. If you click directly on the feed URL with a browser, it will display a formatted web page by doing a client side XSL transform, so I used UltraEdit to open the raw feed data to examine the RSS formatting.

<rss version="2.0" xmlns:rhap="http://feeds.rhapsody.com/dtds/">
<channel>
<title>New Releases on Rhapsody Online</title>
<link>http://www.rhapsody.com?rws=%2Fnew-releases.rss</link>
<description>New Releases on Rhapsody Online</description>
<category />
<language>en</language>
<ttl>720</ttl>
<pubDate>Thu, 19 Aug 2010 02:34:12 -0700</pubDate>
<image>
<url>http://static.realone.com/rotw/images/logo_rhapsody_113x22.gif</url>
<title>New Releases on Rhapsody Online</title>
<link>http://www.rhapsody.com?rws=%2Fnew-releases.rss</link>
<description>New Releases on Rhapsody Online</description>
</image>
<item>
<title>God Willin' &amp; The Creek Don't Rise - Ray LaMontagne</title>
<link>http://www.rhapsody.com/goto?rcid=alb.39896780&amp;variant=play&amp;rws=%2Fnew-releases.rss</link>
<category>Adult Alternative</category>
<pubDate>Tue, 17 Aug 2010 13:17:19 -0700</pubDate>
<source url="http://www.rhapsody.com?rws=%2Fnew-releases.rss">New Releases on Rhapsody Online</source>
<guid isPermaLink="false">alb.39896780</guid>
<rhap:rcid xmlns:rhap="rhap">alb.39896780</rhap:rcid>
<rhap:artist xmlns:rhap="rhap">Ray LaMontagne</rhap:artist>
<rhap:artist-rcid xmlns:rhap="rhap">art.6479139</rhap:artist-rcid>
<rhap:price xmlns:rhap="rhap">9.99</rhap:price>
<rhap:album xmlns:rhap="rhap">God Willin' &amp; The Creek Don't Rise</rhap:album>
<rhap:album-rcid xmlns:rhap="rhap">alb.39896780</rhap:album-rcid>
<rhap:album-art xmlns:rhap="rhap">http://image.listen.com/img/170x170/5/1/4/1/2081415_170x170.jpg</rhap:album-art>
<rhap:album-release-date xmlns:rhap="rhap">2010-08-17 13:17:19.0</rhap:album-release-date>
<rhap:album-original-release-date xmlns:rhap="rhap">2010-08-17 13:17:19.0</rhap:album-original-release-date>
<rhap:album-is-available xmlns:rhap="rhap">true</rhap:album-is-available>
<rhap:explicit xmlns:rhap="rhap">false</rhap:explicit>
<rhap:album-type xmlns:rhap="rhap">main-release</rhap:album-type>
<rhap:play-href xmlns:rhap="rhap">http://www.rhapsody.com/goto?rcid=alb.39896780&amp;variant=play&amp;rws=%2Fnew-releases.rss</rhap:play-href>
<rhap:data-href xmlns:rhap="rhap">http://www.rhapsody.com/goto?rcid=alb.39896780&amp;variant=data&amp;rws=%2Fnew-releases.rss</rhap:data-href>
<description>
<![CDATA[God Willin' & The Creek Don't Rise - Ray LaMontagne]]>
</description>
</item>
.Net 3.5 introduced the System.ServiceModel.Syndication namespace which aids in the creation and parsing of RSS and ATOM feeds. This library will make the task of hydrating the Album/Artist/Song object model very easy. To get at the list of items in the feed, you load them from an XMLReader that you’ve passed the RSS URI to:
List<Album> Albums = null;
Uri myFeedUri = new Uri("http://feeds.rhapsody.com/new-releases.rss");
using(XmlReader myReader = XmlReader.Create(myFeedUri.AbsoluteUri)) {
SyndicationFeed myFeed = SyndicationFeed.Load(myReader);
if(myFeed != null) {
Albums = new List<Album>(myFeed.Items.Count());
foreach(SyndicationItem myItem in myFeed.Items) {
//work with the next item
}
}
}


The SyndicationItem entity makes the standard RSS elements available to the client, and it also provides a way to access vendor-specific element extensions. The Rhapsody feeds make album, artist, track, and other metadata available through a custom namespace as part of each SyndicationItem. SyndicationItem makes these additional elements available in a collection called ElementExtensions. There is a discussion of how to access these items on Stack Overflow. Here is how I grabbed the album art from the new releases feed shown above.


foreach(SyndicationItem myItem in myFeed.Items) {
Album anAlbum = new Album
{
ID = myItem.Id,
Name = myItem.Title.Text,
Category = myItem.Categories[0].Name,
ArtworkUrl = myItem.ElementExtensions.First(
albumext => albumext.OuterNamespace == "rhap" &&
albumext.OuterName == "album-art")
.GetObject<string>()
};
}


I qualified the OuterNamespace as a precaution against some unforeseen change in the feed where another element with the name “album-art” is introduced into a different namespace inside each item.


I’d also like to build the track list for each album while hydrating them from the new releases feed. Rhapsody offers this capability by accessing the following URI: http://www.rhapsody.com/goto?rcid=<rhap:rcid>&variant=rss-feed. This makes fetching the song metadata a snap:


foreach(SyndicationItem myItem in myFeed.Items) {
Album anAlbum = new Album
{
ID = myItem.Id,
Name = myItem.Title.Text,
Category = myItem.Categories[0].Name,
ArtworkUrl = myItem.ElementExtensions.First(
albumext => albumext.OuterNamespace == "rhap" &&
albumext.OuterName == "album-art")
.GetObject<string>()
};
anAlbum.Songs = GetSongsForAlbum(anAlbum);
}

private List<Song> GetSongsForAlbum(Album aParentAlbum) {
List<Song> Songs = null;
string rssURL = string.Format("http://www.rhapsody.com/goto?rcid={0}&variant=rss-feed", aParentAlbum.ID);
Uri myFeedUri = new Uri(rssURL);
using(XmlReader myReader = XmlReader.Create(myFeedUri.AbsoluteUri)) {
SyndicationFeed myFeed = SyndicationFeed.Load(myReader);
if(myFeed != null) {
Songs = new List<Song>(myFeed.Items.Count());
foreach(SyndicationItem myItem in myFeed.Items) {
Song aSong = new Song
{
Album = aParentAlbum,
Artist = aParentAlbum.Artist,
Name = myItem.ElementExtensions.First(
songext => songext.OuterNamespace == "rhap" &&
songext.OuterName == "track")
.GetObject<string>()
};
Songs.Add(aSong);
}
}
}
return Songs;
}


Speeding things up


This does the job just fine, but there’s a problem. At the time of this writing, the new releases feed has 144 items in it, meaning that 145 separate HTTP calls are required in order to load the whole media graph. This can be very slow because the requests are made sequentially on the same thread. Let’s introduce a little parallelism into this process.[NOTE: See update below. This code is not thread safe]


using(XmlReader myReader = XmlReader.Create(myFeedUri.AbsoluteUri)) {
SyndicationFeed myFeed = SyndicationFeed.Load(myReader);
if(myFeed != null) {
Albums = new List<Album>(myFeed.Items.Count());
//foreach(SyndicationItem myItem in myFeed.Items)
Parallel.ForEach(myFeed.Items, myItem => {
Album anAlbum = new Album {
ID = myItem.Id,
Name = myItem.Title.Text,
Category = myItem.Categories[0].Name,
Artist = new Artist {
ID = <snip/>,
Name = <snip/>
},
ArtworkUrl = <snip/>
};
anAlbum.Songs = GetSongsForAlbum(anAlbum);
});
}
}

Ok, much better. By taking advantage of the Parallel tasks library in System.Threading.Tasks.Parallel, I’ve cut the load time in half on my development box. But I think I can do better. What if I load the songs collection for each album on a background thread? [NOTE: See update below. This code is not thread safe]


Albums = new List<Album>(myFeed.Items.Count());
Parallel.ForEach(myFeed.Items, myItem => {
Album anAlbum = new Album
{
ID = myItem.Id,
Name = myItem.Title.Text,
Category = myItem.Categories[0].Name,
Artist = <snip/>
ArtworkUrl = <snip/>
};
BackgroundWorker aWorker = new BackgroundWorker();
aWorker.DoWork += (aSender, e) => {
Album myAlbum = e.Argument as Album;
if(myAlbum != null)
myAlbum.Songs = GetSongsForAlbum(anAlbum);
};
aWorker.RunWorkerAsync(anAlbum);
Albums.Add(anAlbum);
});

Wow, much better. The albums collection is returned almost immediately and the songs fill themselves in on a background thread. This is exactly the sort of experience I expect from a WPF application.


UPDATE: While integrating this code back into the Now Playing application, I found that my Parallel.ForEach loop was not returning. After some research, I determined it was caused by concurrent access to the List<Album>, which is not thread safe. The good news is that the .Net framework 4.0 includes a ConcurrentDictionary class, which lends itself well to my use case.


Download Project

No comments:

Post a Comment