Build a Yahoo Music Mashup with Adobe AIR

When Yahoo released the Yahoo Music application programming interface (API), I was immediately all over it! What could be cooler than playing with a data source containing musicians, bands, album listings, and videos? In this article, we’ll build an Adobe AIR application that takes advantage of this rich source of data.

The trick was getting hold of a list of my favorite artists. I could have used a search field, but that seemed kind of old school. Then it dawned on me that my local iTunes library contained all of the artists that I like; better yet, Adobe AIR would allow me to access that iTunes library, so I could take my web programming skills and use them to build a fun desktop app. So, I decided to create a mashup of my iTunes library and the Yahoo Music API.

Along the way, I learned a lot about the AIR File API and AIR’s SQLite database support, which I used to maintain a list of every Yahoo video retrieved between invocations of the app. I also learned a great deal about how to launch native sub-windows in AIR – especially managing progress bars. As you’ll soon see, these progress bars are necessary because the process of scanning iTunes and then requesting videos of each artist from Yahoo is time-consuming.

Pay attention – there will be a quiz at the end! The first 100 people to complete the quiz will win a copy of my book, Getting Started With Flex 3, delivered to their front door for FREE, thanks to Adobe. You can also download the book in PDF format for free for a limited time.

Setting up Shop

Let’s dig right in and I’ll walk you through how I built this application. Previous Adobe AIR tutorials here on SitePoint have made use of HTML, CSS and JavaScript. However, you can also create AIR applications using Adobe Flex – and that’s what we’ll be doing here. Download the code archive for this application (.zip, 2.2 MB) if you’d like to have all the code listings readily accessible.

Our first step is to set up Adobe Flex Builder 3. You can install a trial edition at no cost, or if you prefer, you can run the examples in this article by downloading the Flex software development kit (SDK) and the Air SDK. However, I’d definitely recommend downloading the Flex Builder integrated development environment (IDE), as it makes building both Flex and AIR applications a lot easier.

For this article, I’ll present the video timelines application in two parts. In the first part we’ll cover the iTunes reader that accesses the list of artists from the iTunes music catalog XML file. The second part will cover how to build an application that looks up the artists from Yahoo.

Note: This code does not currently work on Linux systems. Although AIR runs on Linux, iTunes currently does not.

Accessing the iTunes XML Database

Apple’s iTunes media player stores its complete list of songs in an XML file under the Documents directory on a Mac (My Documents on Windows). Retrieving this list of artists is as simple as opening the XML file and locating the right tags. Because this process can take a fair bit of time to perform, I’ve broken it down into two steps. The first part of the process involves opening the file and reading in all the artist tags from every track. In the second step, we’ll build the list of artists along with the tracks associated with each artist; this is done in small chunks with the aid of a timer.

As the code below shows, all of this is carried out in the Artists Singleton object. A Singleton is an object that has only one instance in the application; you access that instance by using the static instance method. I chose to make our Artists object a Singleton, because we’ll only want to have one music library open at a time in this application. The Artists Singleton object is shown below:

File: Artists.as 
package com.videomashup
{
 import flash.events.Event;
 import flash.events.EventDispatcher;
 import flash.events.TimerEvent;
 import flash.filesystem.File;
 import flash.filesystem.FileMode;
 import flash.filesystem.FileStream;
 import flash.utils.ByteArray;
 import flash.utils.Timer;

 public class Artists extends EventDispatcher
 {
   private static var _artistsInstance:Artists = new Artists();
   
   public static function get instance() : Artists {
     return _artistsInstance;
   }
   
   private var _artists:Object = {};
   private var _dicts:Array = [];
   private var _dictsProcessed:uint = 0;
   private var _dictsTotal:uint = 0;
   private var _timer:Timer = null;
   private var _artistCount:uint = 0;
   
   public function get artists() : Array {  
     var out:Array = [];
     for( var k:String in _artists )
       out.push( k );
     return out.sort();
   }
   
   public function get count() : uint {
     return _artistCount;
   }
   
   public function get bytesLoaded() : uint {
     return _dictsProcessed;
   }
   
   public function get bytesTotal() : uint {
     return _dictsTotal;
   }
   
   public function Artists()
   {
     super();
   }
     
   public function startReader() : void {
     var xmlFile:File = new File( File.userDirectory.nativePath+'/Music/iTunes/iTunes Music Library.xml' );
     var bytes:ByteArray = new ByteArray();

     var fileStream:FileStream = new FileStream();
     fileStream.open( xmlFile, FileMode.READ );
     var doc:XML = XML(fileStream.readUTFBytes(fileStream.bytesAvailable));
     fileStream.close();
     
     for each( var dict:XML in doc..dict.(key[2]=='Artist') )
       _dicts.push( dict );
     _dictsTotal = _dicts.length;

     _timer = new Timer( 100 );
     _timer.addEventListener(TimerEvent.TIMER,onTimer);
     _timer.start();      
   }

   private function onTimer( event:Event ) : void {
     if ( _dicts.length > 0 ) {
       for( var i:int = 0; i < 50; i++ ) {
         var dict:XML = _dicts.pop() as XML;
         if ( dict == null || _dicts.length == 0 ) {
           dispatchEvent( new ArtistEvent( ArtistEvent.ITUNES_ANALYSIS_COMPLETE ) );
           return;
         }
         _dictsProcessed++;
         var art:XML = dict.string[2];
         if ( art != null ) {
           var artName:String = String( art );
           if ( _artists[ artName ] == null ) {
             _artists[ artName ] = { name:artName, count: 1 };
             _artistCount++;
           } else
             _artists[ artName ].count += 1;
         }
       }
 
       dispatchEvent( new ArtistEvent( ArtistEvent.ARTIST_LIST_CHANGE ) );
     }
   }
 }
}

