Use Java to Expand iTunes Functionality

MyTunes is an easy-to-use and extensible framework you can use to build upon iTunes’ built-in functionality. With a little help from Java, you can even run iTunes remotely from another machine.

AppleScript fans have had hooks in iTunes for several years, which makes the information in your music library accessible to other scriptable applications. And while AppleScript can be glued together with other components in OS X (in the Terminal, via the /usr/bin/osascript command and AppleScript’s do shell script command), attempting to provide your music information to nonscriptable applications or resources (e.g., a web site powered by PHP and MySQL) has traditionally been done in hack-and-scratch ways.

MyTunes fills that void by providing an easy-to-use and extensible frame-work that allows you to access and manipulate your iTunes library via Java. This hack introduces you to MyTunes and describes the basic concepts of how and why it works the way it does.

Introducing MyTunes

MyTunes has been around for several years in a previous form. It was originally an AppleScript that created an XML-ish file from the iTunes library, which was then parsed by a Perl script to load into a MySQL database (hence the name: MySQL + iTunes = MyTunes).

However, iTunes Version 4 creates a file called iTunes Music Library.xml in your music library folder, so the first half of MyTunes’ original purpose is no longer necessary. And because the Perl script had to be rewritten to parse the new file that is automatically maintained by iTunes, I overhauled the package to make it do so.

The iTunes Music Library.xml file contains just about every piece of data you could possibly want to know about your music collection, including:

Application data

Version and library location

Tracks

Name, artist, album, length, rating, comments, etc.

Playlists

Name, whether the playlist is smart, and all tracks that belong to it

To see what other information is included, open it in your favorite text editor and take a peek.

Later in this hack, we’ll walk through an example of how the bigger and better MyTunes accomplishes the task previously completed by its predecessor and, in doing so, provides a reusable framework to avoid rewriting the meat and potatoes; instead, we’ll be spending our time using the information, rather than retrieving the information. But before we start playing around with MyTunes, we’ll need to install the necessary components that comprise the package.

Downloading and Installing MyTunes

