Extras/GrowlTunes/GrowlTunesController.m
author Peter Hosey <hg@boredzo.org>
Fri Aug 14 10:55:41 2009 -0700 (2009-08-14)
changeset 4288 5f22e70ed081
parent 4287 c6a0bfc2f06a
child 4289 f3197c12c013
permissions -rw-r--r--
Don't post a notification when the stream gives us a meaningless change that won't show up in the notification. Fixes GrowlTunes spamming the user when iTunes is playing a Live365 stream.
     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:(NSUInteger)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 		BOOL URLChanged = [newTrackURL isEqualToString:trackURL];
   508 		BOOL isStream = [newTrackURL hasPrefix:@"http://"];
   509 		BOOL descriptionChanged = !(lastPostedDescription && [lastPostedDescription isEqualToString:displayString]);
   510 		if (URLChanged || (isStream && descriptionChanged)) {
   511 			// Tell Growl
   512 			[GrowlApplicationBridge notifyWithDictionary:noteDict];
   513 
   514 			// Recent Tracks
   515 			if (streamTitle && [streamTitle length]) {
   516 				//streamed song - insert streamTitle (song name) rather than track (radio name)
   517 				[self addTuneToRecentTracks:streamTitle fromPlaylist:playlistName];
   518 			} else {
   519 				[self addTuneToRecentTracks:track fromPlaylist:playlistName];
   520 			}
   521 		}
   522 
   523 		// set up us some state for next time
   524 		state = newState;
   525 		[trackURL release];
   526 		trackURL = [newTrackURL retain];
   527 		[lastPostedDescription release];
   528 		lastPostedDescription = [displayString retain];
   529 	}
   530 }
   531 
   532 #pragma mark Poll timer
   533 
   534 - (void) poll:(NSTimer *)timer {
   535 #pragma unused(timer)
   536 	NSDictionary			*error = nil;
   537 	NSAppleEventDescriptor	*theDescriptor = [pollScript executeAndReturnError:&error];
   538 	NSAppleEventDescriptor  *curDescriptor;
   539 	NSString				*playerState;
   540 	iTunesState				newState = itUNKNOWN;
   541 	int						newTrackID = -1;
   542 
   543 	curDescriptor = [theDescriptor descriptorAtIndex:1L];
   544 	playerState = [curDescriptor stringValue];
   545 
   546 	if ([playerState isEqualToString:@"paused"]) {
   547 		newState = itPAUSED;
   548 	} else if ([playerState isEqualToString:@"stopped"]) {
   549 		newState = itSTOPPED;
   550 		trackRating = -1;
   551 		[noteDict release];
   552 		noteDict = nil;
   553 	} else {
   554 		newState = itPLAYING;
   555 		newTrackID = [curDescriptor int32Value];
   556 	}
   557 
   558 	if (state == itUNKNOWN) {
   559 		state = newState;
   560 		trackID = newTrackID;
   561 		return;
   562 	}
   563 
   564 	if (newTrackID) {
   565 		NSString		*track = nil;
   566 		NSString		*length = nil;
   567 		NSString		*artist = nil;
   568 		NSString		*album = nil;
   569 		NSString		*composer = nil;
   570 		BOOL			 compilation = NO;
   571 		NSString		*genre = nil;
   572 		NSNumber		*rating = nil;
   573 		NSString		*ratingString = nil;
   574 		NSImage			*artwork = nil;
   575 
   576 		curDescriptor = [theDescriptor descriptorAtIndex:10L];
   577 		playlistName = [curDescriptor stringValue];
   578 
   579 		if ((curDescriptor = [theDescriptor descriptorAtIndex:2L]))
   580 			track = [curDescriptor stringValue];
   581 
   582 		if ((curDescriptor = [theDescriptor descriptorAtIndex:3L]))
   583 			length = [curDescriptor stringValue];
   584 
   585 		if ((curDescriptor = [theDescriptor descriptorAtIndex:4L]))
   586 			artist = [curDescriptor stringValue];
   587 
   588 		if ((curDescriptor = [theDescriptor descriptorAtIndex:5L]))
   589 			album = [curDescriptor stringValue];
   590 
   591 		if ((curDescriptor = [theDescriptor descriptorAtIndex:6L]))
   592 			composer = [curDescriptor stringValue];
   593 
   594 		if ((curDescriptor = [theDescriptor descriptorAtIndex:7L]))
   595 			compilation = (BOOL)[curDescriptor booleanValue];
   596 
   597 		if ((curDescriptor = [theDescriptor descriptorAtIndex:8L]))
   598 			genre = [curDescriptor stringValue];
   599 
   600 		if ((curDescriptor = [theDescriptor descriptorAtIndex:9L])) {
   601 			trackRating = [[curDescriptor stringValue] intValue];
   602 			rating = [NSNumber numberWithInt:trackRating < 0 ? 0 : trackRating];
   603 			ratingString = [self starsForRating:rating];
   604 		}
   605 
   606 		curDescriptor = [theDescriptor descriptorAtIndex:10L];
   607 		const OSType type = [curDescriptor typeCodeValue];
   608 
   609 		if (type != 'null') {
   610 			artwork = [[[NSImage alloc] initWithData:[curDescriptor data]] autorelease];
   611 		} else {
   612 			NSEnumerator *pluginEnum = [plugins objectEnumerator];
   613 			id <GrowlTunesPlugin> plugin;
   614 			while (!artwork && (plugin = [pluginEnum nextObject])) {
   615 				artwork = [plugin artworkForTitle:track
   616 										 byArtist:artist
   617 										  onAlbum:album
   618 									   composedBy:composer
   619 									isCompilation:compilation];
   620 				if (artwork && [plugin usesNetwork])
   621 					[archivePlugin archiveImage:artwork	track:track artist:artist album:album composer:composer compilation:compilation];
   622 			}
   623 		}
   624 
   625 		if (!artwork) {
   626 			if (!error) {
   627 				NSLog(@"Error getting artwork: %@", [error objectForKey:NSAppleScriptErrorMessage]);
   628 				if ([plugins count]) NSLog(@"No plug-ins found anything either, or you wouldn't have this message.");
   629 			}
   630 
   631 			// Use the iTunes icon instead
   632 			artwork = [[NSWorkspace sharedWorkspace] iconForApplication:@"iTunes"];
   633 			[artwork setSize:NSMakeSize(128.0f, 128.0f)];
   634 		}
   635 
   636 		NSString *description = [[NSString alloc] initWithFormat:@"%@ - %@\n%@ (Composed by %@)\n%@\n%@", length, ratingString, artist, composer, album, genre];
   637 		[noteDict release];
   638 		noteDict = [[NSDictionary alloc] initWithObjectsAndKeys:
   639 			(state == itPLAYING ? ITUNES_TRACK_CHANGED : ITUNES_PLAYING), GROWL_NOTIFICATION_NAME,
   640 			APP_NAME,                     GROWL_APP_NAME,
   641 			track,                        GROWL_NOTIFICATION_TITLE,
   642 			description,                  GROWL_NOTIFICATION_DESCRIPTION,
   643 			APP_NAME,                     GROWL_NOTIFICATION_IDENTIFIER,
   644 			[artwork TIFFRepresentation], GROWL_NOTIFICATION_ICON,
   645 			nil];
   646 		[description release];
   647 
   648 		if (trackID != newTrackID) { // this is different from previous note
   649 			// Tell growl
   650 			[GrowlApplicationBridge notifyWithDictionary:noteDict];
   651 
   652 			// Recent Tracks
   653 			[self addTuneToRecentTracks:track fromPlaylist:playlistName];
   654 		}
   655 
   656 		// set up us some state for next time
   657 		state = newState;
   658 		trackID = newTrackID;
   659 	}
   660 }
   661 
   662 - (void) showCurrentTrack {
   663 	if (noteDict)
   664 		[GrowlApplicationBridge notifyWithDictionary:noteDict];
   665 }
   666 
   667 - (void) startTimer {
   668 	if (!pollTimer) {
   669 		pollTimer = [[NSTimer scheduledTimerWithTimeInterval:pollInterval
   670 													  target:self
   671 													selector:@selector(poll:)
   672 													userInfo:nil
   673 													 repeats:YES] retain];
   674 		NSLog(@"%@", @"Polling started - upgrade to iTunes 4.7 or later already, would you?!");
   675 		[self poll:nil];
   676 	}
   677 }
   678 
   679 - (void) stopTimer {
   680 	if (pollTimer){
   681 		[pollTimer invalidate];
   682 		[pollTimer release];
   683 		pollTimer = nil;
   684 		NSLog(@"%@", @"Polling stopped");
   685 	}
   686 }
   687 
   688 #pragma mark Status item
   689 
   690 - (void) createStatusItem {
   691 	if (!statusItem) {
   692 		NSStatusBar *statusBar = [NSStatusBar systemStatusBar];
   693 		statusItem = [[statusBar statusItemWithLength:NSSquareStatusItemLength] retain];
   694 		if (statusItem) {
   695 			[statusItem setMenu:[self statusItemMenu]];
   696 			[statusItem setHighlightMode:YES];
   697 			[statusItem setImage:[NSImage imageNamed:@"growlTunes.png"]];
   698 			[statusItem setAlternateImage:[NSImage imageNamed:@"growlTunes-selected.png"]];
   699 			[statusItem setToolTip:NSLocalizedString(@"GrowlTunes’ control status item.", /*comment*/ nil)];
   700 		}
   701 	}
   702 }
   703 
   704 - (void) tearDownStatusItem {
   705 	if (statusItem) {
   706 		[[NSStatusBar systemStatusBar] removeStatusItem:statusItem]; //otherwise we leave a hole
   707 		[statusItem release];
   708 		statusItem = nil;
   709 	}
   710 }
   711 
   712 - (NSMenu *) statusItemMenu {
   713 	NSMenu *menu = [[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"GrowlTunes"];
   714 	if (menu) {
   715 		NSMenuItem * item;
   716 		NSString *empty = @""; //used for the key equivalent of all the menu items.
   717 
   718 		item = [menu addItemWithTitle:NSLocalizedString(@"Online Help", @"") action:@selector(onlineHelp:) keyEquivalent:empty];
   719 		[item setTarget:self];
   720 		[item setTag:onlineHelpTag];
   721 		[item setToolTip:NSLocalizedString(@"Opens the webpage for GrowlTunes help on the Growl website in your selected browser.", "Online help's tooltip")];
   722 
   723 		item = [NSMenuItem separatorItem];
   724 		[menu addItem:item];
   725 
   726 		item = [menu addItemWithTitle:@"iTunes" action:NULL keyEquivalent:empty];
   727 
   728 		// Set us up a submenu
   729 		[item setSubmenu:[self buildiTunesSubmenu]];
   730 
   731 		// The rating submenu
   732 		item = [menu addItemWithTitle:NSLocalizedString(@"Rating", @"") action:NULL keyEquivalent:empty];
   733 		[item setSubmenu:[self buildRatingSubmenu]];
   734 
   735 		// Back to our regularly scheduled Status Menu
   736 		item = [NSMenuItem separatorItem];
   737 		[menu addItem:item];
   738 
   739 		item = [menu addItemWithTitle:NSLocalizedString(@"Quit GrowlTunes", @"") action:@selector(quitGrowlTunes:) keyEquivalent:empty];
   740 		[item setTarget:self];
   741 		[item setTag:quitGrowlTunesTag];
   742 		item = [menu addItemWithTitle:NSLocalizedString(@"Quit Both", @"") action:@selector(quitBoth:) keyEquivalent:empty];
   743 		[item setTarget:self];
   744 		[item setTag:quitBothTag];
   745 		[item setToolTip:NSLocalizedString(@"Quits both iTunes and GrowlTunes", /*comment*/ nil)];
   746 
   747 		if (polling) {
   748 			item = [NSMenuItem separatorItem];
   749 			[menu addItem:item];
   750 
   751 			item = [menu addItemWithTitle:@"Toggle Polling" action:@selector(togglePolling:) keyEquivalent:empty];
   752 			[item setTarget:self];
   753 			[item setTag:togglePollingTag];
   754 			[item setToolTip:NSLocalizedString(@"Turns on or off GrowlTunes' periodic asking of iTunes for track information.", "Toggle polling tooltip")];
   755 		}
   756 	}
   757 
   758 	return [menu autorelease];
   759 }
   760 
   761 - (IBAction) togglePolling:(id)sender {
   762 #pragma unused(sender)
   763 	if (pollTimer)
   764 		[self stopTimer];
   765 	else
   766 		[self startTimer];
   767 }
   768 
   769 - (NSMenu *) buildiTunesSubmenu {
   770 	NSMenuItem * item;
   771 	if (!iTunesSubMenu)
   772 		iTunesSubMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"iTunes"] autorelease];
   773 
   774 	// Out with the old
   775 	NSArray *items = [iTunesSubMenu itemArray];
   776 	NSEnumerator *itemEnumerator = [items objectEnumerator];
   777 	while ((item = [itemEnumerator nextObject]))
   778 		[iTunesSubMenu removeItem:item];
   779 
   780 	// In with the new
   781 	item = [iTunesSubMenu addItemWithTitle:NSLocalizedString(@"Recently Played Tunes", @"") action:NULL keyEquivalent:@""];
   782 	NSEnumerator *tunesEnumerator = [recentTracks objectEnumerator];
   783 	NSDictionary *aTuneDict = nil;
   784 	int k = 0;
   785 
   786 	while ((aTuneDict = [tunesEnumerator nextObject])) {
   787 		item = [iTunesSubMenu addItemWithTitle:[aTuneDict objectForKey:@"name"]
   788 										action:@selector(jumpToTune:)
   789 								 keyEquivalent:@""];
   790 		[item setTarget:self];
   791 		[item setIndentationLevel:1];
   792 		[item setTag:k++];
   793 		[item setToolTip:NSLocalizedString(@"Tells iTunes to play this track again.", "Tooltip for recent tracks")];
   794 	}
   795 
   796 	[iTunesSubMenu addItem:[NSMenuItem separatorItem]];
   797 	item = [iTunesSubMenu addItemWithTitle:@"Launch iTunes" action:@selector(launchQuitiTunes:) keyEquivalent:@""];
   798 	[item setTarget:self];
   799 	[item setTag:launchQuitiTunesTag];
   800 	//tooltip set by validateMenuItem
   801 
   802 	return iTunesSubMenu;
   803 }
   804 
   805 - (NSMenu *) buildRatingSubmenu {
   806 	NSMenuItem * item;
   807 	if (!ratingSubMenu) {
   808 		ratingSubMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:@"Rating"] autorelease];
   809 		NSString *rating0 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   810 		NSString *rating1 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   811 		NSString *rating2 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86\xe2\x98\x86"];
   812 		NSString *rating3 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86\xe2\x98\x86"];
   813 		NSString *rating4 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x86"];
   814 		NSString *rating5 = [[NSString alloc] initWithUTF8String:"\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85\xe2\x98\x85"];
   815 		item = [ratingSubMenu addItemWithTitle:rating0 action:@selector(setRating:) keyEquivalent:@""];
   816 		[item setTarget:self];
   817 		[item setTag:ratingTag+0];
   818 		item = [ratingSubMenu addItemWithTitle:rating1 action:@selector(setRating:) keyEquivalent:@""];
   819 		[item setTarget:self];
   820 		[item setTag:ratingTag+1];
   821 		item = [ratingSubMenu addItemWithTitle:rating2 action:@selector(setRating:) keyEquivalent:@""];
   822 		[item setTarget:self];
   823 		[item setTag:ratingTag+2];
   824 		item = [ratingSubMenu addItemWithTitle:rating3 action:@selector(setRating:) keyEquivalent:@""];
   825 		[item setTarget:self];
   826 		[item setTag:ratingTag+3];
   827 		item = [ratingSubMenu addItemWithTitle:rating4 action:@selector(setRating:) keyEquivalent:@""];
   828 		[item setTarget:self];
   829 		[item setTag:ratingTag+4];
   830 		item = [ratingSubMenu addItemWithTitle:rating5 action:@selector(setRating:) keyEquivalent:@""];
   831 		[item setTarget:self];
   832 		[item setTag:ratingTag+5];
   833 		[rating0 release];
   834 		[rating1 release];
   835 		[rating2 release];
   836 		[rating3 release];
   837 		[rating4 release];
   838 		[rating5 release];
   839 	}
   840 
   841 	return ratingSubMenu;
   842 }
   843 
   844 - (BOOL) validateMenuItem:(NSMenuItem *)item {
   845 	BOOL retVal = YES;
   846 	int tag = [item tag];
   847 	int i;
   848 
   849 	switch (tag) {
   850 		case launchQuitiTunesTag:
   851 			if ([self iTunesIsRunning])
   852 				[item setTitle:NSLocalizedString(@"Quit iTunes", @"")];
   853 			else
   854 				[item setTitle:NSLocalizedString(@"Launch iTunes", @"")];
   855 			break;
   856 
   857 		case quitBothTag:
   858 			retVal = [self iTunesIsRunning];
   859 			break;
   860 
   861 		case togglePollingTag:
   862 			if (pollTimer) {
   863 				[item setTitle:NSLocalizedString(@"Stop Polling", @"")];
   864 				[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'")];
   865 			} else {
   866 				[item setTitle:NSLocalizedString(@"Start Polling", @"")];
   867 				[item setToolTip:NSLocalizedString(@"Begins asking iTunes for track information. You will then start receiving Growl notifications from GrowlTunes.", "Tooltip for 'start polling'")];
   868 			}
   869 
   870 		case quitGrowlTunesTag:
   871 		case onlineHelpTag:
   872 			break;
   873 
   874 		case ratingTag+0:
   875 		case ratingTag+1:
   876 		case ratingTag+2:
   877 		case ratingTag+3:
   878 		case ratingTag+4:
   879 		case ratingTag+5:
   880 			i = (tag-ratingTag)*20;
   881 			if (trackRating < 0) {
   882 				retVal = NO;
   883 				[item setState:NSOffState];
   884 			} else if (trackRating >= i && trackRating < i+20)
   885 				[item setState:NSOnState];
   886 			else
   887 				[item setState:NSOffState];
   888 			break;
   889 	}
   890 
   891 	return retVal;
   892 }
   893 
   894 - (IBAction) onlineHelp:(id)sender{
   895 #pragma unused(sender)
   896 	[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:ONLINE_HELP_URL]];
   897 }
   898 
   899 - (void) addTuneToRecentTracks:(NSString *)inTune fromPlaylist:(NSString *)inPlaylist {
   900 	NSNumber *recentTrackCountNum = [[NSUserDefaults standardUserDefaults] objectForKey:RECENT_TRACK_COUNT_KEY];
   901 	unsigned trackLimit = recentTrackCountNum ? [recentTrackCountNum unsignedIntValue] : DEFAULT_RECENT_TRACKS_LIMIT;
   902 	NSDictionary *tuneDict = [[NSDictionary alloc] initWithObjectsAndKeys:
   903 		inTune,     @"name",
   904 		inPlaylist, @"playlist",
   905 		nil];
   906 	signed long delta = ([recentTracks count] + 1U) - (signed long)trackLimit;
   907 	if (delta > 0L)
   908 		[recentTracks removeObjectsInRange:NSMakeRange(0U, delta)];
   909 	[recentTracks addObject:tuneDict];
   910 	[tuneDict release];
   911 
   912 	if (![[NSUserDefaults standardUserDefaults] boolForKey:NO_MENU_KEY])
   913 		[self buildiTunesSubmenu];
   914 }
   915 
   916 - (IBAction) quitGrowlTunes:(id)sender {
   917 	[NSApp terminate:sender];
   918 }
   919 
   920 - (IBAction) launchQuitiTunes:(id)sender {
   921 #pragma unused(sender)
   922 	if (![self quitiTunes]) {
   923 		//quit failed, so it wasn't running: launch it.
   924 		[[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:ITUNES_BUNDLE_ID
   925 															 options:NSWorkspaceLaunchDefault
   926 									  additionalEventParamDescriptor:nil
   927 													launchIdentifier:NULL];
   928 	}
   929 }
   930 
   931 - (IBAction) quitBoth:(id)sender {
   932 	[self quitiTunes];
   933 	[self quitGrowlTunes:sender];
   934 }
   935 
   936 - (BOOL) quitiTunes {
   937 	NSDictionary *iTunes = [[NSWorkspace sharedWorkspace] launchedApplicationWithIdentifier:ITUNES_BUNDLE_ID];
   938 	BOOL success = (iTunes != nil);
   939 	if (success) {
   940 		//first disarm the timer. we don't want to launch iTunes right after we quit it if the timer fires.
   941 		[self stopTimer];
   942 
   943 		//now quit iTunes.
   944 		NSAppleEventDescriptor *target = [[NSAppleEventDescriptor alloc] initWithDescriptorType:typeApplicationBundleID
   945 																						   data:[ITUNES_BUNDLE_ID dataUsingEncoding:NSUTF8StringEncoding]];
   946 		NSAppleEventDescriptor *event = [[NSAppleEventDescriptor alloc] initWithEventClass:kCoreEventClass
   947 																				   eventID:kAEQuitApplication
   948 																		  targetDescriptor:target
   949 																				  returnID:kAutoGenerateReturnID
   950 																			 transactionID:kAnyTransactionID];
   951 		OSStatus err = AESendMessage([event aeDesc],
   952 									 /*reply*/ NULL,
   953 									 /*sendMode*/ kAENoReply | kAENeverInteract | kAEDontRecord,
   954 									 kAEDefaultTimeout);
   955 		[target release];
   956 		[event release];
   957 		success = ((err == noErr) || (err == procNotFound));
   958 		//XXX this should be an alert panel (with a better message)
   959 		if (!success)
   960 			NSLog(@"Could not quit iTunes: AESendMessage returned %li", (long)err);
   961 	}
   962 	return success;
   963 }
   964 
   965 - (IBAction) setRating:(id)sender {
   966 	OSStatus err;
   967 	AppleEvent event;
   968 	AEDesc currentTrackObject;
   969 	AEDesc ratingProperty;
   970 	AEDesc trackDescriptor;
   971 	AEDesc ratingDescriptor;
   972 	AEDesc ratingValue;
   973 	AEDesc target;
   974 	AEDesc nullDescriptor = {typeNull, nil};
   975 	DescType trackType = 'pTrk';
   976 	DescType ratingType = 'pRte';
   977 	NSData *bundleID = [ITUNES_BUNDLE_ID dataUsingEncoding:NSUTF8StringEncoding];
   978 	int rating = ([sender tag] - ratingTag) * 20;
   979 
   980 	err = AECreateDesc(typeType, &trackType, sizeof(trackType), &trackDescriptor);
   981 	if (err != noErr)
   982 		NSLog(@"AECreateDesc returned %li", (long)err);
   983 	err = AECreateDesc(typeType, &ratingType, sizeof(ratingType), &ratingDescriptor);
   984 	if (err != noErr)
   985 		NSLog(@"AECreateDesc returned %li", (long)err);
   986 	err = AECreateDesc(typeSInt32, &rating, sizeof(rating), &ratingValue);
   987 	if (err != noErr)
   988 		NSLog(@"AECreateDesc returned %li", (long)err);
   989 	err = AECreateDesc(typeApplicationBundleID, [bundleID bytes], [bundleID length], &target);
   990 	if (err != noErr)
   991 		NSLog(@"AECreateDesc returned %li", (long)err);
   992 
   993 	err = CreateObjSpecifier(typeProperty,
   994 							 &nullDescriptor,
   995 							 formPropertyID,
   996 							 &trackDescriptor,
   997 							 TRUE,
   998 							 &currentTrackObject);
   999 	if (err != noErr)
  1000 		NSLog(@"CreateObjSpecifier returned %li", (long)err);
  1001 	err = CreateObjSpecifier(typeProperty,
  1002 							 &currentTrackObject,
  1003 							 formPropertyID,
  1004 							 &ratingDescriptor,
  1005 							 TRUE,
  1006 							 &ratingProperty);
  1007 	if (err != noErr)
  1008 		NSLog(@"CreateObjSpecifier returned %li", (long)err);
  1009 
  1010 	err = AECreateAppleEvent('core', 'setd', &target, kAutoGenerateReturnID, kAnyTransactionID, &event);
  1011 	if (err != noErr)
  1012 		NSLog(@"AECreateAppleEvent returned %li", (long)err);
  1013 	err = AEPutParamDesc(&event, 'data', &ratingValue);
  1014 	if (err != noErr)
  1015 		NSLog(@"AEPutParamDesc returned %li", (long)err);
  1016 	err = AEPutParamDesc(&event, keyDirectObject, &ratingProperty);
  1017 	if (err != noErr)
  1018 		NSLog(@"AEPutParamDesc returned %li", (long)err);
  1019 
  1020 	err = AESendMessage(&event,
  1021 						/*reply*/ NULL,
  1022 						/*sendMode*/ kAENoReply | kAENeverInteract | kAEDontRecord,
  1023 						kAEDefaultTimeout);
  1024 	if (err != noErr)
  1025 		NSLog(@"AESendMessage returned %li", (long)err);
  1026 
  1027 	AEDisposeDesc(&event);
  1028 	AEDisposeDesc(&target);
  1029 	AEDisposeDesc(&ratingValue);
  1030 	AEDisposeDesc(&ratingProperty);
  1031 
  1032 	trackRating = rating;
  1033 }
  1034 
  1035 #pragma mark AppleScript
  1036 
  1037 - (NSAppleScript *) appleScriptNamed:(NSString *)name {
  1038 	NSURL			*url = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:name ofType:@"scpt"]];
  1039 	NSDictionary	*error;
  1040 
  1041 	return [[NSAppleScript alloc] initWithContentsOfURL:url error:&error];
  1042 }
  1043 
  1044 - (BOOL) iTunesIsRunning {
  1045 	return [[NSWorkspace sharedWorkspace] launchedApplicationWithIdentifier:ITUNES_BUNDLE_ID] != nil;
  1046 }
  1047 
  1048 - (void) jumpToTune:(id) sender {
  1049 	NSDictionary *tuneDict = [recentTracks objectAtIndex:[sender tag]];
  1050 	NSString *jumpScript = [[NSString alloc] initWithFormat:@"tell application \"iTunes\"\nplay track \"%@\" of playlist \"%@\"\nend tell",
  1051 									[tuneDict objectForKey:@"name"],
  1052 									[tuneDict objectForKey:@"playlist"]];
  1053 	NSAppleScript *as = [[NSAppleScript alloc] initWithSource:jumpScript];
  1054 	[as executeAndReturnError:NULL];
  1055 	[as release];
  1056 	[jumpScript release];
  1057 }
  1058 
  1059 - (void) handleAppLaunch:(NSNotification *)notification {
  1060 	if ([ITUNES_BUNDLE_ID caseInsensitiveCompare:[[notification userInfo] objectForKey:@"NSApplicationBundleIdentifier"]] == NSOrderedSame)
  1061 		[self startTimer];
  1062 }
  1063 
  1064 - (void) handleAppQuit:(NSNotification *)notification {
  1065 	if ([ITUNES_BUNDLE_ID caseInsensitiveCompare:[[notification userInfo] objectForKey:@"NSApplicationBundleIdentifier"]] == NSOrderedSame)
  1066 		[self stopTimer];
  1067 }
  1068 
  1069 #pragma mark Plug-ins
  1070 
  1071 // This function is used to sort plugins, trying first the local ones, and then the network ones
  1072 static int comparePlugins(id <GrowlTunesPlugin> plugin1, id <GrowlTunesPlugin> plugin2, void *context) {
  1073 #pragma unused(context)
  1074 	BOOL b1 = [plugin1 usesNetwork];
  1075 	BOOL b2 = [plugin2 usesNetwork];
  1076 	if (b2 && !b1) //b1 is local; b2 is network
  1077 		return NSOrderedAscending;
  1078 	else if (b1 && !b2) //b1 is network; b2 is local
  1079 		return NSOrderedDescending;
  1080 	else //both have the same behaviour
  1081 		return NSOrderedAscending;
  1082 }
  1083 
  1084 - (NSMutableArray *) loadPlugins {
  1085 	NSMutableArray *newPlugins = [[NSMutableArray alloc] init];
  1086 	NSMutableArray *lastPlugins = [[NSMutableArray alloc] init];
  1087 	if (newPlugins) {
  1088 		NSBundle *myBundle = [NSBundle mainBundle];
  1089 		NSString *pluginsPath = [myBundle builtInPlugInsPath];
  1090 		NSString *applicationSupportPath = [@"~/Library/Application Support/GrowlTunes/Plugins" stringByExpandingTildeInPath];
  1091 		NSArray *loadPathsArray = [NSArray arrayWithObjects:pluginsPath, applicationSupportPath, nil];
  1092 		NSEnumerator *loadPathsEnum = [loadPathsArray objectEnumerator];
  1093 		NSString *loadPath;
  1094 		NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  1095 		static NSString *pluginPathExtension = @"plugin";
  1096 
  1097 		while ((loadPath = [loadPathsEnum nextObject])) {
  1098 			NSEnumerator *pluginEnum = [[[NSFileManager defaultManager] directoryContentsAtPath:loadPath] objectEnumerator];
  1099 			NSString *curPath;
  1100 
  1101 			while ((curPath = [pluginEnum nextObject])) {
  1102 				if ([[curPath pathExtension] isEqualToString:pluginPathExtension]) {
  1103 					curPath = [pluginsPath stringByAppendingPathComponent:curPath];
  1104 					NSBundle *plugin = [NSBundle bundleWithPath:curPath];
  1105 
  1106 					if ([plugin load]) {
  1107 						Class principalClass = [plugin principalClass];
  1108 
  1109 						if ([principalClass conformsToProtocol:@protocol(GrowlTunesPlugin)]) {
  1110 							id instance = [[principalClass alloc] init];
  1111 							[newPlugins addObject:instance];
  1112 
  1113 							if (!archivePlugin && ([principalClass conformsToProtocol:@protocol(GrowlTunesPluginArchive)])) {
  1114 								archivePlugin = [instance retain];
  1115 //								NSLog(@"plug-in %@ is archive-Plugin with id %p", [curPath lastPathComponent], instance);
  1116 							}
  1117 							[instance release];
  1118 //							NSLog(@"Loaded plug-in \"%@\" with id %p", [curPath lastPathComponent], instance);
  1119 						} else
  1120 							NSLog(@"Loaded plug-in \"%@\" does not conform to protocol", [curPath lastPathComponent]);
  1121 					} else
  1122 						NSLog(@"Could not load plug-in \"%@\"", [curPath lastPathComponent]);
  1123 				}
  1124 			}
  1125 		}
  1126 
  1127 		[pool release];
  1128 		[newPlugins addObjectsFromArray:lastPlugins];
  1129 		[lastPlugins release];
  1130 		[newPlugins autorelease];
  1131 	}
  1132 
  1133 	// sort the plugins, putting the one that uses network last
  1134 	return (NSMutableArray *)[newPlugins sortedArrayUsingFunction:comparePlugins context:NULL];
  1135 }
  1136 
  1137 @end
  1138 
  1139 @implementation NSObject(GrowlTunesDummyPlugin)
  1140 
  1141 - (NSImage *) artworkForTitle:(NSString *)track
  1142 					byArtist:(NSString *)artist
  1143 					 onAlbum:(NSString *)album
  1144 			   isCompilation:(BOOL)compilation
  1145 {
  1146 #pragma unused(track,artist,album,compilation)
  1147 	NSLog(@"Dummy plug-in %p called for artwork", self);
  1148 	return nil;
  1149 }
  1150 
  1151 @end
  1152 
  1153 @implementation NSString (GrowlTunesMultiplicationAdditions)
  1154 
  1155 - (NSString *)stringByMultiplyingBy:(NSUInteger)multi {
  1156 	NSUInteger length = [self length];
  1157 	NSUInteger length_multi = length * multi;
  1158 
  1159 	unichar *buf = malloc(sizeof(unichar) * length_multi);
  1160 	if (!buf)
  1161 		return nil;
  1162 
  1163 	for (NSUInteger i = 0UL; i < multi; ++i)
  1164 		[self getCharacters:&buf[length * i]];
  1165 
  1166 	NSString *result = [NSString stringWithCharacters:buf length:length_multi];
  1167 	free(buf);
  1168 	return result;
  1169 }
  1170 
  1171 @end