The two primary methods used here are the startReader method and the onTimer method. The startReader method parses the iTunes XML file and extracts all of the artist tags from the XML. It then creates a timer that will handle the asynchronous processing of the tags. The callback for that timer is the onTimer method.

The onTimer method processes a set amount of tags, then dispatches the ARTIST_LIST_CHANGE event to notify our application that there may be some new artists that have been added to this list. This is a custom event that’s dispatched by the Artists Singleton to notify the user that the list of artists is growing and, eventually, has been processed.

When all the processing is complete, the method sends the ITUNES_ANALYSIS_COMPLETE message. Both of these messages are defined below:

File: ArtistEvent.as  
package com.videomashup  
{  
 import flash.events.Event;  
 
 public class ArtistEvent extends Event  
 {  
   public static const ITUNES_ANALYSIS_COMPLETE:String = 'ITUNES_ANALYSIS_COMPLETE';  
     
   public static const ARTIST_LIST_CHANGE:String = 'ARTIST_LIST_CHANGE';  
     
   public function ArtistEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)  
   {  
     super(type, bubbles, cancelable);  
   }  
     
 }  
}

The MXML code for our iTunes processing application looks like this:

File: iTunesVideos.mxml  
<?xml version="1.0" encoding="utf-8"?>  
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="onStartup()"  
 title="iTunes Artist List">  
<mx:Style>  
WindowedApplication { padding-bottom:10; padding-left:10; padding-right:10; padding-top:10 }  
</mx:Style>  
<mx:Script>  
<![CDATA[  
import com.videomashup.ArtistEvent;  
import com.videomashup.Artists;  
 
private function onStartup() : void {  
 Artists.instance.addEventListener( ArtistEvent.ITUNES_ANALYSIS_COMPLETE, onUpdateComplete );  
 
 var ar:ProgressWindow = new ProgressWindow();  
 ar.title = 'Scanning Artists';  
 ar.open( true );  
 
 Artists.instance.startReader();  
}  
 
private function onUpdateComplete( event:ArtistEvent ) : void {  
 artistList.dataProvider = Artists.instance.artists;  
}  
]]>  
</mx:Script>  
<mx:List width="100%" height="100%" id="artistList" labelField="name">  
</mx:List>  
</mx:WindowedApplication>

As you can see, there isn’t much to it! The application starts by registering an event handler that will listen for the completion of our iTunes library analysis. That handler then assigns the list box to the list of artists that were retrieved.

A progress window created by the application tells the user how much iTunes XML processing remains to be done. The code for this window is shown below:

File: ProgressWindow.mxml  
<?xml version="1.0" encoding="utf-8"?>  
<mx:Window xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="onStartup()"  
 styleName="main" width="300" height="70" horizontalAlign="center">  
<mx:Style>  
.main { padding-bottom:10; padding-left:10; padding-right:10; padding-top: 10; }  
</mx:Style>  
<mx:Script>  
<![CDATA[  
import com.videomashup.Artists;  
import com.videomashup.ArtistEvent;  
 
private function onStartup() : void {  
 prgBar.source = Artists.instance;  
 Artists.instance.addEventListener( ArtistEvent.ITUNES_ANALYSIS_COMPLETE, onFinished );  
}  
 
private function onFinished( event:Event ) : void {  
 close();  
}  
]]>  
</mx:Script>  
<mx:ProgressBar id="prgBar" label="Scanning %3%%" mode="polled" />  
</mx:Window>

