Now that I successfully have iTunes talking to my website, I thought it would be nice to post the code that does it, since I didn’t really write much of it.

The server side is just a PHP script that handles a POST and stores the information in a single-row, single-table SQLite database, so it’s not interesting.

The client is marginally interesting to me. Basically, I modified every line from code I discovered by googling, and all credit belongs to those that wrote it (but of course, I forgot to note who wrote what as I went along).

I started with the main program definition, which sets up the listener and starts waiting for events to be passed to said listener.

#import "iTunesObserver.h"

int main (int argc, const char * argv[]) {
  NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
  NSDistributedNotificationCenter* nc;
  iTunesObserver* observer;
  NSRunLoop* rl;
  
  /* create notification center and iTunes observer objects */
  nc = [NSDistributedNotificationCenter defaultCenter];
  observer = [[iTunesObserver alloc] init];
  
  /* and tell the notification center to notify the iTunes observer when
   * an event from the iTunes player occurs
   */
  [nc addObserver:observer
         selector:@selector(trackNotification:)
             name:@"com.apple.iTunes.playerInfo" object:nil];
  /* create a run loop object */
  rl = [NSRunLoop currentRunLoop];
  /* and start running */
  [rl run];
  
  /* if we get here, the run loop ended */
  [pool drain];
  return 0;
}

The real work is all done inside the iTunesObserver object

First, the header (iTunesObserver.h):

#import <cocoa/cocoa.h>
#import <foundation/foundation.h>


@interface iTunesObserver : NSObject {
}

-(void)trackNotification:(NSNotification*)notif;
- (NSString *)urlEncodeValue:(NSString *)str;

@end
</foundation/foundation.h></cocoa/cocoa.h>

We have two functions defined, trackNotification, which is called by the notification center when the iTunes event occurs, and urlEncodeValue, which will encode a string to be passed in a URL.

The code is defined in iTunesObserver.m:

#import "iTunesObserver.h"

@implementation iTunesObserver

-(void)trackNotification:(NSNotification*)notif
{
  NSString *playing = @"Playing";
  NSString *post = @"track=";
  NSString *weburl = @"http://*";
  
  /* when notified, take the dictionary passed to us, find the elements we care about, and
   * convert the whole mess into a big long NSString containing what we want to send to
   * panix.  Note we are encoding any special characters as UTF-8 by calling urlEncodeValue
   * here.
   */
  if ([[[notif userInfo] objectForKey:@"Player State"] compare:playing] == 0) {
    if ([[[notif userInfo] objectForKey:@"Location"] isLike:weburl]) {
      post = [NSString stringWithFormat:
                    @"track=%@&album=%@&artist=%@&rating=%@&genre=%@&url=%@",
                     [self urlEncodeValue:[[notif userInfo] objectForKey:@"Name"]]
                    ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Album"]]
                    ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Artist"]]
                    ,[[notif userInfo] objectForKey:@"Rating"]
                    ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Genre"]]
                    ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Location"]]
      ];
    /* if iTunes is paused, make an empty request */
    } else {
      post = [NSString stringWithFormat:
      @"track=%@&album=%@&artist=%@&year=%@&rating=%@&genre=%@&time=%@",
      [self urlEncodeValue:[[notif userInfo] objectForKey:@"Name"]]
      ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Album"]]
      ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Artist"]]
      ,[[notif userInfo] objectForKey:@"Year"]
      ,[[notif userInfo] objectForKey:@"Rating"]
      ,[self urlEncodeValue:[[notif userInfo] objectForKey:@"Genre"]]
      ,[[notif userInfo] objectForKey:@"Total Time"]
              ];
    }
  }
  /* log what we are going to send, this will end up in the console log when we run from launchd */
  NSLog(@"%@",post);

  /* convert the post message into raw ASCII encoded data
  NSData *postData = [post dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];

  /* find length of string, dump it to a string value */
  NSString *postLength = [NSString stringWithFormat:@"%d", [postData length]];

  /* now, we have the post data, we know how big it is, we just have to send it. */
  NSMutableURLRequest *request = [[[NSMutableURLRequest alloc] init] autorelease];

  /* url here is not the real upload handler */
  [request setURL:[NSURL URLWithString:@"http://example.com/my_upload_handler.cgi"]];
  [request setHTTPMethod:@"POST"];
  [request setValue:postLength forHTTPHeaderField:@"Content-Length"];
  [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
  [request setHTTPBody:postData];

  /* create a download object for the request and off it goes */
  NSURLDownload  *theDownload=[[NSURLDownload alloc] initWithRequest:request delegate:self];
  if (!theDownload) {
    NSLog(@"POST failed, sorry");
  }
}

- (NSString *)urlEncodeValue:(NSString *)str
{
  NSString *result = (NSString *) CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (CFStringRef)str,
              NULL, CFSTR("?=&+"), kCFStringEncodingUTF8);
  return [result autorelease];
}

@end

That’s basically it. Add CoreServices.framework and CoreFoundation.framework to the Xcode project, compile, and save the resulting binary in a good location. I use ~/bin

Then, create a launchd config file in Library/LaunchAgents directory in your Home directory

<plist version="1.0">
 <dict>
  <key>Label</key>
  <string?com.panix.users.awhitema.itunes</string>
  <key>ProgramArguments>/key>
   <array>
   <string>/Users/awhitema/bin/iTunesUploader</string>
  </array>
  <key>KeepAlive</key>
  <true>
  <key>RunAtLoad</key>
  <true>
  <key>LimitLoadToSessionType</key>
  <string>Aqua</string>
 </dict>
</plist>

The entry LimitLoadToSessionType enables the tool to talk to the GUI and launchd will launch the tool on login when the user has a GUI session. In particular, this means the tool will not run for ssh sessions. When the tool has access to the Aqua layer, it can talk to the keychain, which means if the upload handler on the remote server is protected by a HTTP password, the NSMutableURLRequest can get that password from the keychain.

All that’s left is to start the tool, which is accomplished by loading the launchd plist into launchd.

launchctl load -S Aqua ~/Library/LaunchAgents/com.panix.users.awhitema.itunes.list