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