This window launches and attaches the progress bar to the list of artists. The Artists class contains methods for bytesLoaded and bytesTotal, which are called by the progress bar automatically to obtain the current status. Then the window automatically closes when the “iTunes Analysis Complete” dialog is displayed.

Upon launching the application from Flex Builder, an empty main window will open, along with the progress window, as seen in Figure 1.

Figure 1. The iTunes Scanning Progress Indicator

When all the artists have been processed, the progress window will disappear. The list of artists is updated in the main window, as shown in Figure 2.

Figure 2. The iTunes Artist List

The next step is to merge the completed list of artists with the video service from Yahoo Music.

Mashing up the Yahoo Music API

The first thing you need to access the user-friendly Yahoo Music API, is an API key; you can apply for one directly from the Yahoo Developer Network home page. When you have an API key, you can use it to perform one of any number of Yahoo Music API calls. For example, you can do an API search that, when given an artist name, returns a list of matching artists along with their videos. You can then store these videos in an SQLite database, so that you don’t have to make the same query from Yahoo again.

Our Artists list object now pulls data from Yahoo and iTunes, and manages a cached database of videos. The complete code listing for this object is quite long, so I’ll only talk about the key functionality. If you’re interested in exploring any of the other methods contained in this class, be sure to download the code archive for this tutorial.

The differences start in the constructor, where I build an SQLite database in a file called artists.db.

File: Artists.as   
public function Artists()  
   {  
     super();  
 
     _sqlConnection = new SQLConnection();  
     var dbFile:File = File.applicationStorageDirectory.resolvePath( "artists.db" );  
     _sqlConnection.open( dbFile );  
       
     var artCreateStmt:SQLStatement = new SQLStatement();  
     artCreateStmt.sqlConnection = _sqlConnection;  
     artCreateStmt.text = 'CREATE TABLE IF NOT EXISTS artist ( artistName VARCHAR( 128 ), artistID VARCHAR( 32 ) )';  
     artCreateStmt.execute();  
       
     var vidCreateStmt:SQLStatement = new SQLStatement();  
     vidCreateStmt.sqlConnection = _sqlConnection;  
     vidCreateStmt.text = 'CREATE TABLE IF NOT EXISTS video ( artistID VARCHAR( 32 ), year INT, videoID VARCHAR( 32 ), name VARCHAR( 128 ) )';  
     vidCreateStmt.execute();  
   }  

As you can see from the code above, our artists.db database has two tables: Artist and Video. As the names suggest, the Artist table stores the name of the artists and their IDs, which are supplied by Yahoo, while the Video table stores information about the videos for each artist. This includes the artist ID, the year of the video, the video ID (required for video thumbnails), and the name of the video.

With this in hand, we need to upgrade our onTimer method to retrieve the artist ID from the database–if it’s known. The onTimer method also triggers the Yahoo scanning portion of the list creation, if the iTunes scanning has completed. This is done by invoking the updateArtistData method:

File: Artists.as   
private function onTimer( event:Event ) : void {  
     if ( _dicts.length > 0 ) {  
       for( var i:int = 0; i < 50; i++ ) {  
         var dict:XML = _dicts.pop() as XML;  
         if ( dict == null || _dicts.length == 0 ) {  
           dispatchEvent( new ArtistEvent( ArtistEvent.ITUNES_ANALYSIS_COMPLETE ) );  
           return;  
         }  
         _dictsProcessed++;  
         var art:XML = dict.string[2];  
         if ( art != null ) {  
           var artName:String = String( art );  
           if ( _artists[ artName ] == null ) {  
             _artists[ artName ] = { name:artName, count: 1 };  
 
             var fetchIdStmt:SQLStatement = new SQLStatement();  
             fetchIdStmt.sqlConnection = _sqlConnection;  
             fetchIdStmt.text = 'SELECT * FROM artist WHERE artistName=:name';  
             fetchIdStmt.parameters[':name'] = artName;  
             fetchIdStmt.execute();  
             var fetchResult:SQLResult = fetchIdStmt.getResult();  
             for each ( var row:Object in fetchResult.data ) {  
               _artists[ artName ].artistID = row.artistID;  
             }  
               
             _artistCount++;  
           } else  
             _artists[ artName ].count += 1;  
         }  
       }  
   
       dispatchEvent( new ArtistEvent( ArtistEvent.ARTIST_LIST_CHANGE ) );  
     }  
     else  
     {  
       if ( updateArtistData() == false ) {  
         _timer.stop();  
         dispatchEvent( new ArtistEvent( ArtistEvent.YAHOO_UPDATE_COMPLETE ) );  
       }  
     }  
   }  

