Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Integrating Pandora API with MediaLibrary on Windows Phone 7

DZone's Guide to

Integrating Pandora API with MediaLibrary on Windows Phone 7

·
Free Resource

I blogged a while ago about a hidden endpoint in Pandora that allows developers to query the Pandora service for artist information. The service itself is great, but after a short conversation with Justin Angel today, I decided to take this a bit further and integrate it with MediaLibrary - the core class, that allows the use (partial) of existing media content on the device in a third-party application.

The first thing you need to be aware is the format and the information available through the Pandora service. The data received after a query is a XML-formatted document of a similar structure:

(Click on image to enlarge)

It is fairly easy to see that all data returned (and that can be used here) is:

  • Artist album art
  • Artist bio
  • Similar artists 

And that's where I am able to bring additional value to the media playing application by tying existing content to additional metadata that once again can be linked back to other existing content (e.g. similar artists).

The data model

The first thing I did was creating a ComplexArtist class, that serves as the main data model in my case. I wish the Artist class (provided by Microsoft.Xna.Framework.Media) was enough, but it is not. More than that, it is not abstract, which renders it almost useless in my situation. Almost.

[XmlRoot(ElementName = "artistExplorer")]
public class ComplexArtist : ViewModelBase
{
    [XmlAttribute(AttributeName = "artUrl")]
    public string ImageUrl { get; set; }

    private string _name;
    [XmlAttribute(AttributeName = "name")]
    public string Name
    {
        get
        {
            return _name;
        }
        set
        {
            if (_name != value)
            {
                _name = value;
                RaisePropertyChanged("Name");
            }
        }
    }

    [XmlAttribute(AttributeName="detailUrl")]
    public string PandoraUrl { get; set; }

    private string _bio;
    [XmlElement(ElementName="bio")]
    public string Bio
    {
        get
        {
            return _bio;
        }
        set
        {
            if (_bio != value)
            {
                _bio = value;
                RaisePropertyChanged("Bio");
            }
        }
    }

    private BitmapImage _image;
    [XmlIgnore()]
    public BitmapImage Image
    {
        get
        {
            return _image;
        }
        set
        {
            if (_image != value)
            {
                _image = value;
                RaisePropertyChanged("Image");
            }
        }
    }

    private List<ComplexArtist> _similar;
    [XmlArrayItem(ElementName="artist")]
    [XmlArray(ElementName = "similar")]
    public List<ComplexArtist> Similar
    {
        get
        {
            return _similar;
        }
        set
        {
            if (_similar != value)
            {
                _similar = value;
                RaisePropertyChanged("Similar");
            }
        }
    }
}

You might be wondering - what is ViewModelBase here? I am using MVVM light to organize the MVVM architecture of the application. ViewModelBase allow me to have a quick abstraction layer over the notification system (in case you were thinking of manual INotifyPropertyChanged implementation), so I can easily manage binding via RaisePropertyChanged.

Given the structure of the XML document that gives me artist information, I am loading all kinds of data by simply deserializing the retrieved document. That is, in my opinion, the most optimal way to go instead of manual node reading. The only item I am not deserializing here is the album art, due to the fact that it is not passed as an image but rather as a URL. I will download the image later.

Data downloader

I am downloading data via a static Loader class that was designed to either load data for the entire list of artists or just for a specific artist. This is introduced because the application itself needs to retrieve artist information in two cases:

  • For the entire Artist collection present on the phone
  • For similar artists (returned by Pandora) that by default do not carry the bio and a subset of similar artists applied to their own entity

Inside the Loader class I have an enum and a property that define which behavior should be used:

private enum DownloadType
{
    FullSet,
    Selective
}

private static DownloadType type;

There are two LoadArtists methods defined, for each of the situations defined above:

public static void LoadArtists()
{
type = DownloadType.FullSet;
MediaLibrary library = new MediaLibrary();
foreach (Artist a in library.Artists)
{
DownloadAdditionalData(a);
}
}

public static void LoadArtists(ComplexArtist artist)
{
type = DownloadType.Selective;
DownloadAdditionalData(artist);
}

The first case is when I go through the existing collection of Artist instances in the media library and for each registered entity I am downloading additional data. The second case is when a ComplexArtist instance is passed and this means that I don't even need to touch the main library for now.

Setting the type here will be used later for verification. Here is how I download the data:

private static void DownloadAdditionalData(object artist)
{
    WebClient client = new WebClient();
    client.DownloadStringCompleted += (s, e) => HandleDownloadCompleted(e, artist);
    string name;

    if (type == DownloadType.FullSet)
        name = ((Artist)artist).Name;
    else
        name = ((ComplexArtist)artist).Name;

    client.DownloadStringAsync(new Uri(Constants.PandoraURL + name));
}

That's where the type was needed! Casting to either Artist or ComplexArtist is determined by the download type to avoid problems with custom types.

Once the download completes, I am making sure that no error was returned and I am instantly deserializing the content:

