281 lines
7.8 KiB
Objective-C
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
|