/* * $Id: AudioScrobbler.m 238 2007-01-26 22:55:20Z stephen_booth $ * * Copyright (C) 2006 - 2007 Stephen F. Booth * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #import "AudioScrobbler.h" #import "AudioScrobblerClient.h" #import "PlaylistEntry.h" #import "Logging.h" // ======================================== // Symbolic Constants // ======================================== NSString * const AudioScrobblerRunLoopMode = @"org.cogx.Cog.AudioScrobbler.RunLoopMode"; // ======================================== // Helpers // ======================================== static NSString * escapeForLastFM(NSString *string) { NSMutableString *result = [string mutableCopy]; [result replaceOccurrencesOfString:@"&" withString:@"&&" options:NSLiteralSearch range:NSMakeRange(0, [result length])]; return (nil == result ? @"" : [result autorelease]); } @interface AudioScrobbler (Private) - (NSMutableArray *) queue; - (NSString *) pluginID; - (void) sendCommand:(NSString *)command; - (BOOL) keepProcessingAudioScrobblerCommands; - (void) setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands; - (BOOL) audioScrobblerThreadCompleted; - (void) setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted; - (semaphore_t) semaphore; - (void) processAudioScrobblerCommands:(id)unused; @end @implementation AudioScrobbler + (BOOL) isRunning { NSArray *launchedApps = [[NSWorkspace sharedWorkspace] runningApplications]; BOOL running = NO; for(NSRunningApplication *app in launchedApps) { if([[app bundleIdentifier] isEqualToString:@"fm.last.Last.fm"] || [[app bundleIdentifier] isEqualToString:@"fm.last.Scrobbler"]) { running = YES; break; } } return running; } - (id) init { if((self = [super init])) { _pluginID = @"cog"; if([[NSUserDefaults standardUserDefaults] boolForKey:@"automaticallyLaunchLastFM"]) { if(![AudioScrobbler isRunning]) { [[NSWorkspace sharedWorkspace] launchApplication:@"Last.fm.app"]; } } _keepProcessingAudioScrobblerCommands = YES; kern_return_t result = semaphore_create(mach_task_self(), &_semaphore, SYNC_POLICY_FIFO, 0); if(KERN_SUCCESS != result) { ALog(@"Couldn't create semaphore (%s).", mach_error_type(result)); [self release]; return nil; } [NSThread detachNewThreadSelector:@selector(processAudioScrobblerCommands:) toTarget:self withObject:nil]; } return self; } - (void) dealloc { if([self keepProcessingAudioScrobblerCommands] || NO == [self audioScrobblerThreadCompleted]) [self shutdown]; [_queue release], _queue = nil; semaphore_destroy(mach_task_self(), _semaphore), _semaphore = 0; [super dealloc]; } - (void) start:(PlaylistEntry *)pe { [self sendCommand:[NSString stringWithFormat:@"START c=%@&a=%@&t=%@&b=%@&m=%@&l=%i&p=%@\n", [self pluginID], escapeForLastFM([pe artist]), escapeForLastFM([pe title]), escapeForLastFM([pe album]), @"", // TODO: MusicBrainz support [[pe length] intValue], escapeForLastFM([[pe URL] path]) ]]; } - (void) stop { [self sendCommand:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]]; } - (void) pause { [self sendCommand:[NSString stringWithFormat:@"PAUSE c=%@\n", [self pluginID]]]; } - (void) resume { [self sendCommand:[NSString stringWithFormat:@"RESUME c=%@\n", [self pluginID]]]; } - (void) shutdown { [self setKeepProcessingAudioScrobblerCommands:NO]; semaphore_signal([self semaphore]); // Wait for the thread to terminate while(NO == [self audioScrobblerThreadCompleted]) [[NSRunLoop currentRunLoop] runMode:AudioScrobblerRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]]; } @end @implementation AudioScrobbler (Private) - (NSMutableArray *) queue { if(nil == _queue) _queue = [[NSMutableArray alloc] init]; return _queue; } - (NSString *) pluginID { return _pluginID; } - (void) sendCommand:(NSString *)command { @synchronized([self queue]) { [[self queue] addObject:command]; } semaphore_signal([self semaphore]); } - (BOOL) keepProcessingAudioScrobblerCommands { return _keepProcessingAudioScrobblerCommands; } - (void) setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands { _keepProcessingAudioScrobblerCommands = keepProcessingAudioScrobblerCommands; } - (BOOL) audioScrobblerThreadCompleted { return _audioScrobblerThreadCompleted; } - (void) setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted { _audioScrobblerThreadCompleted = audioScrobblerThreadCompleted; } - (semaphore_t) semaphore { return _semaphore; } - (void) processAudioScrobblerCommands:(id)unused { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; AudioScrobblerClient *client = [[AudioScrobblerClient alloc] init]; mach_timespec_t timeout = { 5, 0 }; NSString *command = nil; NSString *response = nil; in_port_t port = 33367; while([self keepProcessingAudioScrobblerCommands]) { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Get the first command to be sent @synchronized([self queue]) { if ([[self queue] count]) { command = [[[self queue] objectAtIndex:0] retain]; [[self queue] removeObjectAtIndex:0]; } } if(nil != command) { @try { if([client connectToHost:@"localhost" port:port]) { port = [client connectedPort]; [client send:command]; [command release]; command = nil; response = [client receive]; if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)]) ALog(@"AudioScrobbler error: %@", response); [client shutdown]; } } @catch(NSException *exception) { [command release]; command = nil; [client shutdown]; // ALog(@"Exception: %@",exception); [pool release]; continue; } } semaphore_timedwait([self semaphore], timeout); [pool release]; } // Send a final stop command to cleanup @try { if([client connectToHost:@"localhost" port:port]) { [client send:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]]; response = [client receive]; if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)]) ALog(@"AudioScrobbler error: %@", response); [client shutdown]; } } @catch(NSException *exception) { [client shutdown]; } [client release]; [self setAudioScrobblerThreadCompleted:YES]; [pool release]; } @end