The updateArtistData method first checks to see whether a request to Yahoo is in process. If there is a pending request, it lets this invocation go. If there is no request in process, then it looks to find the first artist for which no ArtistID is assigned. It then makes a request of Yahoo to search for that artist. Here’s what that method looks like:

File: Artists.as   
private function updateArtistData() : Boolean {  
 if ( _searchingYahoo == false ) {  
       dispatchEvent( new ArtistEvent( ArtistEvent.STARTING_YAHOO_SEARCH ) );  
       _searchingYahoo = true;  
     }  
       
     if ( _artistSearch == null ) {  
       _yahooSearchTotal = 0;  
       _yahooSearchCompleted = 0;  
 
       // Find any artist that doens't have an AristID associated with it          
       var artist:Object = null;  
       for each ( var art:Object in artists ) {  
         if ( art.artistID == null ) {  
           if ( artist != null )  
             artist = art;  
           _yahooSearchCompleted++;  
         }  
         _yahooSearchTotal++;  
       }  
 
       if ( artist == null )  
         return false;  
 
       // Send a search request to Yahoo for that artist  
       _searchingArtist = artist.name;  
       _artistSearch = new HTTPService();  
       _artistSearch.addEventListener( ResultEvent.RESULT, onArtistResult );  
       _artistSearch.addEventListener( FaultEvent.FAULT, onArtistFault );  
       _artistSearch.url = "http://us.music.yahooapis.com/artist/v1/list/search/artist/"+escape(artist.name)+"?appid="+Constants.APPID+"&response=videos";  
       _artistSearch.resultFormat = 'e4x';  
       _artistSearch.send();  
     }  
     return true;  
   }  

Search results are sent to either onArtistResult or onArtistFault. In the case of a failure, we set the ArtistID to blank. If a result is recorded, we find the first artist listed and store that ArtistID along with the list of videos in the database.

The remaining methods in this class provide information about the artist list. As you can see, their names are self-explanatory. For example, videosMinYear and videosMaxYear return the minimum and maximum year values in the database, respectively; The getVideosByYear method returns an array of the videos for any given year. I won’t list them all here; feel free to poke around the code archive – the code is well commented.

Our custom event class also needs some upgrades to handle the new events that the Artists list generates. This updated code is shown here:

File: ArtistEvent.as    
package com.videomashup    
{    
 import flash.events.Event;    
   
 public class ArtistEvent extends Event    
 {    
   public static const ITUNES_ANALYSIS_COMPLETE:String = 'ITUNES_ANALYSIS_COMPLETE';    
   public static const ARTIST_LIST_CHANGE:String = 'ARTIST_LIST_CHANGE';    
   public static const ARTISTID_UPDATE:String = 'ARTISTID_UPDATE';    
   public static const YAHOO_UPDATE_COMPLETE:String = 'UPDATE_COMPLETE';    
   public static const STARTING_YAHOO_SEARCH:String = 'STARTING_YAHOO_SEARCH';    
       
   public function ArtistEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)    
   {    
     super(type, bubbles, cancelable);    
   }    
       
 }    
}

This code contains events for the artist list to start searching Yahoo, notification of the artist IDs update status, and notification that the entire update of the Artists list is complete.

You must also store the Yahoo API key in the Constants class, as shown here:

File: Constants.as    
package com.videomashup    
{    
 public class Constants    
 {    
   public static const APPID:String = 'Your Yahoo APP ID';    
 }    
}

Next, we replace the placeholder string with the application ID that we receive from Yahoo. Our main interface also undergoes a few upgrades, as shown here:

File: iTunesVideos.mxml    
<?xml version="1.0" encoding="utf-8"?>    
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="onStartup()"    
 title="iTunes Video Timeline">    