static void HandleDownloadCompleted(DownloadStringCompletedEventArgs e, object artist)
{
    ComplexArtist cArtist = new ComplexArtist();

    if (!e.Result.Contains("<error"))
    {
        StringReader reader = new StringReader(e.Result);

        XmlSerializer serializer = new XmlSerializer(typeof(ComplexArtist));
        cArtist = (ComplexArtist)serializer.Deserialize(reader);
    }

    if (!string.IsNullOrEmpty(cArtist.ImageUrl))
    {
        cArtist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri(cArtist.ImageUrl));
    }
    else
    {
        cArtist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri("/Images/unknown.png", UriKind.Relative));
    }

    if (type == DownloadType.FullSet)
    {
        cArtist.Name = ((Artist)artist).Name;
        ModelLocator.MainStatic.Artists.Add(cArtist);
    }
    else
    {
        ModelLocator.MainStatic.CurrentArtist = cArtist;
    }
}

Notice that since I have no idea what artist is going to be used, I am passing it as an object - either ComplexArtist or Artist will be there. I am also downloading the album art for artists that were found on Pandora. If the artist was not found through the API, but is still present in the library, I am assigning it a "not found" image that is stored locally.

Notice one very important thing - to avoid confusion later on when I will integrate the application even deeper with the existing media library, if the artist was taken from the existing library, I make sure that I set its name to the one from the media library and not the one deserialized from the Pandora document.

Displaying and managing the obtained data

The next part is interesting because I am using the so called ModelLocator. It is a ViewModelLocator implementation (once again involving MVVM Light) and I use it for easier binding inside XAML (for each component page):

public class ModelLocator
{
    private static MainViewModel _main;

    public ModelLocator()
    {
        MainStatic.Artists = new System.Collections.ObjectModel.ObservableCollection<ComplexArtist>();
    }

    public static MainViewModel MainStatic
    {
        get
        {
            if (_main == null)
            {
                CreateMain();
            }

            return _main;
        }
    }

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
        "CA1822:MarkMembersAsStatic",
        Justification = "This non-static member is needed for data binding purposes.")]
    public MainViewModel BoundMainViewModel
    {
        get
        {
            return MainStatic;
        }
    }

    public static void CreateMain()
    {
        if (_main == null)
        {
            _main = new MainViewModel();
        }
    }

    public static void Cleanup()
    {

    }
}

The MainViewModel is the ViewModel that holds the data itself that is used for binding:

public class MainViewModel: ViewModelBase
{
    private ObservableCollection<ComplexArtist> _artists;
    public ObservableCollection<ComplexArtist> Artists
    {
        get
        {
            return _artists;
        }
        set
        {
            if (_artists != value)
            {
                _artists = value;
                RaisePropertyChanged("Artists");
            }
        }
    }

    private ComplexArtist _currentArtist;
    public ComplexArtist CurrentArtist
    {
        get
        {
            return _currentArtist;
        }
        set
        {
            if (_currentArtist != value)
            {
                _currentArtist = value;

                if (_currentArtist.Similar != null)
                {
                    foreach (ComplexArtist artist in ModelLocator.MainStatic.CurrentArtist.Similar)
                    {
                        if (!string.IsNullOrEmpty(artist.ImageUrl))
                        {
                            artist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri(artist.ImageUrl));
                        }
                        else
                        {
                            artist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri("/Images/unknown.png", UriKind.Relative));
                        }
                    }
                }

                RaisePropertyChanged("CurrentArtist");
            }
        }
    }
}

Notice the RaisePropertyChanged that is also used here to notify the view about any changes that might occur (either to the current artist or the entire ComplexArtist collection that is based on the MediaLibrary set). This is pretty much it for the core of the application. Let's take a look at the default application UI:

All album art here is fetched from the Pandora servers. The idea is pretty easy here - I have a ListBox with a custom ItemTemplate that is bound to the Artists collection in MainViewModel. That being said, here is the XAML:

<phone:PhoneApplicationPage 
    x:Class="LightPlayer.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True" x:Name="MainWindow"
    DataContext="{Binding BoundMainViewModel, Source={StaticResource Locator}}">

    <Grid x:Name="LayoutRoot" Background="Transparent">
            <ListBox x:Name="ArtistList" ItemsSource="{Binding Artists}" SelectionChanged="ListBox_SelectionChanged">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="10,0,0,20">
                            <Image HorizontalAlignment="Left" Height="120" Width="120" Source="{Binding Image}"></Image>
                            <TextBlock Width="300" TextWrapping="Wrap" Margin="140,40,0,0" Text="{Binding Name}"></TextBlock>
                    </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
    </Grid>
</phone:PhoneApplicationPage>
The confusing moment here is the DataContext for the main page. What exactly is it? Well, in App.xaml I declared my ViewModelLocator instance (represented by ModelLocator.cs) and here I am defining
  1. The ViewModel to bind to
  2. The ModelLocator instance to bind to