As with any other time that you deal with XML, the first thing you’ll need is an XML parser. MyTunes uses the Apache Software Foundation’s Xerces parser (http://xml.apache.org/dist/xerces-j/; free). You’ll also need Apache’s XMP-RPC library (http://ws.apache.org/xmlrpc/; free) to take advantage of the new remote control features available.

The latest version of MyTunes is available as a free download from http://www.macdevcenter.com/mac/2003/09/03/examples/mytunes.zip.

Be sure to add these files to your CLASSPATH (refer to http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/classpath.html to set up the CLASSPATH environment variable), or place all the JAR files in the /Library/Java/Extensions/ folder, so that all classes will be automatically made available to your Java applications.

Customizing MyTunes

A configuration file holds all necessary parameters; all classes in the package that have parameterized variables will look for the configuration file ~/.mytunes.xml. Once you’ve downloaded the sample to your desktop, place it in the proper location with the following command:

mv ~/Desktop/mytunes.xml ~/.mytunes.xml

You’ll notice that it won’t show up when you view your home directory in the Finder. This is because every file that has a period (.) as the first character in its name is hidden. In order to view and edit the file, you’ll have to use the Terminal in conjunction with a text editor such as emacs (http://www.gnu.org/software/emacs/emacs.html), vi (http://www.vim.org), or pico (http://www.washington.edu/pine/faq/whatis.html#2.2):

	emacs ~/.mytunes.xml

The configuration file should look similar to Figure 4-35.

As you can see from Figure 4-35, MyTunes has several groups of properties:

Sample configuration file for the MyTunes package

Figure 4-35. Sample configuration file for the MyTunes package

library

Location of the iTunes Music Library.xml file

remote.*

Features with remote control (via XML-RPC)

mysqlimport.*

Chord that will be created

Opening MyTunes

Once you have installed the necessary components and have set the proper values in your configuration file, open a Terminal window and enter the following command:

	java com.fivevoltlogic.mytunes.MyTunesLibrary

You should see something similar to Figure 4-36.

Sample output from MyTunesLibrary

Figure 4-36. Sample output from MyTunesLibrary

If you receive a ClassNotFoundException, then one (or more) of the JAR files is missing from /Library/Java/Extensions or the location you specified in your CLASSPATH.

One other problem that you might run into is a thrown java.io.IOException. This arises if you are not connected to the Internet when using MyTunesLibrary. Because the iTunes Music Library.xmlfile’s Document Type Declaration (DTD)—the guide that describes how a particular XML document is constructed—is declared as PUBLIC it is located on Apple’s servers. Thus, the XML parser attempts to load the document from this location via the internet and throws a java.io.IOException since the file can’t be accessed.

Looking at the sample output, we can see that I currently have 1839 tracks and 20 playlists in my iTunes library. However, this is the only information that MyTunesLibrary provides on its own. While MyTunesLibrary parses the information in the library (track names, albums, the playlists to which songs belong, etc.), the class doesn’t actually do anything with the data. This separation of logic allows us to reuse the class in different applications without rewriting the bulk of the code (which consists mainly of parsing the XML file to extract the desired information).

With that in mind, in order to use the information made available to us, we have to create a class with appropriate methods invoked by MyTunesLibrary as callbacks. For anyone who has worked with SAX before, the ContentHandler interface should spring immediately to mind; with SAX, we receive notifications when we’ve reached XML elements, text, or other items.

Design Considerations

While MyTunes’ callback structure is similar to the Simple API for XML (SAX), the XML file is actually parsed with Document Object Model (DOM). The reasoning behind this choice derives from the nonsemantic markup in iTunes Music Library.xml; the entire library is described by approximately 10 tags, which makes keeping track of our location in the file while parsing problematic. Because you are using DOM, there is a chance that you could run into memory issues if you have a large music library. In the event that you do run into memory errors, try increasing the memory allocated to your Java Virtual Machine (JVM) to fix the problem.

Extending Chord

The com.fivevoltlogic.mytunes.Chord class is to MyTunes what the ContentHandler interface is to SAX; Chord's methods can be overridden to suit your needs. As mentioned earlier, we’ll create a Chord to populate a MySQL database with the information from your iTunes library. While the design could be improved in many ways, it serves as an excellent starting point, and after reading and walking through the following steps, you’ll be confident and knowledgeable enough to use MyTunes in any way that you could want.

Tip

This hack assumes you already have MySQL up and running on your Mac and a basic level of competency in using and administrating the server. If this is not the case, Marc Liyonage (http://www.entropy.ch/home/) has an excellent tutorial for installing the MySQL server on OS X. In addition to xerces.jar and mytunes.jar, you’ll need a JDBC driver to connect to the MySQL database that you’re populating. MySQL Connector/J will do exactly that. Again, extract and place the JAR file in /Library/Java/Extensions/ and you’ll be ready to go.

The first step is to create the database tables that will hold the desired information. Here is a MySQL script that will accomplish that task:

# attempt to delete tables named Playlists, PlaylistTracks, or Tracks that
# already exist
# if your database already has table(s)
#
DROP TABLE IF EXISTS Playlists;
DROP TABLE IF EXISTS PlaylistTracks;
DROP TABLE IF EXISTS Tracks;

# holds all information regarding an iTunes track
#
#
#
#
CREATE TABLE Tracks ( trackId INT UNSIGNED NOT NULL,

                      trackAlbum VARCHAR(128),
                      trackArtist VARCHAR(128),
                      trackBitRate INT DEFAULT 128,
                      trackComment VARCHAR(128),
                      trackComposer VARCHAR(128),
                      trackDateAdded DATETIME,
                      trackDateModified DATETIME,
                      trackDiscCount SMALLINT DEFAULT 1,
                      trackDiscNumber SMALLINT DEFAULT 1,
                      trackDuration SMALLINT UNSIGNED NOT NULL,
                      trackEqualizer VARCHAR(32),
                      trackGenre VARCHAR(128) DEFAULT "",
                      trackKind VARCHAR(32),
                      trackLocation VARCHAR(128) NOT NULL,
                      trackName VARCHAR(128) DEFAULT "",
                      trackPlayedCount SMALLINT UNSIGNED DEFAULT 0,
                      trackPlayedDate DATETIME,
                      trackRating SMALLINT UNSIGNED DEFAULT 0,
                      trackSampleRate INT DEFAULT 44100,
                      trackSize INT,
                      trackCount SMALLINT DEFAULT 0,
                      trackNumber SMALLINT DEFAULT 0,
                      trackYear SMALLINT DEFAULT 0,
                      trackCurrent ENUM("true", "false"),

                      PRIMARY KEY(trackId),
                      INDEX(trackId)
);
# holds all the information regarding an iTunes playlist
# EXCEPT for the tracks that it contains; because a playlist can
# (and should) have more than track, we'll need a separate table
# to map this relationship
#
CREATE TABLE Playlists ( playlistId SMALLINT UNSIGNED,
                         playlistName VARCHAR(32),
                         playlistSmart enum("true", "false"),
                         playlistCurrent enum("true", "false"),

                         PRIMARY KEY(playlistId),
                         INDEX(playlistId)
);

# maps the many-to-one relationships between Tracks and Playlists
#
CREATE TABLE PlaylistTracks ( playlistId SMALLINT UNSIGNED NOT NULL,
                             trackId INT UNSIGNED NOT NULL,
                    trackIndex SMALLINT UNSIGNED NOT NULL,

                                PRIMARY KEY(playlistId, trackId,
trackIndex),
                                INDEX (trackId)
);

Three tables are created (see the script for details on the columns that will be created in each table to correspond to the fields of the Track and Playlist beans), two of which are self-explanatory:

Tracks

Holds information about an iTunes track.

Playlists

Holds information about an iTunes playlist.

PlaylistTracks

Maps Tracks to a Playlist. This table is necessary to describe the many-to-one relationship between Tracks and Playlists.

To create the tables in the FVL database (as an example), open the Terminal and type the following command:

mysql FVL < mytunes.mysql

With the database ready to go, the next step is to extend the Chord class to populate the database with the information it receives. Here’s the resulting class:

**************************************************************************************************
*
* FILE:		MySQLImport.java
* AUTHOR:	David Miller http://www.sqlmagic.com/d/
* ABOUT:	Describes of how to extend com.fivevoltlogic.mytunes.Chord to
*			provide customized functionality.
* DATE:		September 1, 2003
*
**************************************************************************************************/
import com.fivevoltlogic.mytunes.*;
import java.io.IOException;
import java.util.List;
import java.sql.*;

public class MySQLImport extends Chord {
   // sql classes to provide connection, query, and
   private Connection con;
   private PreparedStatement insertTrack,
                             insertPlaylist,
                             insertPlaylistTrack;
   private ResultSet rs;
   private Statement stmt;
 
   public MySQLImport() throws SQLException, ClassNotFoundException,
IllegalAccessException, java.lang.InstantiationException, IOException {

      // will read the values in from ~/.mytunes.xml;
      // these properties will be stored in the props Properties instance
      super();

      // connect to the database with the appropriate parameters
      Class.forName((String)props.get("mysqlimport.driver")).newInstance(
);
      this.con = DriverManager.getConnection("jdbc:mysql://" + (String)
props.get("mysqlimport.host") + "/" + (String) props.get("mysqlimport.
database") + "?user=" + (String) props.get("mysqlimport.user") +
"&password=" + (String) props.get("mysqlimport.password"));
        this.stmt = con.createStatement();
     }
     public void onStart() {

         try {

       // clear all existing information from the database
       this.stmt.executeUpdate("DELETE FROM Tracks");
       this.stmt.executeUpdate("DELETE FROM Playlists");
      this.stmt.executeUpdate("DELETE FROM PlaylistTracks");
       
	   // prepare statements for queries
       // refer to mytunes.mysql to see the details on the tables
       this.insertTrack = con.prepareStatement("INSERT INTO Tracks
VALUES(" +
                "?, " + // 01 trackId
                "?, " + // 02 trackAlbum
                "?, " + // 03 trackArtist
                "?, " + // 04 trackBitRate
                "?, " + // 05 trackComment
                "?, " + // 06 trackComposer
                "?, " + // 07 trackDateAdded
                "?, " +    // 08 trackDateModified
                "?, " + // 09 trackDiscCount
                "?, " + // 10 trackDiscNumber
                "?, " + // 11 trackDuration
                "?, " + // 12 trackEqualizer
                "?, " + // 13 trackGenre
                "?, " + // 14 trackKind
                "?, " + // 15 trackLocation
                "?, " + // 16 trackName
                "?, " + // 17 trackPlayedCount
                "?, " + // 18 trackPlayedDate
                "?, " + // 19 trackRating
                "?, " + // 20 trackSample
                "?, " + // 21 trackSize
                "?, " + // 22 trackCount
                "?, " + // 23 trackNumber
                "?, " + // 24 trackYear
                "'false'" + // 25 trackCurrent
                ")");
        this.insertPlaylist = con.prepareStatement("INSERT INTO
Playlists VALUES("
                "?, " + // 01 playlistId
                "?, " + // 02 playlistName
                "?, " + // 03 playlistSmart
                "'false'" + // 04 playlistCurrent
                ")");
        this.insertPlaylistTrack = con.prepareStatement("INSERT INTO
PlaylistTracks VALUES("
                "?, " + // 01 playlistId
                "?, " + // 02 trackId
                "? " + // 03 trackIndex
                ")");
      } catch (Exception e) { this.onError(e.getMessage()); }
   }

public void onTrack(Track t) {
       
		try {
  // get the information about the track and populate the
PreparedStatement with the values
            this.insertTrack.setInt(1, t.getId());
            this.insertTrack.setString(2, t.getAlbum());
            this.insertTrack.setString(3, t.getArtist());
            this.insertTrack.setInt(4, t.getBitRate());
            this.insertTrack.setString(5, t.getComment());
            this.insertTrack.setString(6, t.getComposer());

            // convert the XML date format into an SQL format
            if (t.getDateAdded() != null) {
                this.insertTrack.setTimestamp(7, new Timestamp(t.
getDateAdded().getTime()));
            }

            // convert the XML date format into an SQL format
            if (t.getDateModified() != null) {
                this.insertTrack.setTimestamp(8, new Timestamp(t.
getDateModified().getTime()));
           }

           this.insertTrack.setInt(9, t.getDiscCount());
           this.insertTrack.setInt(10, t.getDiscNumber());
           this.insertTrack.setInt(11, t.getDuration());
           this.insertTrack.setString(12, t.getEqualizer());
           this.insertTrack.setString(13, t.getGenre());
           this.insertTrack.setString(14, t.getKind());
           this.insertTrack.setString(15, t.getLocation());
           this.insertTrack.setString(16, t.getName());
           this.insertTrack.setInt(17, t.getPlayedCount());

           // convert the XML date format into an SQL format
           if (t.getPlayedDate() != null) {
               this.insertTrack.setTimestamp(18, new Timestamp(t.
getPlayedDate().getTime()));
           } else {
               this.insertTrack.setTimestamp(18, null);
           }

           this.insertTrack.setInt(19, t.getRating());
           this.insertTrack.setInt(20, t.getSampleRate());
           this.insertTrack.setInt(21, t.getSize());
           this.insertTrack.setInt(22, t.getTrackCount());
           this.insertTrack.setInt(23, t.getTrackNumber());
           this.insertTrack.setInt(24, t.getYear());
           // execute the query
 insertTrack.executeUpdate();
  } catch (SQLException e) { this.onError(e.getMessage()); }
       }

       public void onPlaylist(Playlist p) {

           try {

               // note: Playlist.isSmart() always returns false,
               // as this feature isn't complete yet
               String b = new Boolean(p.isSmart()).toString();
               insertPlaylist.setInt(1, p.getId());
               insertPlaylist.setString(2, p.getName());
               // as of right now, we don't check to see if a playlist is smart
or not,
               // so we'll default to false for now...
               insertPlaylist.setString(3, "false");

               // execute the query
               insertPlaylist.executeUpdate();

               // get a list of the database ids of all tracks in this
playlist,
               List tracks = p.getTracks();
               for (int i = 0; i < tracks.size(); i++) {
                    // prepare the statement
                    int t = ((Integer)tracks.get(i)).intValue();
                    insertPlaylistTrack.setInt(1, p.getId());
                    insertPlaylistTrack.setInt(2, t);
                    insertPlaylistTrack.setInt(3, i + 1);

                    // insert the query
                    insertPlaylistTrack.executeUpdate();
                }

            } catch (Exception e) {
                this.onError(e.getMessage());
            }
        }

         // simply echo the error to the screen
         public void onError(String message) {
             System.out.println("error: " + message);
        }

        // close the database connection
 public void onFinish() {
              try {
                 con.close();
             } catch (SQLException e) { this.onError(e.getMessage()); }
         }

         // if we have a commandline argument, we interpret it to be the path
         // to the user's config file; if there are no commandline arguments,
         // look for the config file at ~/.mytunes.xml
         public static void main(String[] args) {

             try {

                 if (args.length != 0) {
                     System.out.println("Usage: java MySQLImport");
                     System.exit(0);

                 } else {

                     // create an instance of MyTunesLibrary and set
                     // an instance of this class to be the handlers
                     MyTunesLibrary lib = new MyTunesLibrary();
                     lib.setHandler(new MySQLImport());

                     // begin parsing, at which point this class's methods
                     // will be invoked as callbacks throughout the process
                     lib.parse();
                 }

             } catch (IOException e) {
                 System.out.println("io: " + e.getMessage());
             } catch (SQLException e) {
                 System.out.println("sql: " + e.getMessage());
             } catch (org.xml.sax.SAXException e) {
                 System.out.println("sax: " + e.getMessage());
             } catch (ClassNotFoundException e) {
                 System.out.println("cnf: " + e.getMessage());
             } catch (InstantiationException e) {
                 System.out.println("i: " + e.getMessage());
             } catch (IllegalAccessException e) {
                 System.out.println("iae: " + e.getMessage());
             }
         }
     }

Upon running the Chord class, you should receive no output. However, upon checking your database, you’ll see that the information has been properly stored.

And now that our information is stored in a SQL database, we can view the number of tracks in our iTunes music library, as shown in Figure 4-37.

Viewing the number of tracks in the iTunes music library

Figure 4-37. Viewing the number of tracks in the iTunes music library

We can also run a variety of queries on our music data. This query returns all information from all tracks by either Bran Van 3000 or The Weakerthans:

select * from Tracks where trackArtist="Bran Van 3000" or trackArtist="The Weakerthans"

And this query returns the database ID for all tracks that are shorter than three minutes and have a rating of four or five stars:

select trackId from Tracks where trackDuration < 180 and trackRating > 80

Controlling iTunes Remotely

The previous section showed how to create a Chord to populate a MySQL database with information from the iTunes library. However, the com. fivevoltlogic.mytunes package contains several utility classes that are not related to the library. As you can probably guess by the heading of this section, MyTunes also allows you to control iTunes via Java.

If you open iTunes’ dictionary in Script Editor and compare it to the com. fivevoltlogic.mytunes.Remote API, you’ll see there is a similarity between the two. There’s good reason for this: the majority of Remote’s methods are actually just wrappers around iTunes’ available AppleScript commands. Commands such as pause(), backTrack(), nextTrack(), playPause(), previousTrack(), and stop() are self-explanatory. Here are a couple commands that might require a bit more information:

playTrack(int)

Plays a track with the corresponding database ID from the library

playArtist(String)

Plays the first track in the library with the corresponding artist

When one of these methods is invoked, a file is created in the directory indicated by the System property java.io.tmpdir (which, by default, evaluates to / tmp). The contents of this file are merely an AppleScript that will be passed as an argument to the /usr/bin/osascript command. For example, create an instance of Remote and invoke the playPlaylistTrack(playlist, track) command, as in this example:

       import com.fivevoltlogic.mytunes.Remote;

       public class RemoteTest {

           public static void main(String[] args) {

               try {

                   Remote r = new Remote();

                   // we need two arguments to for this method:
                   // (1) the playlist index, and
                   // (2) the track number
                   if (args.length != 2) {
                       System.out.println("Usage: PlaylistTrack [playlist index]
       [track number]");
                       System.exit(0);
                  }

                  if (! r.playPlaylistTrack(Integer.parseInt(args[0]), Integer.  
      parseInt(args[1]))) { 
                      System.out.println("Unable to play track");
                  }

            // if we aren't able to instantiate a Remote object
            // a basic exception will be thrown
         } catch (java.lang.Exception e) {
           System.out.println("Error: " + e.getMessage());
         }
      }
   }

A file named mytunes.remote will be created containing the following text:

      tell application "iTunes" 
        play (track 4 of playlist 1) 
      end tell

This file is then passed as an argument to the /usr/bin/osascript command.

Following the completion (successful or not) of the method’s execution, the temporary file is deleted to allow the next command to be executed. Because iTunes is controlled in this manner, the method must be synchronized.

You have been introduced to all classes in the com.fivevoltlogic.mytunes package but one. You saw how to control iTunes from a Java class that is running on the local machine (localhost); the next step is to control iTunes from a different machine. And since we’re using Java and XML as our foundation, there are three ways to implement this feature:

  • XML Remote Procedure Calling (XML-RPC)

  • Simple Object Access Protocol (SOAP)

  • Remote Method Invocation (RMI)

All three frameworks are under the same general umbrella called distributed computing. And of the three choices there are a variety of reasons to choose one over the others in certain situations. RMI’s main drawback is that both sides of the communication line must be written in Java. While this isn’t necessarily bad, the client, ideally, shouldn’t be tied to just one language. And because XML-RPC is easier than SOAP to get up and running, we’ll use it as our transport from client to server and back again. For a more detailed description and comparison of the protocols, see Java and XML, by Brett McLaughlin (O’Reilly).

The first step is to get the server up and running. If you like, you can specify a port that the server should listen to; by default, it uses the property given in ~/.mytunes.xml.

Once BaseStation is listening, we’ll need to create a client to talk to the server. Because of XML-RPC’s simplicity, this can be done in a matter of minutes.

Tip

For Eric Kidd’s introduction to XML-RPC, see http://xmlrpc-c.sourceforge.net/xmlrpc-howto/xmlrpc-howto.html.

The source for RPCTest.java is available at http://www.macdevcenter.com/pub/ a/mac/2003/09/03/RPCTest.java. Because both the server and client run on the same machine, this example isn’t very practical. But if you have two or more Macs on a network, it is easy to control another Mac to behave as a jukebox. Throw this into a servlet (see http://developer.apple.com/internet/java/tomcat1. html on Apple’s Developer Connection to get Apache Tomcat up and running on OS X) and you’ve got remote control of iTunes from a web interface.

Updating MyTunes Features

Most of the desired features of MyTunes are in place. However, several issues remain to be addressed. For example:

  • The isSmart() method of the Playlist class always returns false.

  • Playlists are dealt with in the order in which they are located in the XML file; hence, their getId() method returns a value based on that, which has nothing to do with the index of the playlist in iTunes’ sorting order.

These changes and additions will be incorporated into the package as time permits. If you’re interested in the package, stay tuned to the project’s home page (http://www.sqlmagic.com/d/mytunes/) for updates and changes as they are released.

David Miller

Get iPod and iTunes Hacks now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.