<mx:Style>    
WindowedApplication { padding-bottom:10; padding-left:10; padding-right:10; padding-top:10 }    
</mx:Style>    
<mx:Script>    
<![CDATA[    
import com.videomashup.Artists;    
import com.videomashup.ArtistEvent;    
   
private function onStartup() : void {    
 Artists.instance.addEventListener( ArtistEvent.YAHOO_UPDATE_COMPLETE, onUpdateComplete );    
 Artists.instance.addEventListener( ArtistEvent.STARTING_YAHOO_SEARCH, onStartYahoo );    
     
 var ar:ProgressWindow = new ProgressWindow();    
 ar.title = 'Scanning Artists';    
 ar.open( true );    
   
 Artists.instance.startReader();    
}    
   
private function onStartYahoo( event:ArtistEvent ) : void {    
 var ar:ProgressWindow = new ProgressWindow();    
 ar.title = 'Scanning Yahoo';    
 ar.open( true );    
}    
   
private function onUpdateComplete( event:ArtistEvent ) : void {    
 timeline.dataProvider = Artists.instance.videosByYear;    
}    
]]>    
</mx:Script>    
<mx:List width="100%" height="100%" id="timeline" itemRenderer="YearRenderer">    
</mx:List>    
</mx:WindowedApplication>

Now we have two progress windows: one for the iTunes reader and another for Yahoo scanning. When all this processing has completed, the onUpdateComplete method sets the dataProvider of the timeline to the list of artists.

The timeline itself is custom-rendered using the YearRenderer MXML code shown below:

File: YearRenderer.mxml    
<?xml version="1.0" encoding="utf-8"?>    
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" height="100" creationComplete="onDataChange()" dataChange="onDataChange()"    
 horizontalScrollPolicy="off" verticalScrollPolicy="off">    
<mx:Script>    
<![CDATA[    
import mx.controls.Image;    
   
private function onDataChange() : void {    
 if ( data == null || initialized == false ) return;    
 videosCanvas.removeAllChildren();    
 var index:int = 0;    
 for each( var video:Object in data.videos ) {    
   var img:Image = new Image();    
   img.x = ( index++ * 70 ) + 5;    
   img.y = 5;    
   img.toolTip = video.name;    
   img.source = "http://d.yimg.com/img.music.yahoo.com/image/v1/video/"+video.videoID+"?fallback=defaultImage&size=90x90";    
   videosCanvas.addChild( img );    
 }    
}    
]]>    
</mx:Script>    
<mx:Canvas id="videosCanvas" />    
<mx:Label text="{data.year}" left="10" top="10" fontSize="38" fontWeight="bold" color="white">    
<mx:filters>    
 <mx:GlowFilter blurX="5" blurY="5" color="#cccccc" />    
</mx:filters>    
</mx:Label>    
</mx:Canvas>

This custom-rendering object creates a set of images in our video canvas. The images represent all of the videos from the year specified in the Data field for this item. The creation of the images is performed in the onDataChange method; this method takes the list of videos and creates Image objects that point to Yahoo.

Whew! If you’ve managed to understand all that we’ve covered here, you’re doing well. Let’s launch this application in Flex Builder; it should look something like the screenshot in Figure 3, though of course with thumbnails of much cooler music than that stored in my library.

Figure 3. Loading videos from Yahoo

The completed video timeline – minus the progress window – is shown in Figure 4.

Figure 4. The completed video timeline

While my motivation for building this application was for educational purposes, it’s actually a cool little AIR widget that could provide the basis for a useful application. And it’s also a fun way to visualize what your favorite artists have been doing over the years.

Summary

In this tutorial, we built a fully fledged Adobe AIR mashup of our iTunes library and the Yahoo Music API.

Dissecting this example application exposed us to several different aspects of both the Flex and AIR platforms. On the Flex side, we learned how to parse XML from the desktop and from a web service. We also saw an example of how to use a List control as a way to scroll around a custom visualization by creating a renderer.

And from an AIR perspective, we looked at how to read data from an XML file, as well as how to create and maintain an SQLite database. We also used the Window Flex class to create native pop-up windows that look right at home in the host operating system.

I hope you enjoyed mashing up your iTunes library, and that you’ll use the concepts here to build your own exciting AIR applications!

Quiz Yourself!

Test your understanding of this article with a short quiz, and receive a FREE PDF of my book, Getting Started With Flex 3. The first 100 people to complete the quiz will also receive a paper copy delivered to their door for FREE, thanks to Adobe Systems.

Take the quiz!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • http://twitter.com/zanuka Michael Delucchi

    this is cool. but do you think it would work on a Flex app compiled for iOS running on iPhone or iPad?