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.
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.
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.
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:
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.
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.
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.
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:
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.
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
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:
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.
Most of the desired features of MyTunes are in place. However, several issues remain to be addressed. For example:
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.