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