<Application 
    x:Class="LightPlayer.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"       
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:local="clr-namespace:LightPlayer">

    <!--Application Resources-->
    <Application.Resources>
        <local:ModelLocator x:Key="Locator"></local:ModelLocator>
    </Application.Resources>

    <Application.ApplicationLifetimeObjects>
        <!--Required object that handles lifetime events for the application-->
        <shell:PhoneApplicationService 
            Launching="Application_Launching" Closing="Application_Closing" 
            Activated="Application_Activated" Deactivated="Application_Deactivated"/>
    </Application.ApplicationLifetimeObjects>

</Application>

Notice that I am also declaring the local namespace, that is used to define the location of ModelLocator.

When the main page loads, I also need to load the list of artists and populate the existing collection in my ViewModel. To do this, I have the MainPage_Loaded event wired at startup and I am loading new data only if the collection hasn't been populated yet:

void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    if (ModelLocator.MainStatic.Artists.Count == 0)
    {
        Loader.LoadArtists();
    }
}

You might ask - why do I need to check whether the collection is populated or not? Of course it is not, since the page just loaded! Well yes and no. Yes, if the application just started. No, if the user clicked on an artist, viewed the details and then returned back to the main page. The Loaded event will be fired then too, so I need to make sure that I won't re-populate the existing collection.

When the selection in the ListBox changes, I am getting the ComplexArtist instance and setting it to the CurrentArtist in the ViewModel:

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (ArtistList.SelectedItem != null)
    {
        ComplexArtist artist = (ComplexArtist)ArtistList.SelectedItem;
        ModelLocator.MainStatic.CurrentArtist = artist;

        NavigationService.Navigate(new Uri("/ArtistDetails.xaml", UriKind.Relative));
    }
}

Once it is set, I am loading the page that will show me the artist details, that is bound to the CurrentArtist instance. Its structure is defined by the XAML code below:

<phone:PhoneApplicationPage 
    x:Class="LightPlayer.ArtistDetails"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignHeight="768" d:DesignWidth="480"
    shell:SystemTray.IsVisible="True"
    DataContext="{Binding BoundMainViewModel, Source={StaticResource Locator}}">

    <StackPanel DataContext="{Binding CurrentArtist}"  x:Name="LayoutRoot" Background="Transparent">
        <TextBlock Text="{Binding Name}" Margin="20,20,20,20" FontSize="36"></TextBlock>
        <Grid Margin="20,0,20,20" Height="300">
            <Image Source="{Binding Image}" Height="128" Width="128" Margin="0,0,300,160"></Image>
            <ScrollViewer Width="280" Margin="140,0,0,0">
                <TextBlock TextWrapping="Wrap" Text="{Binding Bio}"></TextBlock>
            </ScrollViewer>
        </Grid>
        <TextBlock Text="Similar Artists" Margin="20,0,0,0"></TextBlock>
        <StackPanel>
            <ListBox x:Name="SimilarList" ItemsSource="{Binding Similar}" Height="300" Margin="20,20,20,20" SelectionChanged="SimilarList_SelectionChanged">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <Grid Margin="10,0,0,20">
                            <Image HorizontalAlignment="Left" Height="120" Width="120" Source="{Binding Image}"></Image>
                            <TextBlock Width="300" TextWrapping="Wrap" Margin="140,40,0,0" Text="{Binding Name}"></TextBlock>
                        </Grid>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </StackPanel>
    </StackPanel>
</phone:PhoneApplicationPage>

Nothing too complicated here. I have some controls that are bound to respective properties of the CurrentArtist instance. As I showed above, each ComplexArtist instance has a list of similar artists (that are also instances of CurrentArtist) that also have an image associated with them (album art). I decided not to load those images the moment similar artists are downloaded, but I can surely download the images when similar artists are displayed in the context of their parent ComplexArtist instance.

So when the details page is loaded, I am initiating a download process:

if (ModelLocator.MainStatic.CurrentArtist.Similar != null)
{
    foreach (ComplexArtist artist in ModelLocator.MainStatic.CurrentArtist.Similar)
    {
        if (!string.IsNullOrEmpty(artist.ImageUrl))
        {
            artist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri(artist.ImageUrl));
        }
        else
        {
            artist.Image = new System.Windows.Media.Imaging.BitmapImage(new Uri("/Images/unknown.png", UriKind.Relative));
        }
    }
}

The list of similar artists is also held by a ListBox, so once the selection changes, I want to switch the current artist:

private void SimilarList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (SimilarList.SelectedItem != null)
    {
        bool found = false;
        ComplexArtist cArtist = (ComplexArtist)SimilarList.SelectedItem;

        foreach (ComplexArtist artist in ModelLocator.MainStatic.Artists)
        {
            if (artist.Name == cArtist.Name)
            {
                ModelLocator.MainStatic.CurrentArtist = artist;
                found = true;
                break;
            }
        }

        if (!found)
        {
            Loader.LoadArtists(cArtist);
        }
    }
}

I am looking through the existing artist list to find whether I already have data on him (to avoid downloading redundant information). If the artist is found, I am simply taking that instance and assigning him to the CurrentArtist property in my ViewModel. If not, I am downloading additional info about him from Pandora.

If you would like to download the sample application (to avoid re-constructing everything I showed above), you can do so by clicking here.

Topics:

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}