cog/AudioScrobbler/AudioScrobbler.m

281 lines
7.8 KiB
Objective-C

/*
* $Id: AudioScrobbler.m 238 2007-01-26 22:55:20Z stephen_booth $
*
* Copyright (C) 2006 - 2007 Stephen F. Booth <me@sbooth.org>
*
* 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);
}
@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 = nil;
return nil;
}
[NSThread detachNewThreadSelector:@selector(processAudioScrobblerCommands:) toTarget:self withObject:nil];
}
return self;
}
- (void) dealloc
{
if([self keepProcessingAudioScrobblerCommands] || NO == [self audioScrobblerThreadCompleted])
[self shutdown];
_queue = nil;
semaphore_destroy(mach_task_self(), _semaphore); _semaphore = 0;
}
- (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
{
@autoreleasepool {
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]) {
@autoreleasepool {
// Get the first command to be sent
@synchronized([self queue]) {
if ([[self queue] count]) {
command = [[self queue] objectAtIndex:0];
[[self queue] removeObjectAtIndex:0];
}
}
if(nil != command) {
@try {
if([client connectToHost:@"localhost" port:port]) {
port = [client connectedPort];
[client send:command];
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 = nil;
[client shutdown];
// ALog(@"Exception: %@",exception);
continue;
}
}
semaphore_timedwait([self semaphore], timeout);
}
}
// 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];
}
[self setAudioScrobblerThreadCompleted:YES];
}
}
@end