Extras/GrowlTunes/GrowlTunesController.m
author Evan Schoenberg
Thu Feb 26 06:36:43 2009 -0500 (2009-02-26)
changeset 4177 2776a948782a
parent 3865 c867ac4b806c
child 4287 c6a0bfc2f06a
permissions -rw-r--r--
Patch from Ludek (http://www.dolejsky.com/2009/02/19/growltunes-patch/) which fixes display of radio streams. Thanks!
     1 /*
     2  Copyright (c) The Growl Project, 2004
     3  All rights reserved.
     4 
     5 
     6  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
     7 
     8 
     9  1. Redistributions of source code must retain the above copyright
    10  notice, this list of conditions and the following disclaimer.
    11  2. Redistributions in binary form must reproduce the above copyright
    12  notice, this list of conditions and the following disclaimer in the
    13  documentation and/or other materials provided with the distribution.
    14  3. Neither the name of Growl nor the names of its contributors
    15  may be used to endorse or promote products derived from this software
    16  without specific prior written permission.
    17 
    18 
    19  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    20 
    21  */
    22 
    23 //
    24 //  GrowlTunesController.m
    25 //  GrowlTunes
    26 //
    27 //  Created by Nelson Elhage on Mon Jun 21 2004.
    28 //  Copyright (c) 2004 Nelson Elhage. All rights reserved.
    29 //
    30 
    31 #import "GrowlTunesController.h"
    32 #import "GrowlTunesPlugin.h"
    33 #import "NSWorkspaceAdditions.h"
    34 
    35 @interface NSString (GrowlTunesMultiplicationAdditions)
    36 
    37 - (NSString *)stringByMultiplyingBy:(unsigned)multi;
    38 
    39 @end
    40 
    41 #define ONLINE_HELP_URL		    @"http://growl.info/documentation/growltunes.php"
    42 
    43 @interface GrowlTunesController (PRIVATE)
    44 - (NSAppleScript *) appleScriptNamed:(NSString *)name;
    45 - (void) addTuneToRecentTracks:(NSString *)inTune fromPlaylist:(NSString *)inPlaylist;
    46 - (NSMenu *) buildiTunesSubmenu;
    47 - (NSMenu *) buildRatingSubmenu;
    48 - (void) jumpToTune:(id) sender;
    49 @end
    50 
    51 #define ITUNES_TRACK_CHANGED	@"Changed Tracks"
    52 #define ITUNES_PAUSED			@"Paused"
    53 #define ITUNES_STOPPED			@"Stopped"
    54 #define ITUNES_PLAYING			@"Started Playing"
    55 
    56 #define APP_NAME		        @"GrowlTunes"
    57 #define ITUNES_APP_NAME         @"iTunes.app"
    58 #define ITUNES_BUNDLE_ID        @"com.apple.itunes"
    59 
    60 #define POLL_INTERVAL_KEY       @"Poll interval"
    61 #define NO_MENU_KEY             @"GrowlTunesWithoutMenu"
    62 #define RECENT_TRACK_COUNT_KEY  @"Recent Tracks Count"
    63 
    64 #define DEFAULT_POLL_INTERVAL	    2
    65 #define DEFAULT_RECENT_TRACKS_LIMIT 20U
    66 
    67 //status item menu item tags.
    68 enum {
    69 	ratingTag = -11,
    70 	onlineHelpTag = -5,
    71 	quitGrowlTunesTag,
    72 	launchQuitiTunesTag,
    73 	quitBothTag,
    74 	togglePollingTag,
    75 };
    76 
    77 @implementation GrowlTunesController
    78 
    79 - (id) init;
    80 {
    81 	/* NOTE: The class currently gets instatiated from within a nib file, therefore init will get called 
    82 	 regardless. Would be cleaner if the app didnt use a nib file, but I didnt have the energy to work out
    83 	 how to get a decent return value from NSApplication when setting things up manaully ala GHA.  For now
    84 	 I've just overridden init to return the sharedInstance */
    85 	return [[self class] sharedInstance];
    86 }
    87 
    88 - (id) initSingleton {
    89 	self = [super initSingleton];
    90 	if (!self)
    91 		return nil;
    92 
    93 	[GrowlApplicationBridge setGrowlDelegate:self];
    94 
    95 	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    96 	NSDictionary *defaultDefaults = [[NSDictionary alloc] initWithObjectsAndKeys:
    97 		[NSNumber numberWithDouble:DEFAULT_POLL_INTERVAL], POLL_INTERVAL_KEY,
    98 		[NSNumber numberWithInt:20],                       RECENT_TRACK_COUNT_KEY,
    99 		nil];
   100 	[defaults registerDefaults:defaultDefaults];
   101 	[defaultDefaults release];
   102 
   103 	state = itUNKNOWN;
   104 	NSNumber *recentTrackCountNum = [defaults objectForKey:RECENT_TRACK_COUNT_KEY];
   105 	recentTracks = [[NSMutableArray alloc] initWithCapacity:(recentTrackCountNum ? [recentTrackCountNum unsignedIntValue] : DEFAULT_RECENT_TRACKS_LIMIT)];
   106 	archivePlugin = nil;
   107 	plugins = [[self loadPlugins] retain];
   108 	trackID = 0;
   109 	trackURL = @"";
   110 	trackRating = -1;
   111 
   112 	return self;
   113 }
   114 
   115 - (void) applicationWillFinishLaunching: (NSNotification *)notification {
   116 #pragma unused(notification)
   117 	getInfoScript = [self appleScriptNamed:@"jackItunesArtwork"];
   118 
   119 	NSString *itunesPath = [[NSWorkspace sharedWorkspace] fullPathForApplication:@"iTunes"];
   120 	if ([[[NSBundle bundleWithPath:itunesPath] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] floatValue] >= 4.7f)
   121 		[self setPolling:NO];
   122 	else
   123 		[self setPolling:YES];
   124 
   125 	if (polling) {
   126 		pollScript   = [self appleScriptNamed:@"jackItunesInfo"];
   127 		pollInterval = [[NSUserDefaults standardUserDefaults] floatForKey:POLL_INTERVAL_KEY];
   128 
   129 		if ([self iTunesIsRunning]) [self startTimer];
   130 
   131 		NSNotificationCenter *workspaceCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
   132 		[workspaceCenter addObserver:self
   133 							selector:@selector(handleAppLaunch:)
   134 								name:NSWorkspaceDidLaunchApplicationNotification
   135 							  object:nil];
   136 
   137 		[workspaceCenter addObserver:self
   138 							selector:@selector(handleAppQuit:)
   139 								name:NSWorkspaceDidTerminateApplicationNotification
   140 							  object:nil];
   141 	} else {
   142 		[[NSDistributedNotificationCenter defaultCenter] addObserver:self
   143 															selector:@selector(songChanged:)
   144 																name:@"com.apple.iTunes.playerInfo"
   145 															  object:nil];
   146 	}
   147 	if (![[NSUserDefaults standardUserDefaults] boolForKey:NO_MENU_KEY])
   148 		[self createStatusItem];
   149 }
   150 
   151 - (void) applicationWillTerminate:(NSNotification *)notification {
   152 #pragma unused(notification)
   153 	[[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
   154 	[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
   155 	[self stopTimer];
   156 	[self tearDownStatusItem];
   157 
   158 	[pollScript    release]; 
   159 	[getInfoScript release];
   160 	[recentTracks  release];
   161 
   162 	[noteDict release];
   163 
   164 	[plugins release];
   165 	if (archivePlugin)
   166 		[archivePlugin release];
   167 }
   168 
   169 #pragma mark -
   170 #pragma mark Growl delegate conformance
   171 
   172 - (NSDictionary *) registrationDictionaryForGrowl {
   173 	NSArray	*allNotes = [[NSArray alloc] initWithObjects:
   174 		ITUNES_TRACK_CHANGED,
   175 //		ITUNES_PAUSED,
   176 //		ITUNES_STOPPED,
   177 		ITUNES_PLAYING,
   178 		nil];
   179 	NSDictionary *readableNames = [NSDictionary dictionaryWithObjectsAndKeys:
   180 								   NSLocalizedString(@"Changed Tracks", nil), ITUNES_TRACK_CHANGED,
   181 								   NSLocalizedString(@"Started Playing", nil), ITUNES_PLAYING,
   182 								   nil];
   183 	
   184 	NSImage			*iTunesIcon = [[NSWorkspace sharedWorkspace] iconForApplication:ITUNES_APP_NAME];
   185 	NSDictionary	*regDict = [NSDictionary dictionaryWithObjectsAndKeys:
   186 		APP_NAME,                        GROWL_APP_NAME,
   187 		[iTunesIcon TIFFRepresentation], GROWL_APP_ICON,
   188 		allNotes,                        GROWL_NOTIFICATIONS_ALL,
   189 		allNotes,                        GROWL_NOTIFICATIONS_DEFAULT,
   190 		readableNames,					 GROWL_NOTIFICATIONS_HUMAN_READABLE_NAMES,
   191 		nil];
   192 	[allNotes release];
   193 	return regDict;
   194 }
   195 
   196 - (NSString *) applicationNameForGrowl {
   197 	return APP_NAME;
   198 }
   199 
   200 - (void) setPolling:(BOOL)flag {
   201 	polling = flag;
   202 }
   203 
   204 #pragma mark -
   205 
   206 - (NSString *) starsForRating:(NSNumber *)aRating withStarCharacter:(unichar)star {
   207 	int rating = aRating ? [aRating intValue] : 0;
   208 
   209 	enum {
   210 		BLACK_STAR  = 0x272F, SPACE          = 0x0020, MIDDLE_DOT   = 0x00B7,
   211 		ONE_HALF    = 0x00BD,
   212 		ONE_QUARTER = 0x00BC, THREE_QUARTERS = 0x00BE,
   213 		ONE_THIRD   = 0x2153, TWO_THIRDS     = 0x2154,
   214 		ONE_FIFTH   = 0x2155, TWO_FIFTHS     = 0x2156, THREE_FIFTHS = 0x2157, FOUR_FIFTHS   = 0x2158,
   215 		ONE_SIXTH   = 0x2159, FIVE_SIXTHS    = 0x215a,
   216 		ONE_EIGHTH  = 0x215b, THREE_EIGHTHS  = 0x215c, FIVE_EIGHTHS = 0x215d, SEVEN_EIGHTHS = 0x215e,
   217 
   218 		//rating <= 0: dot, space, dot, space, dot, space, dot, space, dot (five dots).
   219 		//higher ratings mean fewer characters. rating >= 100: five black stars.
   220 		numChars = 9,
   221 	};
   222 
   223 	static unichar fractionChars[] = {
   224 		/*0/20*/ 0,
   225 		/*1/20*/ ONE_FIFTH, TWO_FIFTHS, THREE_FIFTHS,
   226 		/*4/20 = 1/5*/ ONE_FIFTH,
   227 		/*5/20 = 1/4*/ ONE_QUARTER,
   228 		/*6/20*/ ONE_THIRD, FIVE_EIGHTHS,
   229 		/*8/20 = 2/5*/ TWO_FIFTHS, TWO_FIFTHS,
   230 		/*10/20 = 1/2*/ ONE_HALF, ONE_HALF,
   231 		/*12/20 = 3/5*/ THREE_FIFTHS,
   232 		/*13/20 = 0.65; 5/8 = 0.625*/ FIVE_EIGHTHS,
   233 		/*14/20 = 7/10*/ FIVE_EIGHTHS, //highly approximate, of course, but it's as close as I could get :)
   234 		/*15/20 = 3/4*/ THREE_QUARTERS,
   235 		/*16/20 = 4/5*/ FOUR_FIFTHS, FOUR_FIFTHS,
   236 		/*18/20 = 9/10*/ SEVEN_EIGHTHS, SEVEN_EIGHTHS, //another approximation
   237 	};
   238 
   239 	unichar starBuffer[numChars];
   240 	int     wholeStarRequirement = 20;
   241 	unsigned starsRemaining = 5U;
   242 	unsigned i = 0U;
   243 	for (; starsRemaining--; ++i) {
   244 		if (rating >= wholeStarRequirement) {
   245 			starBuffer[i] = star;
   246 			rating -= 20;
   247 		} else {
   248 			/*examples:
   249 			 *if the original rating is 95, then rating = 15, and we get 3/4.
   250 			 *if the original rating is 80, then rating = 0,  and we get MIDDLE DOT.
   251 			 */
   252 			starBuffer[i] = fractionChars[rating];
   253 			if (!starBuffer[i]) {
   254 				//add a space if this isn't the first 'star'.
   255 				if (i) starBuffer[i++] = SPACE;
   256 				starBuffer[i] = MIDDLE_DOT;
   257 			}
   258 			rating = 0; //ensure that remaining characters are MIDDLE DOT.
   259 		}
   260 	}
   261 
   262 	return [NSString stringWithCharacters:starBuffer length:i];
   263 }
   264 
   265 - (NSString *) starsForRating:(NSNumber *)aRating withStarString:(NSString *)star {
   266 	if (!star)
   267 		star = [[NSUserDefaults standardUserDefaults] stringForKey:@"Substitute for BLACK STAR"];
   268 
   269 	enum {
   270 		BLACK_STAR  = 0x2605, PINWHEEL_STAR  = 0x272F,
   271 		SPACE       = 0x0020, MIDDLE_DOT	 = 0x00B7,
   272 		ONE_HALF    = 0x00BD,
   273 		ONE_QUARTER = 0x00BC, THREE_QUARTERS = 0x00BE,
   274 		ONE_THIRD   = 0x2153, TWO_THIRDS     = 0x2154,
   275 		ONE_FIFTH   = 0x2155, TWO_FIFTHS     = 0x2156, THREE_FIFTHS = 0x2157, FOUR_FIFTHS   = 0x2158,
   276 		ONE_SIXTH   = 0x2159, FIVE_SIXTHS    = 0x215a,
   277 		ONE_EIGHTH  = 0x215b, THREE_EIGHTHS  = 0x215c, FIVE_EIGHTHS = 0x215d, SEVEN_EIGHTHS = 0x215e,
   278 	};
   279 
   280 	unsigned starLength = [star length];
   281 	if( (!star) || (starLength == 0U))
   282 		return [self starsForRating:aRating withStarCharacter:(floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_3_5) ? PINWHEEL_STAR : BLACK_STAR];
   283 	else if (starLength == 1U)
   284 		return [self starsForRating:aRating withStarCharacter:[star characterAtIndex:0U]];
   285 	else {
   286 		int rating = aRating ? [aRating intValue] : 0;
   287 		//invert.
   288 		int ratingInv = 100 - rating;
   289 
   290 		int numStars = rating / 20;
   291 		int numDots = ratingInv / 20;
   292 		unsigned fractionIndex = ratingInv % 20;
   293 
   294 		static unichar fractionChars[] = {
   295 			/*0/20*/ 0,
   296 			/*1/20*/ ONE_FIFTH, TWO_FIFTHS, THREE_FIFTHS,
   297 			/*4/20 = 1/5*/ ONE_FIFTH,
   298 			/*5/20 = 1/4*/ ONE_QUARTER,
   299 			/*6/20*/ ONE_THIRD, FIVE_EIGHTHS,
   300 			/*8/20 = 2/5*/ TWO_FIFTHS, TWO_FIFTHS,
   301 			/*10/20 = 1/2*/ ONE_HALF, ONE_HALF,
   302 			/*12/20 = 3/5*/ THREE_FIFTHS,
   303 			/*13/20 = 0.65; 5/8 = 0.625*/ FIVE_EIGHTHS,
   304 			/*14/20 = 7/10*/ FIVE_EIGHTHS, //highly approximate, of course, but it's as close as I could get :)
   305 			/*15/20 = 3/4*/ THREE_QUARTERS,
   306 			/*16/20 = 4/5*/ FOUR_FIFTHS, FOUR_FIFTHS,
   307 			/*18/20 = 9/10*/ SEVEN_EIGHTHS, SEVEN_EIGHTHS, //another approximation
   308 		};
   309 
   310 		unichar *buf = alloca(sizeof(unichar) * ((numDots * 2) - (!rating) + (fractionIndex > 0)));
   311 		unsigned i = 0U;
   312 		if (fractionIndex > 0)
   313 			buf[i++] = fractionChars[fractionIndex];
   314 
   315 		//place first dot without a leading space.
   316 		if ((!rating) && numDots) {
   317 			buf[i++] = MIDDLE_DOT;
   318 			--numDots;
   319 		}
   320 
   321 		while(numDots--) {
   322 			buf[i++] = SPACE;
   323 			buf[i++] = MIDDLE_DOT;
   324 		}
   325 
   326 		//place first star without a leading space.
   327 		NSString *firstStar = nil;
   328 		if ((starLength > 1U) && ([star characterAtIndex:0U] == SPACE)) {
   329 			NSRange range = { 1U, starLength - 1U };
   330 			firstStar = [star substringWithRange:range];
   331 		}
   332 
   333 		NSString *stars = (numStars && firstStar) ? [firstStar stringByAppendingString:[star stringByMultiplyingBy:numStars - 1]] : [star stringByMultiplyingBy:numStars];
   334 		NSString *dots = [[NSString alloc] initWithCharacters:buf length:i];
   335 		NSString *ratingString = [stars stringByAppendingString:dots];
   336 		[dots release];
   337 
   338 		return ratingString;
   339 	}
   340 }
   341 
   342 - (NSString *) starsForRating:(NSNumber *)rating {
   343 	return [self starsForRating:rating withStarString:nil];
   344 }
   345 
   346 #pragma mark -
   347 #pragma mark iTunes 4.7 notifications
   348 
   349 - (void) songChanged:(NSNotification *)aNotification {
   350 	NSString     *playerState = nil;
   351 	iTunesState   newState    = itUNKNOWN;
   352 	NSString     *newTrackURL = nil;
   353 	NSDictionary *userInfo    = [aNotification userInfo];
   354 
   355 	playerState = [[aNotification userInfo] objectForKey:@"Player State"];
   356 	if ([playerState isEqualToString:@"Paused"]) {
   357 		newState = itPAUSED;
   358 	} else if ([playerState isEqualToString:@"Stopped"]) {
   359 		newState = itSTOPPED;
   360 		trackRating = -1;
   361 		[noteDict release];
   362 		noteDict = nil;
   363 	} else if ([playerState isEqualToString:@"Playing"]){
   364 		newState = itPLAYING;
   365 		/*For radios and files, the ID is the location.
   366 		 *For iTMS purchases, it's the Store URL.
   367 		 *For Bonjour shares, we'll hash a compilation of a bunch of info.
   368 		 */
   369 		if ([userInfo objectForKey:@"Location"]) {
   370 			newTrackURL = [userInfo objectForKey:@"Location"];
   371 		} else if ([userInfo objectForKey:@"Store URL"]) {
   372 			newTrackURL = [userInfo objectForKey:@"Store URL"];
   373 		} else {
   374 			/*Get all the info we can, in such a way that the empty fields are
   375 			 *	blank rather than (null).
   376 			 *Then we hash it and turn that into our identifier string.
   377 			 *That way a track name of "file://foo" won't confuse our code later on.
   378 			 */
   379 			NSArray *keys = [[NSArray alloc] initWithObjects:@"Name", @"Artist",
   380 				@"Album", @"Composer", @"Genre", @"Year", @"Track Number",
   381 				@"Track Count", @"Disc Number", @"Disc Count", @"Total Time",
   382 				@"Stream Title", nil];
   383 			NSArray *args = [userInfo objectsForKeys:keys notFoundMarker:@""];
   384 			[keys release];
   385 			newTrackURL = [args componentsJoinedByString:@"|"];
   386 			newTrackURL = [[NSNumber numberWithUnsignedLong:[newTrackURL hash]] stringValue];
   387 		}
   388 	}
   389 
   390 	if (newTrackURL) {
   391 		NSString		*track         = nil;
   392 		NSString		*length        = nil;
   393 		NSString		*artist        = @"";
   394 		NSString		*composer	   = @"";
   395 		NSString		*album         = @"";
   396 		BOOL			compilation    = NO;
   397 		NSString		*genre         = @"";
   398 		NSNumber		*rating        = nil;
   399 		NSString		*ratingString  = nil;
   400 		NSImage			*artwork       = nil;
   401 		NSDictionary	*error         = nil;
   402 		NSString		*displayString;
   403  		NSString		*streamTitle   = @"";
   404 
   405 		artist      = [userInfo objectForKey:@"Artist"];
   406 		album       = [userInfo objectForKey:@"Album"];
   407 		composer	= [userInfo objectForKey:@"Composer"];
   408 		
   409 		if ([userInfo objectForKey:@"Track Number"]) {
   410 			track = [[NSString alloc] initWithFormat:@"%@. %@", [userInfo objectForKey:@"Track Number"], [userInfo objectForKey:@"Name"]];
   411 		} else {
   412 			//track number is nil for radio streams, ignore it
   413 			track = [userInfo objectForKey:@"Name"];
   414 		}
   415 		genre       = [userInfo objectForKey:@"Genre"];
   416 		streamTitle = [userInfo objectForKey:@"Stream Title"];
   417 		if(!streamTitle)
   418 			streamTitle = @"";
   419 
   420 		length  = [userInfo objectForKey:@"Total Time"];
   421 		// need to format a bit the length as it is returned in ms
   422 		int sec  = [length intValue] / 1000;
   423 		int hr   = sec/3600;
   424 		sec -= 3600 * hr;
   425 		int min  = sec/60;
   426 		sec -= 60 * min;
   427 		if (hr > 0)
   428 			length = [NSString stringWithFormat:@"%d:%02d:%02d", hr, min, sec];
   429 		else
   430 			length = [NSString stringWithFormat:@"%d:%02d", min, sec];
   431 
   432 		compilation = ([userInfo objectForKey:@"Compilation"] != nil);
   433 
   434 		if ([newTrackURL hasPrefix:@"file:/"] || [newTrackURL hasPrefix:@"itms:/"]) {
   435 			NSAppleEventDescriptor	*theDescriptor = [getInfoScript executeAndReturnError:&error];
   436 			NSAppleEventDescriptor  *curDescriptor;
   437 
   438 			rating = [userInfo objectForKey:@"Rating"];
   439 			ratingString = [self starsForRating:rating];
   440 			trackRating = [rating intValue];
   441 
   442 			curDescriptor = [theDescriptor descriptorAtIndex:2L];
   443 			playlistName = [curDescriptor stringValue];
   444 			curDescriptor = [theDescriptor descriptorAtIndex:1L];
   445 			const OSType type = [curDescriptor typeCodeValue];
   446 
   447 			if (type != 0)
   448 				artwork = [[[NSImage alloc] initWithData:[curDescriptor data]] autorelease];
   449 		}
   450 
   451 		//get artwork via plugins if needed (for file:/ and itms:/ id only)
   452 		if (!artwork && ![newTrackURL hasPrefix:@"http://"]) {
   453 			NSEnumerator *pluginEnum = [plugins objectEnumerator];
   454 			id <GrowlTunesPlugin> plugin;
   455 			while (!artwork && (plugin = [pluginEnum nextObject])) {
   456 				artwork = [plugin artworkForTitle:track
   457 										 byArtist:artist
   458 										  onAlbum:album
   459 									   composedBy:composer
   460 									isCompilation:(compilation ? compilation : NO)];
   461 				if (artwork && [plugin usesNetwork])
   462 					[archivePlugin archiveImage:artwork	track:track artist:artist album:album composer:composer compilation:compilation];
   463 			}
   464 		}
   465 
   466 		if (!artwork) {
   467 			if (!error && !![newTrackURL hasPrefix:@"http://"]) {
   468 				NSLog(@"Error getting artwork: %@", [error objectForKey:NSAppleScriptErrorMessage]);
   469 				if ([plugins count]) NSLog(@"No plug-ins found anything either, or you wouldn't have this message.");
   470 			}
   471 
   472 			// Use the iTunes icon instead
   473 			artwork = [[NSWorkspace sharedWorkspace] iconForApplication:@"iTunes"];
   474 			[artwork setSize:NSMakeSize(128.0f, 128.0f)];
   475 		}
   476 		if ([newTrackURL hasPrefix:@"http://"]) {
   477 			//If we're streaming music, display only the name of the station and genre
   478 			NSLog(@"new track URL: %@", newTrackURL);
   479 			if (!streamTitle) streamTitle = @"";
   480 			displayString = [[NSString alloc] initWithFormat:@"%@\n%@", streamTitle, genre];
   481 		} else {
   482 			if (!length)		length			= @"";
   483 			if (!ratingString)	ratingString	= @"";
   484 			if (!album)			album			= @"";
   485 			if (!genre)			genre			= @"";
   486 			if (!composer)		composer		= @"";
   487 			if (!artist)		artist			= composer;
   488 			
   489 			if ([composer length]) {
   490 				displayString = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ — %@\n%@ (Composed by %@)\n%@\n%@", "This is the format used for a normal song. In the order shown in English, the parameters are length, rating, artist, composer, album, and genre"), length, ratingString, artist, composer, album, genre];
   491 			} else {
   492 				displayString = [[NSString alloc] initWithFormat:NSLocalizedString(@"%@ — %@\n%@\n%@\n%@", "This is the format used for a normal song. In the order shown in English, the parameters are length, rating, artist, album, and genre"), length, ratingString, artist, album, genre];
   493 			}
   494 		}
   495 
   496 		[noteDict release];
   497 		noteDict = [[NSDictionary alloc] initWithObjectsAndKeys:
   498 			(state == itPLAYING ? ITUNES_TRACK_CHANGED : ITUNES_PLAYING), GROWL_NOTIFICATION_NAME,
   499 			APP_NAME,      GROWL_APP_NAME,
   500 			track,         GROWL_NOTIFICATION_TITLE,
   501 			displayString, GROWL_NOTIFICATION_DESCRIPTION,
   502 			APP_NAME,      GROWL_NOTIFICATION_IDENTIFIER,
   503 			[artwork TIFFRepresentation], GROWL_NOTIFICATION_ICON,
   504 			nil];
   505 		[displayString release];
   506 
   507 		if (![newTrackURL isEqualToString:trackURL] || [newTrackURL hasPrefix:@"http://"]) { // this is different from previous notification, or it's a stream
   508 			// Tell Growl
   509 			[GrowlApplicationBridge notifyWithDictionary:noteDict];
   510 
   511 			// Recent Tracks
   512 			if (streamTitle && [streamTitle length]) {
   513 				//streamed song - insert streamTitle (song name) rather than track (radio name)
   514 				[self addTuneToRecentTracks:streamTitle fromPlaylist:playlistName];
   515 			} else {
   516 				[self addTuneToRecentTracks:track fromPlaylist:playlistName];
   517 			}
   518 		}
   519 
   520 		// set up us some state for next time
   521 		state = newState;
   522 		[trackURL release];
   523 		trackURL = [newTrackURL retain];
   524 	}
   525 }
   526 
   527 #pragma mark Poll timer
   528 
   529 - (void) poll:(NSTimer *)timer {
   530 #pragma unused(timer)
   531 	NSDictionary			*error = nil;
   532 	NSAppleEventDescriptor	*theDescriptor = [pollScript executeAndReturnError:&error];
   533 	NSAppleEventDescriptor  *curDescriptor;
   534 	NSString				*playerState;
   535 	iTunesState				newState = itUNKNOWN;
   536 	int						newTrackID = -1;
   537 
   538 	curDescriptor = [theDescriptor descriptorAtIndex:1L];
   539 	playerState = [curDescriptor stringValue];
   540 
   541 	if ([playerState isEqualToString:@"paused"]) {
   542 		newState = itPAUSED;
   543 	} else if ([playerState isEqualToString:@"stopped"]) {
   544 		newState = itSTOPPED;
   545 		trackRating = -1;
   546 		[noteDict release];
   547 		noteDict = nil;
   548 	} else {
   549 		newState = itPLAYING;
   550 		newTrackID = [curDescriptor int32Value];
   551 	}
   552 
   553 	if (state == itUNKNOWN) {
   554 		state = newState;
   555 		trackID = newTrackID;
   556 		return;
   557 	}
   558 
   559 	if (newTrackID) {
   560 		NSString		*track = nil;
   561 		NSString		*length = nil;
   562 		NSString		*artist = nil;
   563 		NSString		*album = nil;
   564 		NSString		*composer = nil;
   565 		BOOL			 compilation = NO;
   566 		NSString		*genre = nil;
   567 		NSNumber		*rating = nil;
   568 		NSString		*ratingString = nil;
   569 		NSImage			*artwork = nil;
   570 
   571 		curDescriptor = [theDescriptor descriptorAtIndex:10L];
   572 		playlistName = [curDescriptor stringValue];
   573 
   574 		if ((curDescriptor = [theDescriptor descriptorAtIndex:2L]))
   575 			track = [curDescriptor stringValue];
   576 
   577 		if ((curDescriptor = [theDescriptor descriptorAtIndex:3L]))
   578 			length = [curDescriptor stringValue];
   579 
   580 		if ((curDescriptor = [theDescriptor descriptorAtIndex:4L]))
   581 			artist = [curDescriptor stringValue];
   582 
   583 		if ((curDescriptor = [theDescriptor descriptorAtIndex:5L]))
   584 			album = [curDescriptor stringValue];
   585 
   586 		if ((curDescriptor = [theDescriptor descriptorAtIndex:6L]))
   587 			composer = [curDescriptor stringValue];
   588 
   589 		if ((curDescriptor = [theDescriptor descriptorAtIndex:7L]))
   590 			compilation = (BOOL)[curDescriptor booleanValue];
   591 
   592 		if ((curDescriptor = [theDescriptor descriptorAtIndex:8L]))
   593 			genre = [curDescriptor stringValue];
   594 
   595 		if ((curDescriptor = [theDescriptor descriptorAtIndex:9L])) {
   596 			trackRating = [[curDescriptor stringValue] intValue];
   597 			rating = [NSNumber numberWithInt:trackRating < 0 ? 0 : trackRating];
   598 			ratingString = [self starsForRating:rating];
   599 		}
   600 
   601 		curDescriptor = [theDescriptor descriptorAtIndex:10L];
   602 		const OSType type = [curDescriptor typeCodeValue];
   603 
   604 		if (type != 'null') {
   605 			artwork = [[[NSImage alloc] initWithData:[curDescriptor data]] autorelease];
   606 		} else {
   607 			NSEnumerator *pluginEnum = [plugins objectEnumerator];
   608 			id <GrowlTunesPlugin> plugin;
   609 			while (!artwork && (plugin = [pluginEnum nextObject])) {
   610 				artwork = [plugin artworkForTitle:track
   611 										 byArtist:artist
   612 										  onAlbum:album
   613 									   composedBy:composer
   614 									isCompilation:compilation];
   615 				if (artwork && [plugin usesNetwork])
   616 					[archivePlugin archiveImage:artwork	track:track artist:artist album:album composer:composer compilation:compilation];
   617 			}
   618 		}
   619 
   620 		if (!artwork) {
   621 			if (!error) {
   622 				NSLog(@"Error getting artwork: %@", [error objectForKey:NSAppleScriptErrorMessage]);
   623 				if ([plugins count]) NSLog(@"No plug-ins found anything either, or you wouldn't have this message.");
   624 			}
   625 
   626 			// Use the iTunes icon instead
   627 			artwork = [[NSWorkspace sharedWorkspace] iconForApplication:@"iTunes"];
   628 			[artwork setSize:NSMakeSize(128.0f, 128.0f)];
   629 		}
   630 
   631 		NSString *description = [[NSString alloc] initWithFormat:@"%@ - %@\n%@ (Composed by %@)\n%@\n%@", length, ratingString, artist, composer, album, genre];
   632 		[noteDict release];
   633 		noteDict = [[NSDictionary alloc] initWithObjectsAndKeys:
   634 			(state == itPLAYING ? ITUNES_TRACK_CHANGED : ITUNES_PLAYING), GROWL_NOTIFICATION_NAME,
   635 			APP_NAME,                     GROWL_APP_NAME,
   636 			track,                        GROWL_NOTIFICATION_TITLE,
   637 			description,                  GROWL_NOTIFICATION_DESCRIPTION,
   638 			APP_NAME,                     GROWL_NOTIFICATION_IDENTIFIER,
   639 			[artwork TIFFRepresentation], GROWL_NOTIFICATION_ICON,
   640 			nil];
   641 		[description release];
   642 
   643 		if (trackID != newTrackID) { // this is different from previous note
   644 			// Tell growl
   645 			[GrowlApplicationBridge notifyWithDictionary:noteDict];
   646 
   647 			// Recent Tracks
   648 			[self addTuneToRecentTracks:track fromPlaylist:playlistName];
   649 		}
   650 
   651 		// set up us some state for next time
   652 		state = newState;
   653 		trackID = newTrackID;
   654 	}
   655 }
   656 
   657 - (void) showCurrentTrack {
   658 	if (noteDict)
   659 		[GrowlApplicationBridge notifyWithDictionary:noteDict];
   660 }
   661 
   662 - (void) startTimer {
   663 	if (!pollTimer) {
   664 		pollTimer = [[NSTimer scheduledTimerWithTimeInterval:pollInterval
   665 													  target:self
   666 													selector:@selector(poll:)
   667 													userInfo:nil
   668 													 repeats:YES] retain];
   669 		NSLog(@"%@", @"Polling started - upgrade to iTunes 4.7 or later already, would you?!");
   670 		[self poll:nil];
   671 	}
   672 }
   673 
   674 - (void) stopTimer {
   675 	if (pollTimer){
   676 		[pollTimer invalidate];
   677 		[pollTimer release];
   678 		pollTimer = nil;
   679 		NSLog(@"%@", @"Polling stopped");
   680 	}
   681 }
   682 
   683 #pragma mark Status item
   684 
   685 - (void) createStatusItem {
   686 	if (!statusItem) {
   687 		NSStatusBar *statusBar = [NSStatusBar systemStatusBar];
   688 		statusItem = [[statusBar statusItemWithLength:NSSquareStatusItemLength] retain];
   689 		if (statusItem) {
   690 			[statusItem setMenu:[self statusItemMenu]];
   691 			[statusItem setHighlightMode:YES];
   692 			[statusItem setImage:[NSImage imageNamed:@"growlTunes.png"]];
   693 			[statusItem setAlternateImage:[NSImage imageNamed:@"growlTunes-selected.png"]];
   694 			[statusItem setToolTip:NSLocalizedString(@"GrowlTunes’ control status item.", /*comment*/ nil)];
   695 		}
   696 	}
   697 }
   698 
   699 - (void) tearDownStatusItem {
   700 	if (statusItem) {
   701 		[[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; //otherwise we leave a hole
   702 		[statusItem release];
   703 		statusItem = nil;
   704 	}
   705 }
   706 
   707 - (NSMenu *) statusItemMenu {
   708 	NSMenu *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"GrowlTunes"];
   709 	if (menu) {
   710 		NSMenuItem * item;
   711 		NSString *empty = @""; //used for the key equivalent of all the menu items.
   712 
   713 		item = [menu addItemWithTitle:NSLocalizedString(@"Online Help", @"") action:@selector(onlineHelp:) keyEquivalent:empty];
   714 		[item setTarget:self];
   715 		[item setTag:onlineHelpTag];
   716 		[item setToolTip:NSLocalizedString(@"Opens the webpage for GrowlTunes help on the Growl website in your selected browser.", "Online help's tooltip")];
   717 
   718 		item = [NSMenuItem separatorItem];
   719 		[menu addItem:item];
   720 
   721 		item = [menu addItemWithTitle:@"iTunes" action:NULL keyEquivalent:empty];
   722 
   723 		// Set us up a submenu
   724 		[item setSubmenu:[self buildiTunesSubmenu]];
   725 
   726 		// The rating submenu
   727 		item = [menu addItemWithTitle:NSLocalizedString(@"Rating", @"") action:NULL keyEquivalent:empty];
   728 		[item setSubmenu:[self buildRatingSubmenu]];
   729 
   730 		// Back to our regularly scheduled Status Menu
   731 		item = [NSMenuItem separatorItem];
   732 		[menu addItem:item];
   733 
   734 		item = [menu addItemWithTitle:NSLocalizedString(@"Quit GrowlTunes", @"") action:@selector(quitGrowlTunes:) keyEquivalent:empty];
   735 		[item setTarget:self];
   736 		[item setTag:quitGrowlTunesTag];
   737 		item = [menu addItemWithTitle:NSLocalizedString(@"Quit Both", @"") action:@selector(quitBoth:) keyEquivalent:empty];
   738 		[item setTarget:self];
   739 		[item setTag:quitBothTag];
   740 		[item setToolTip:NSLocalizedString(@"Quits both iTunes and GrowlTunes", /*comment*/ nil)];
   741 
   742 		if (polling) {
   743 			item = [NSMenuItem separatorItem];
   744 			[menu addItem:item];
   745 
   746 			item = [menu addItemWithTitle:@"Toggle Polling" action:@selector(togglePolling:) keyEquivalent:empty];
   747 			[item setTarget:self];
   748 			[item setTag:togglePollingTag];
   749 			[item setToolTip:NSLocalizedString(@"Turns on or off GrowlTunes' periodic asking of iTunes for track information.", "Toggle polling tooltip")];
   750 		}
   751 	}
   752 
   753 	return [menu autorelease];
   754 }
   755 
   756 - (IBAction) togglePolling:(id)sender {
   757 #pragma unused(sender)
   758 	if (pollTimer)
   759 		[self stopTimer];
   760 	else
   761 		[self startTimer];
   762 }
   763 
   764 - (NSMenu *) buildiTunesSubmenu {
   765 	NSMenuItem * item;
   766 	if (!iTunesSubMenu)
   767 		iTunesSubMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"iTunes"] autorelease];
   768 
   769 	// Out with the old
   770 	NSArray *items = [iTunesSubMenu itemArray];
   771 	NSEnumerator *itemEnumerator = [items objectEnumerator];
   772 	while ((item = [itemEnumerator nextObject]))
   773 		[iTunesSubMenu removeItem:item];
   774 
   775 	// In with the new
   776 	item = [iTunesSubMenu addItemWithTitle:NSLocalizedString(@"Recently Played Tunes", @"") action:NULL keyEquivalent:@""];
   777 	NSEnumerator *tunesEnumerator = [recentTracks objectEnumerator];
   778 	NSDictionary *aTuneDict = nil;
   779 	int k = 0;
   780 
   781 	while ((aTuneDict = [tunesEnumerator nextObject])) {
   782 		item = [iTunesSubMenu addItemWithTitle:[aTuneDict objectForKey:@"name"]
   783 										action:@selector(jumpToTune:)
   784 								 keyEquivalent:@""];
   785 		[item setTarget:self];
   786 		[item setIndentationLevel:1];
   787 		[item setTag:k++];
   788 		[item setToolTip:NSLocalizedString(@"Tells iTunes to play this track again.", "Tooltip for recent tracks")];
   789 	}
   790 
   791 	[iTunesSubMenu addItem:[NSMenuItem separatorItem]];
   792 	item = [iTunesSubMenu addItemWithTitle:@"Launch iTunes" action:@selector(launchQuitiTunes:) keyEquivalent:@""];
   793 	[item setTarget:self];
   794 	[item setTag:launchQuitiTunesTag];
   795 	//tooltip set by validateMenuItem
   796 
   797 	return iTunesSubMenu;
   798 }
   799 
   800 - (NSMenu *) buildRatingSubmenu {
   801 	NSMenuItem * item;
   802 	if (!ratingSubMenu) {
   803 		ratingSubMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"Rating"] autorelease];
   804 		NSString *rating0 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   805 		NSString *rating1 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   806 		NSString *rating2 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   807 		NSString *rating3 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86"];
   808 		NSString *rating4 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86"];
   809 		NSString *rating5 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85"];
   810 		item = [ratingSubMenu addItemWithTitle:rating0 action:@selector(setRating:) keyEquivalent:@""];
   811 		[item setTarget:self];
   812 		[item setTag:ratingTag+0];
   813 		item = [ratingSubMenu addItemWithTitle:rating1 action:@selector(setRating:) keyEquivalent:@""];
   814 		[item setTarget:self];
   815 		[item setTag:ratingTag+1];
   816 		item = [ratingSubMenu addItemWithTitle:rating2 action:@selector(setRating:) keyEquivalent:@""];
   817 		[item setTarget:self];
   818 		[item setTag:ratingTag+2];
   819 		item = [ratingSubMenu addItemWithTitle:rating3 action:@selector(setRating:) keyEquivalent:@""];
   820 		[item setTarget:self];
   821 		[item setTag:ratingTag+3];
   822 		item = [ratingSubMenu addItemWithTitle:rating4 action:@selector(setRating:) keyEquivalent:@""];
   823 		[item setTarget:self];
   824 		[item setTag:ratingTag+4];
   825 		item = [ratingSubMenu addItemWithTitle:rating5 action:@selector(setRating:) keyEquivalent:@""];
   826 		[item setTarget:self];
   827 		[item setTag:ratingTag+5];
   828 		[rating0 release];
   829 		[rating1 release];
   830 		[rating2 release];
   831 		[rating3 release];
   832 		[rating4 release];
   833 		[rating5 release];
   834 	}
   835 
   836 	return ratingSubMenu;
   837 }
   838 
   839 - (BOOL) validateMenuItem:(NSMenuItem *)item {
   840 	BOOL retVal = YES;
   841 	int tag = [item tag];
   842 	int i;
   843 
   844 	switch (tag) {
   845 		case launchQuitiTunesTag:
   846 			if ([self iTunesIsRunning])
   847 				[item setTitle:NSLocalizedString(@"Quit iTunes", @"")];
   848 			else
   849 				[item setTitle:NSLocalizedString(@"Launch iTunes", @"")];
   850 			break;
   851 
   852 		case quitBothTag:
   853 			retVal = [self iTunesIsRunning];
   854 			break;
   855 
   856 		case togglePollingTag:
   857 			if (pollTimer) {
   858 				[item setTitle:NSLocalizedString(@"Stop Polling", @"")];
   859 				[item setToolTip:NSLocalizedString(@"Stops GrowlTunes from asking iTunes for track information. You will then no longer receive Growl notifications from GrowlTunes.", "Tooltip for 'stop polling'")];
   860 			} else {
   861 				[item setTitle:NSLocalizedString(@"Start Polling", @"")];
   862 				[item setToolTip:NSLocalizedString(@"Begins asking iTunes for track information. You will then start receiving Growl notifications from GrowlTunes.", "Tooltip for 'start polling'")];
   863 			}
   864 
   865 		case quitGrowlTunesTag:
   866 		case onlineHelpTag:
   867 			break;
   868 
   869 		case ratingTag+0:
   870 		case ratingTag+1:
   871 		case ratingTag+2:
   872 		case ratingTag+3:
   873 		case ratingTag+4:
   874 		case ratingTag+5:
   875 			i = (tag-ratingTag)*20;
   876 			if (trackRating < 0) {
   877 				retVal = NO;
   878 				[item setState:NSOffState];
   879 			} else if (trackRating >= i && trackRating < i+20)
   880 				[item setState:NSOnState];
   881 			else
   882 				[item setState:NSOffState];
   883 			break;
   884 	}
   885 
   886 	return retVal;
   887 }
   888 
   889 - (IBAction) onlineHelp:(id)sender{
   890 #pragma unused(sender)
   891 	[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:ONLINE_HELP_URL]];
   892 }
   893 
   894 - (void) addTuneToRecentTracks:(NSString *)inTune fromPlaylist:(NSString *)inPlaylist {
   895 	NSNumber *recentTrackCountNum = [[NSUserDefaults standardUserDefaults] objectForKey:RECENT_TRACK_COUNT_KEY];
   896 	unsigned trackLimit = recentTrackCountNum ? [recentTrackCountNum unsignedIntValue] : DEFAULT_RECENT_TRACKS_LIMIT;
   897 	NSDictionary *tuneDict = [[NSDictionary alloc] initWithObjectsAndKeys:
   898 		inTune,     @"name",
   899 		inPlaylist, @"playlist",
   900 		nil];
   901 	signed long delta = ([recentTracks count] + 1U) - (signed long)trackLimit;
   902 	if (delta > 0L)
   903 		[recentTracks removeObjectsInRange:NSMakeRange(0U, delta)];
   904 	[recentTracks addObject:tuneDict];
   905 	[tuneDict release];
   906 
   907 	if (![[NSUserDefaults standardUserDefaults] boolForKey:NO_MENU_KEY])
   908 		[self buildiTunesSubmenu];
   909 }
   910 
   911 - (IBAction) quitGrowlTunes:(id)sender {
   912 	[NSApp terminate:sender];
   913 }
   914 
   915 - (IBAction) launchQuitiTunes:(id)sender {
   916 #pragma unused(sender)
   917 	if (![self quitiTunes]) {
   918 		//quit failed, so it wasn't running: launch it.
   919 		[[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:ITUNES_BUNDLE_ID
   920 															 options:NSWorkspaceLaunchDefault
   921 									  additionalEventParamDescriptor:nil
   922 													launchIdentifier:NULL];
   923 	}
   924 }
   925 
   926 - (IBAction) quitBoth:(id)sender {
   927 	[self quitiTunes];
   928 	[self quitGrowlTunes:sender];
   929 }
   930 
   931 - (BOOL) quitiTunes {
   932 	NSDictionary *iTunes = [[NSWorkspace sharedWorkspace] launchedApplicationWithIdentifier:ITUNES_BUNDLE_ID];
   933 	BOOL success = (iTunes != nil);
   934 	if (success) {
   935 		//first disarm the timer. we don't want to launch iTunes right after we quit it if the timer fires.
   936 		[self stopTimer];
   937 
   938 		//now quit iTunes.
   939 		NSAppleEventDescriptor *target = [[NSAppleEventDescriptor alloc] initWithDescriptorType:typeApplicationBundleID
   940 																						   data:[ITUNES_BUNDLE_ID dataUsingEncoding:NSUTF8StringEncoding]];
   941 		NSAppleEventDescriptor *event = [[NSAppleEventDescriptor alloc] initWithEventClass:kCoreEventClass
   942 																				   eventID:kAEQuitApplication
   943 																		  targetDescriptor:target
   944 																				  returnID:kAutoGenerateReturnID
   945 																			 transactionID:kAnyTransactionID];
   946 		OSStatus err = AESendMessage([event aeDesc],
   947 									 /*reply*/ NULL,
   948 									 /*sendMode*/ kAENoReply | kAENeverInteract | kAEDontRecord,
   949 									 kAEDefaultTimeout);
   950 		[target release];
   951 		[event release];
   952 		success = ((err == noErr) || (err == procNotFound));
   953 		//XXX this should be an alert panel (with a better message)
   954 		if (!success)
   955 			NSLog(@"Could not quit iTunes: AESendMessage returned %li", (long)err);
   956 	}
   957 	return success;
   958 }
   959 
   960 - (IBAction) setRating:(id)sender {
   961 	OSStatus err;
   962 	AppleEvent event;
   963 	AEDesc currentTrackObject;
   964 	AEDesc ratingProperty;
   965 	AEDesc trackDescriptor;
   966 	AEDesc ratingDescriptor;
   967 	AEDesc ratingValue;
   968 	AEDesc target;
   969 	AEDesc nullDescriptor = {typeNull, nil};
   970 	DescType trackType = 'pTrk';
   971 	DescType ratingType = 'pRte';
   972 	NSData *bundleID = [ITUNES_BUNDLE_ID dataUsingEncoding:NSUTF8StringEncoding];
   973 	int rating = ([sender tag] - ratingTag) * 20;
   974 
   975 	err = AECreateDesc(typeType, &trackType, sizeof(trackType), &trackDescriptor);
   976 	if (err != noErr)
   977 		NSLog(@"AECreateDesc returned %li", (long)err);
   978 	err = AECreateDesc(typeType, &ratingType, sizeof(ratingType), &ratingDescriptor);
   979 	if (err != noErr)
   980 		NSLog(@"AECreateDesc returned %li", (long)err);
   981 	err = AECreateDesc(typeSInt32, &rating, sizeof(rating), &ratingValue);
   982 	if (err != noErr)
   983 		NSLog(@"AECreateDesc returned %li", (long)err);
   984 	err = AECreateDesc(typeApplicationBundleID, [bundleID bytes], [bundleID length], &target);
   985 	if (err != noErr)
   986 		NSLog(@"AECreateDesc returned %li", (long)err);
   987 
   988 	err = CreateObjSpecifier(typeProperty,
   989 							 &nullDescriptor,
   990 							 formPropertyID,
   991 							 &trackDescriptor,
   992 							 TRUE,
   993 							 &currentTrackObject);
   994 	if (err != noErr)
   995 		NSLog(@"CreateObjSpecifier returned %li", (long)err);
   996 	err = CreateObjSpecifier(typeProperty,
   997 							 &currentTrackObject,
   998 							 formPropertyID,
   999 							 &ratingDescriptor,
  1000 							 TRUE,
  1001 							 &ratingProperty);
  1002 	if (err != noErr)
  1003 		NSLog(@"CreateObjSpecifier returned %li", (long)err);
  1004 
  1005 	err = AECreateAppleEvent('core', 'setd', &target, kAutoGenerateReturnID, kAnyTransactionID, &event);
  1006 	if (err != noErr)
  1007 		NSLog(@"AECreateAppleEvent returned %li", (long)err);
  1008 	err = AEPutParamDesc(&event, 'data', &ratingValue);
  1009 	if (err != noErr)
  1010 		NSLog(@"AEPutParamDesc returned %li", (long)err);
  1011 	err = AEPutParamDesc(&event, keyDirectObject, &ratingProperty);
  1012 	if (err != noErr)
  1013 		NSLog(@"AEPutParamDesc returned %li", (long)err);
  1014 
  1015 	err = AESendMessage(&event,
  1016 						/*reply*/ NULL,
  1017 						/*sendMode*/ kAENoReply | kAENeverInteract | kAEDontRecord,
  1018 						kAEDefaultTimeout);
  1019 	if (err != noErr)
  1020 		NSLog(@"AESendMessage returned %li", (long)err);
  1021 
  1022 	AEDisposeDesc(&event);
  1023 	AEDisposeDesc(&target);
  1024 	AEDisposeDesc(&ratingValue);
  1025 	AEDisposeDesc(&ratingProperty);
  1026 
  1027 	trackRating = rating;
  1028 }
  1029 
  1030 #pragma mark AppleScript
  1031 
  1032 - (NSAppleScript *) appleScriptNamed:(NSString *)name {
  1033 	NSURL			*url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:name ofType:@"scpt"]];
  1034 	NSDictionary	*error;
  1035 
  1036 	return [[NSAppleScript alloc] initWithContentsOfURL:url error:&error];
  1037 }
  1038 
  1039 - (BOOL) iTunesIsRunning {
  1040 	return [[NSWorkspace sharedWorkspace] launchedApplicationWithIdentifier:ITUNES_BUNDLE_ID] != nil;
  1041 }
  1042 
  1043 - (void) jumpToTune:(id) sender {
  1044 	NSDictionary *tuneDict = [recentTracks objectAtIndex:[sender tag]];
  1045 	NSString *jumpScript = [[NSString alloc] initWithFormat:@"tell application \"iTunes\"\nplay track \"%@\" of playlist \"%@\"\nend tell",
  1046 									[tuneDict objectForKey:@"name"],
  1047 									[tuneDict objectForKey:@"playlist"]];
  1048 	NSAppleScript *as = [[NSAppleScript alloc] initWithSource:jumpScript];
  1049 	[as executeAndReturnError:NULL];
  1050 	[as release];
  1051 	[jumpScript release];
  1052 }
  1053 
  1054 - (void) handleAppLaunch:(NSNotification *)notification {
  1055 	if ([ITUNES_BUNDLE_ID caseInsensitiveCompare:[[notification userInfo] objectForKey:@"NSApplicationBundleIdentifier"]] == NSOrderedSame)
  1056 		[self startTimer];
  1057 }
  1058 
  1059 - (void) handleAppQuit:(NSNotification *)notification {
  1060 	if ([ITUNES_BUNDLE_ID caseInsensitiveCompare:[[notification userInfo] objectForKey:@"NSApplicationBundleIdentifier"]] == NSOrderedSame)
  1061 		[self stopTimer];
  1062 }
  1063 
  1064 #pragma mark Plug-ins
  1065 
  1066 // This function is used to sort plugins, trying first the local ones, and then the network ones
  1067 static int comparePlugins(id <GrowlTunesPlugin> plugin1, id <GrowlTunesPlugin> plugin2, void *context) {
  1068 #pragma unused(context)
  1069 	BOOL b1 = [plugin1 usesNetwork];
  1070 	BOOL b2 = [plugin2 usesNetwork];
  1071 	if (b2 && !b1) //b1 is local; b2 is network
  1072 		return NSOrderedAscending;
  1073 	else if (b1 && !b2) //b1 is network; b2 is local
  1074 		return NSOrderedDescending;
  1075 	else //both have the same behaviour
  1076 		return NSOrderedAscending;
  1077 }
  1078 
  1079 - (NSMutableArray *) loadPlugins {
  1080 	NSMutableArray *newPlugins = [[NSMutableArray alloc] init];
  1081 	NSMutableArray *lastPlugins = [[NSMutableArray alloc] init];
  1082 	if (newPlugins) {
  1083 		NSBundle *myBundle = [NSBundle mainBundle];
  1084 		NSString *pluginsPath = [myBundle builtInPlugInsPath];
  1085 		NSString *applicationSupportPath = [@"~/Library/Application Support/GrowlTunes/Plugins" stringByExpandingTildeInPath];
  1086 		NSArray *loadPathsArray = [NSArray arrayWithObjects:pluginsPath, applicationSupportPath, nil];
  1087 		NSEnumerator *loadPathsEnum = [loadPathsArray objectEnumerator];
  1088 		NSString *loadPath;
  1089 		NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1090 		static NSString *pluginPathExtension = @"plugin";
  1091 
  1092 		while ((loadPath = [loadPathsEnum nextObject])) {
  1093 			NSEnumerator *pluginEnum = [[[NSFileManager defaultManager] directoryContentsAtPath:loadPath] objectEnumerator];
  1094 			NSString *curPath;
  1095 
  1096 			while ((curPath = [pluginEnum nextObject])) {
  1097 				if ([[curPath pathExtension] isEqualToString:pluginPathExtension]) {
  1098 					curPath = [pluginsPath stringByAppendingPathComponent:curPath];
  1099 					NSBundle *plugin = [NSBundle bundleWithPath:curPath];
  1100 
  1101 					if ([plugin load]) {
  1102 						Class principalClass = [plugin principalClass];
  1103 
  1104 						if ([principalClass conformsToProtocol:@protocol(GrowlTunesPlugin)]) {
  1105 							id instance = [[principalClass alloc] init];
  1106 							[newPlugins addObject:instance];
  1107 
  1108 							if (!archivePlugin && ([principalClass conformsToProtocol:@protocol(GrowlTunesPluginArchive)])) {
  1109 								archivePlugin = [instance retain];
  1110 //								NSLog(@"plug-in %@ is archive-Plugin with id %p", [curPath lastPathComponent], instance);
  1111 							}
  1112 							[instance release];
  1113 //							NSLog(@"Loaded plug-in \"%@\" with id %p", [curPath lastPathComponent], instance);
  1114 						} else
  1115 							NSLog(@"Loaded plug-in \"%@\" does not conform to protocol", [curPath lastPathComponent]);
  1116 					} else
  1117 						NSLog(@"Could not load plug-in \"%@\"", [curPath lastPathComponent]);
  1118 				}
  1119 			}
  1120 		}
  1121 
  1122 		[pool release];
  1123 		[newPlugins addObjectsFromArray:lastPlugins];
  1124 		[lastPlugins release];
  1125 		[newPlugins autorelease];
  1126 	}
  1127 
  1128 	// sort the plugins, putting the one that uses network last
  1129 	return (NSMutableArray *)[newPlugins sortedArrayUsingFunction:comparePlugins context:NULL];
  1130 }
  1131 
  1132 @end
  1133 
  1134 @implementation NSObject(GrowlTunesDummyPlugin)
  1135 
  1136 - (NSImage *) artworkForTitle:(NSString *)track
  1137 					byArtist:(NSString *)artist
  1138 					 onAlbum:(NSString *)album
  1139 			   isCompilation:(BOOL)compilation
  1140 {
  1141 #pragma unused(track,artist,album,compilation)
  1142 	NSLog(@"Dummy plug-in %p called for artwork", self);
  1143 	return nil;
  1144 }
  1145 
  1146 @end
  1147 
  1148 @implementation NSString (GrowlTunesMultiplicationAdditions)
  1149 
  1150 - (NSString *)stringByMultiplyingBy:(unsigned)multi {
  1151 	unsigned length = [self length];
  1152 	unsigned length_multi = length * multi;
  1153 
  1154 	unichar *buf = malloc(sizeof(unichar) * length_multi);
  1155 	if (!buf)
  1156 		return nil;
  1157 
  1158 	for (unsigned i = 0U; i < multi; ++i)
  1159 		[self getCharacters:&buf[length * i]];
  1160 
  1161 	NSString *result = [NSString stringWithCharacters:buf length:length_multi];
  1162 	free(buf);
  1163 	return result;
  1164 }
  1165 
  1166 @end