From 35b1c886a821839593c093e81928e05613bab93f Mon Sep 17 00:00:00 2001 From: vspader Date: Sun, 25 Feb 2007 02:43:56 +0000 Subject: [PATCH] Added Last.fm support for those with the last.fm client. Fixed up remote and scrobbler preferences. --- Application/AppController.m | 41 ++- Application/PlaybackController.h | 3 + Application/PlaybackController.m | 24 +- AudioScrobbler/AudioScrobbler.h | 44 +++ AudioScrobbler/AudioScrobbler.m | 257 ++++++++++++++++++ AudioScrobbler/AudioScrobblerClient.h | 39 +++ AudioScrobbler/AudioScrobblerClient.m | 189 +++++++++++++ Cog.xcodeproj/project.pbxproj | 24 ++ .../English.lproj/Preferences.nib/info.nib | 12 +- .../Preferences.nib/keyedobjects.nib | Bin 14916 -> 14974 bytes 10 files changed, 614 insertions(+), 19 deletions(-) create mode 100644 AudioScrobbler/AudioScrobbler.h create mode 100644 AudioScrobbler/AudioScrobbler.m create mode 100644 AudioScrobbler/AudioScrobblerClient.h create mode 100644 AudioScrobbler/AudioScrobblerClient.m diff --git a/Application/AppController.m b/Application/AppController.m index f3386b7de..40385aa6b 100644 --- a/Application/AppController.m +++ b/Application/AppController.m @@ -25,15 +25,13 @@ // Listen to the remote in exclusive mode, only when Cog is the active application - (void)applicationDidBecomeActive:(NSNotification *)notification { - BOOL onlyOnActive = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"remoteOnlyOnActive"] boolValue]; - if (onlyOnActive) { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"remoteEnabled"] && [[NSUserDefaults standardUserDefaults] boolForKey:@"remoteOnlyOnActive"]) { [remote startListening: self]; } } - (void)applicationDidResignActive:(NSNotification *)motification { - BOOL onlyOnActive = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"remoteOnlyOnActive"] boolValue]; - if (onlyOnActive) { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"remoteEnabled"] && [[NSUserDefaults standardUserDefaults] boolForKey:@"remoteOnlyOnActive"]) { [remote stopListening: self]; } } @@ -204,7 +202,7 @@ increase/decrease as long as the user holds the left/right, plus/minus button */ [self registerHotKeys]; //Init Remote - if (![[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"remoteOnlyOnActive"] boolValue]) { + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"remoteEnabled"] && ![[NSUserDefaults standardUserDefaults] boolForKey:@"remoteOnlyOnActive"]) { [remote startListening:self]; } @@ -360,17 +358,30 @@ increase/decrease as long as the user holds the left/right, plus/minus button */ [userDefaultsValuesDict setObject:[@"~/Music" stringByExpandingTildeInPath] forKey:@"fileDrawerRootPath"]; + [userDefaultsValuesDict setObject:[NSNumber numberWithBool:YES] forKey:@"remoteEnabled"]; [userDefaultsValuesDict setObject:[NSNumber numberWithBool:YES] forKey:@"remoteOnlyOnActive"]; + [userDefaultsValuesDict setObject:[NSNumber numberWithBool:YES] forKey:@"enableAudioScrobbler"]; + [userDefaultsValuesDict setObject:[NSNumber numberWithBool:YES] forKey:@"automaticallyLaunchLastFM"]; + //Register and sync defaults [[NSUserDefaults standardUserDefaults] registerDefaults:userDefaultsValuesDict]; [[NSUserDefaults standardUserDefaults] synchronize]; - + + //Add observers [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.hotKeyPlayKeyCode" options:0 context:nil]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.hotKeyPreviousKeyCode" options:0 context:nil]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.hotKeyNextKeyCode" options:0 context:nil]; [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fileDrawerRootPath" options:0 context:nil]; + + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fileDrawerRootPath" options:0 context:nil]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fileDrawerRootPath" options:0 context:nil]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fileDrawerRootPath" options:0 context:nil]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.fileDrawerRootPath" options:0 context:nil]; + + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.remoteEnabled" options:0 context:nil]; + [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:@"values.remoteOnlyOnActive" options:0 context:nil]; } - (void) observeValueForKeyPath:(NSString *)keyPath @@ -390,16 +401,22 @@ increase/decrease as long as the user holds the left/right, plus/minus button */ else if ([keyPath isEqualToString:@"values.fileDrawerRootPath"]) { [fileTreeController setRootPath:[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"fileDrawerRootPath"]]; } - else if ([keyPath isEqualToString:@"values.remoteOnlyOnActive"]) { - BOOL onlyOnActive = [[[[NSUserDefaultsController sharedUserDefaultsController] defaults] objectForKey:@"remoteOnlyOnActive"] boolValue]; - if (!onlyOnActive || [NSApp isActive]) { - [remote startListening: self]; + else if ([keyPath isEqualToString:@"values.remoteEnabled"] || [keyPath isEqualToString:@"values.remoteOnlyOnActive"]) { + if([[NSUserDefaults standardUserDefaults] boolForKey:@"remoteEnabled"]) { + NSLog(@"Remote enabled..."); + BOOL onlyOnActive = [[NSUserDefaults standardUserDefaults] boolForKey:@"remoteOnlyOnActive"]; + if (!onlyOnActive || [NSApp isActive]) { + [remote startListening: self]; + } + if (onlyOnActive && ![NSApp isActive]) { //Setting a preference without being active? *shrugs* + [remote stopListening: self]; + } } - if (onlyOnActive && ![NSApp isActive]) { //Setting a preference without being active? *shrugs* + else { + NSLog(@"DISABLE REMOTE"); [remote stopListening: self]; } } - } - (void)registerHotKeys diff --git a/Application/PlaybackController.h b/Application/PlaybackController.h index 00d5aa3df..c7f140994 100644 --- a/Application/PlaybackController.h +++ b/Application/PlaybackController.h @@ -5,6 +5,7 @@ #import "CogAudio/AudioPlayer.h" #import "PlaylistController.h" #import "TrackingSlider.h" +#import "AudioScrobbler.h" @class PlaylistView; @@ -33,6 +34,8 @@ double currentVolume; BOOL showTimeRemaining; + + AudioScrobbler *scrobbler; } - (IBAction)toggleShowTimeRemaining:(id)sender; diff --git a/Application/PlaybackController.m b/Application/PlaybackController.m index cd4e48aa1..6a623c544 100644 --- a/Application/PlaybackController.m +++ b/Application/PlaybackController.m @@ -16,6 +16,8 @@ playbackStatus = kCogStatusStopped; showTimeRemaining = NO; + + scrobbler = [[AudioScrobbler alloc] init]; } return self; @@ -50,12 +52,21 @@ { // DBLog(@"Pause Sent!"); [audioPlayer pause]; + + if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) { + [scrobbler pause]; + } } - (IBAction)resume:(id)sender { // DBLog(@"Resume Sent!"); [audioPlayer resume]; + + + if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) { + [scrobbler resume]; + } } - (IBAction)stop:(id)sender @@ -63,6 +74,10 @@ // DBLog(@"Stop Sent!"); [audioPlayer stop]; + + if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) { + [scrobbler stop]; + } } //called by double-clicking on table @@ -81,7 +96,7 @@ [self playEntryAtIndex:[playlistView selectedRow]]; } -- (void)playEntry:(PlaylistEntry *)pe; +- (void)playEntry:(PlaylistEntry *)pe { // DBLog(@"PlayEntry: %@ Sent!", [pe filename]); if (playbackStatus != kCogStatusStopped) @@ -95,6 +110,10 @@ [audioPlayer play:[NSURL fileURLWithPath:[pe filename]] withUserInfo:pe]; [audioPlayer setVolume:currentVolume]; + + if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) { + [scrobbler start:pe]; + } } - (IBAction)next:(id)sender @@ -264,6 +283,9 @@ [self updateTimeField:0.0f]; + if([[NSUserDefaults standardUserDefaults] boolForKey:@"enableAudioScrobbler"]) { + [scrobbler start:pe]; + } } - (void)updatePosition:(id)sender diff --git a/AudioScrobbler/AudioScrobbler.h b/AudioScrobbler/AudioScrobbler.h new file mode 100644 index 000000000..3bcc4b237 --- /dev/null +++ b/AudioScrobbler/AudioScrobbler.h @@ -0,0 +1,44 @@ +/* + * $Id: AudioScrobbler.h 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 + +#import + +@class PlaylistEntry; + +@interface AudioScrobbler : NSObject +{ + NSString *_pluginID; + NSMutableArray *_queue; + + BOOL _audioScrobblerThreadCompleted; + BOOL _keepProcessingAudioScrobblerCommands; + semaphore_t _semaphore; +} + +- (void) start:(PlaylistEntry *)pe; +- (void) stop; +- (void) pause; +- (void) resume; + +- (void) shutdown; + +@end diff --git a/AudioScrobbler/AudioScrobbler.m b/AudioScrobbler/AudioScrobbler.m new file mode 100644 index 000000000..5cbcc81a4 --- /dev/null +++ b/AudioScrobbler/AudioScrobbler.m @@ -0,0 +1,257 @@ +/* + * $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" + +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:(AudioScrobbler *)myself; + +@end + +@implementation AudioScrobbler + +- (id) init +{ + if((self = [super init])) { + + _pluginID = @"tst"; + + if([[NSUserDefaults standardUserDefaults] boolForKey:@"automaticallyLaunchLastFM"]) { + [[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) { + NSLog(@"Couldn't create semaphore (%s).", mach_error_type(result)); + + [self release]; + return nil; + } + + [NSThread detachNewThreadSelector:@selector(processAudioScrobblerCommands:) toTarget:self withObject:self]; + } + 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 + (int)([pe length]/1000.0), + escapeForLastFM([pe filename]) + ]]; +} + +- (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:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.5]]; + } +} + +@end + +@implementation AudioScrobbler (Private) + +- (NSMutableArray *) queue +{ + if(nil == _queue) { + _queue = [[NSMutableArray alloc] init]; + } + + return _queue; +} + +- (NSString *) pluginID +{ + return _pluginID; +} + +- (void) sendCommand:(NSString *)command +{ + NSLog(@"Command: %@", 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:(AudioScrobbler *)myself +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + AudioScrobblerClient *client = [[AudioScrobblerClient alloc] init]; + mach_timespec_t timeout = { 5, 0 }; + NSEnumerator *enumerator = nil; + NSString *command = nil; + NSString *response = nil; + in_port_t port = 33367; + + while([myself keepProcessingAudioScrobblerCommands]) { + + // Get the first command to be sent + @synchronized([myself queue]) { + + enumerator = [[myself queue] objectEnumerator]; + command = [enumerator nextObject]; + + [[myself queue] removeObjectIdenticalTo:command]; + } + + if(nil != command) { + @try { + port = [client connectToHost:@"localhost" port:port]; + [client send:command]; + + response = [client receive]; + if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)]) { + NSLog(@"AudioScrobbler error: %@", response); + } + + [client shutdown]; + } + + @catch(NSException *exception) { + [client shutdown]; +// NSLog(@"Exception: %@",exception); + continue; + } + } + + semaphore_timedwait([myself semaphore], timeout); + } + + // Send a final stop command to cleanup + @try { + port = [client connectToHost:@"localhost" port:port]; + [client send:[NSString stringWithFormat:@"STOP c=%@\n", [myself pluginID]]]; + + response = [client receive]; + if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)]) { + NSLog(@"AudioScrobbler error: %@", response); + } + + [client shutdown]; + } + + @catch(NSException *exception) { + [client shutdown]; + } + + [client release]; + [pool release]; + + [myself setAudioScrobblerThreadCompleted:YES]; +} + +@end diff --git a/AudioScrobbler/AudioScrobblerClient.h b/AudioScrobbler/AudioScrobblerClient.h new file mode 100644 index 000000000..7456d9644 --- /dev/null +++ b/AudioScrobbler/AudioScrobblerClient.h @@ -0,0 +1,39 @@ +/* + * $Id: AudioScrobblerClient.h 241 2007-01-26 23:02:09Z 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 + +#include + +@interface AudioScrobblerClient : NSObject +{ + int _socket; + BOOL _doPortStepping; + in_port_t _lastPort; +} + +- (in_port_t) connectToHost:(NSString *)hostname port:(in_port_t)port; + +- (void) send:(NSString *)data; +- (NSString *) receive; + +- (void) shutdown; + +@end diff --git a/AudioScrobbler/AudioScrobblerClient.m b/AudioScrobbler/AudioScrobblerClient.m new file mode 100644 index 000000000..af6285880 --- /dev/null +++ b/AudioScrobbler/AudioScrobblerClient.m @@ -0,0 +1,189 @@ +/* + * $Id: AudioScrobblerClient.m 362 2007-02-13 05:30:49Z 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 + */ + +/* + * This is a port of the BlockingClient client class from + * the Last.fm ScrobSub library by sharevari + */ + +#import "AudioScrobblerClient.h" + +#define kBufferSize 1024 +#define kPortsToStep 5 + +static in_addr_t +addressForHost(NSString *hostname) +{ + NSCParameterAssert(nil != hostname); + + in_addr_t address; + struct hostent *hostinfo; + + address = inet_addr([hostname cStringUsingEncoding:NSASCIIStringEncoding]); + + if(INADDR_NONE == address) { + hostinfo = gethostbyname([hostname cStringUsingEncoding:NSASCIIStringEncoding]); + if(NULL == hostinfo) { + NSLog(@"Unable to resolve address for \"%@\".", hostname); + return INADDR_NONE; + } + + address = *((in_addr_t *)hostinfo->h_addr_list[0]); + } + + return address; +} + +@interface AudioScrobblerClient (Private) +- (void) connectToSocket:(in_addr_t)remoteAddress port:(in_port_t)port; +@end + +@implementation AudioScrobblerClient + +- (id) init +{ + if((self = [super init])) { + _socket = -1; + _lastPort = -1; + _doPortStepping = YES; + } + return self; +} + +- (in_port_t) connectToHost:(NSString *)hostname port:(in_port_t)port +{ + in_addr_t remoteAddress = addressForHost(hostname); + + [self connectToSocket:remoteAddress port:port]; + + return _lastPort; +} + +- (void) send:(NSString *)data +{ + const char *utf8data = [data UTF8String]; + unsigned len = strlen(utf8data); + unsigned bytesToSend = len; + unsigned totalBytesSent = 0; + ssize_t bytesSent = 0; + + while(totalBytesSent < bytesToSend && -1 != bytesSent) { + bytesSent = send(_socket, utf8data + totalBytesSent, bytesToSend - totalBytesSent, 0); + + if(-1 == bytesSent || 0 == bytesSent) { + NSLog(@"Unable to send data through socket"); + } + + totalBytesSent += bytesSent; + } +} + +- (NSString *) receive +{ + char buffer [ kBufferSize ]; + int readSize = kBufferSize - 1; + ssize_t bytesRead = 0; + BOOL keepGoing = YES; + NSString *result = nil; + + do { + bytesRead = recv(_socket, buffer, readSize, 0); + NSAssert1(-1 != bytesRead && 0 < bytesRead, @"Unable to receive data through socket (%s).", strerror(errno)); + + if('\n' == buffer[bytesRead - 1]) { + --bytesRead; + keepGoing = NO; + } + + buffer[bytesRead] = '\0'; + result = [[NSString alloc] initWithUTF8String:buffer]; + + } while(keepGoing); + + return [result autorelease]; +} + +- (void) shutdown +{ + int result; + char buffer [ kBufferSize ]; + ssize_t bytesRead; + + if(-1 == _socket) { + return; + } + + result = shutdown(_socket, SHUT_WR); + NSAssert1(-1 != result, @"Socket shutdown failed (%s).", strerror(errno)); + + for(;;) { + bytesRead = recv(_socket, buffer, kBufferSize, 0); + + NSAssert1(-1 != bytesRead, @"Waiting for shutdown confirmation failed (%s).", strerror(errno)); + + if(0 != bytesRead) { + NSLog(@"Received unexpected bytes during shutdown: %@.", [[[NSString alloc] initWithBytes:buffer length:bytesRead encoding:NSUTF8StringEncoding] autorelease]); + } + else { + break; + } + } + + result = close(_socket); + NSAssert1(-1 != result, @"Couldn't close socket (%s).", strerror(errno)); + + _socket = -1; +} + +@end + +@implementation AudioScrobblerClient (Private) + +- (void) connectToSocket:(in_addr_t)remoteAddress port:(in_port_t)port +{ + struct sockaddr_in socketAddress; + int result; + + _socket = socket(AF_INET, SOCK_STREAM, 0); + NSAssert1(-1 != _socket, @"Unable to create socket (%s).", strerror(errno)); + + _lastPort = port; + socketAddress.sin_family = AF_INET; + socketAddress.sin_addr.s_addr = remoteAddress; + socketAddress.sin_port = htons(_lastPort); + + result = connect(_socket, (const struct sockaddr *)&socketAddress, sizeof(struct sockaddr_in)); + + if(_doPortStepping) { + while(-1 == result && _lastPort <= (port + kPortsToStep)) { + socketAddress.sin_port = htons(++_lastPort); + result = connect(_socket, (const struct sockaddr *)&socketAddress, sizeof(struct sockaddr_in)); + } + } + + if(-1 == result) { + _doPortStepping = NO; + close(_socket); + _socket = -1; + NSAssert1(-1 != result, @"Couldn't connect to server (%s).", strerror(errno)); + } +} + +@end diff --git a/Cog.xcodeproj/project.pbxproj b/Cog.xcodeproj/project.pbxproj index 7607d3805..2836d9da8 100644 --- a/Cog.xcodeproj/project.pbxproj +++ b/Cog.xcodeproj/project.pbxproj @@ -9,6 +9,10 @@ /* Begin PBXBuildFile section */ 1705F1510B8BCB0C00C8B40D /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 1705F1420B8BCB0C00C8B40D /* Help */; }; 171678C00AC8C39E00C28CF3 /* SmartFolderNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 171678BE0AC8C39E00C28CF3 /* SmartFolderNode.m */; }; + 1766C6920B911DF1004A7AE4 /* AudioScrobbler.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1766C68E0B911DF1004A7AE4 /* AudioScrobbler.h */; }; + 1766C6930B911DF1004A7AE4 /* AudioScrobbler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1766C68F0B911DF1004A7AE4 /* AudioScrobbler.m */; }; + 1766C6940B911DF1004A7AE4 /* AudioScrobblerClient.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 1766C6900B911DF1004A7AE4 /* AudioScrobblerClient.h */; }; + 1766C6950B911DF1004A7AE4 /* AudioScrobblerClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 1766C6910B911DF1004A7AE4 /* AudioScrobblerClient.m */; }; 1770429C0B8BC53600B86321 /* AppController.m in Sources */ = {isa = PBXBuildFile; fileRef = 177042980B8BC53600B86321 /* AppController.m */; }; 1770429E0B8BC53600B86321 /* PlaybackController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1770429A0B8BC53600B86321 /* PlaybackController.m */; }; 177EBF9E0B8BC2A70000BC8C /* AMRemovableColumnsTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 177EBF7A0B8BC2A70000BC8C /* AMRemovableColumnsTableView.m */; }; @@ -139,6 +143,8 @@ files = ( 17B61B630B90A28100BC003F /* CogAudio.framework in CopyFiles */, 17F94CCD0B8D090800A34E87 /* Sparkle.framework in CopyFiles */, + 1766C6920B911DF1004A7AE4 /* AudioScrobbler.h in CopyFiles */, + 1766C6940B911DF1004A7AE4 /* AudioScrobblerClient.h in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -150,6 +156,10 @@ 1705F1420B8BCB0C00C8B40D /* Help */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Help; sourceTree = ""; }; 171678BD0AC8C39E00C28CF3 /* SmartFolderNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SmartFolderNode.h; sourceTree = ""; }; 171678BE0AC8C39E00C28CF3 /* SmartFolderNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SmartFolderNode.m; sourceTree = ""; }; + 1766C68E0B911DF1004A7AE4 /* AudioScrobbler.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = AudioScrobbler.h; sourceTree = ""; }; + 1766C68F0B911DF1004A7AE4 /* AudioScrobbler.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = AudioScrobbler.m; sourceTree = ""; }; + 1766C6900B911DF1004A7AE4 /* AudioScrobblerClient.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = AudioScrobblerClient.h; sourceTree = ""; }; + 1766C6910B911DF1004A7AE4 /* AudioScrobblerClient.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = AudioScrobblerClient.m; sourceTree = ""; }; 1770424E0B8BC41800B86321 /* Cog.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Cog.app; sourceTree = BUILT_PRODUCTS_DIR; }; 177042970B8BC53600B86321 /* AppController.h */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.h; path = AppController.h; sourceTree = ""; }; 177042980B8BC53600B86321 /* AppController.m */ = {isa = PBXFileReference; fileEncoding = 30; lastKnownFileType = sourcecode.c.objc; path = AppController.m; sourceTree = ""; }; @@ -310,6 +320,7 @@ isa = PBXGroup; children = ( 177042960B8BC53600B86321 /* Application */, + 1766C68D0B911DF1004A7AE4 /* AudioScrobbler */, 8E07AAEA0AAC90DC00A4B32F /* Preferences */, 8EFFCD410AA093AF00C458A5 /* FileDrawer */, 8E75752309F31D5A0080F1EE /* Feedback */, @@ -345,6 +356,17 @@ name = "Other Frameworks"; sourceTree = ""; }; + 1766C68D0B911DF1004A7AE4 /* AudioScrobbler */ = { + isa = PBXGroup; + children = ( + 1766C68E0B911DF1004A7AE4 /* AudioScrobbler.h */, + 1766C68F0B911DF1004A7AE4 /* AudioScrobbler.m */, + 1766C6900B911DF1004A7AE4 /* AudioScrobblerClient.h */, + 1766C6910B911DF1004A7AE4 /* AudioScrobblerClient.m */, + ); + path = AudioScrobbler; + sourceTree = ""; + }; 177042960B8BC53600B86321 /* Application */ = { isa = PBXGroup; children = ( @@ -802,6 +824,8 @@ 1770429C0B8BC53600B86321 /* AppController.m in Sources */, 1770429E0B8BC53600B86321 /* PlaybackController.m in Sources */, 17D21DF60B8BE86900D1EBDE /* CoreAudioUtils.m in Sources */, + 1766C6930B911DF1004A7AE4 /* AudioScrobbler.m in Sources */, + 1766C6950B911DF1004A7AE4 /* AudioScrobblerClient.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Preferences/General/English.lproj/Preferences.nib/info.nib b/Preferences/General/English.lproj/Preferences.nib/info.nib index 62aa141ab..47f396a63 100644 --- a/Preferences/General/English.lproj/Preferences.nib/info.nib +++ b/Preferences/General/English.lproj/Preferences.nib/info.nib @@ -9,25 +9,25 @@ 10 587 659 506 102 0 0 1680 1028 11 - 703 634 273 151 0 0 1680 1028 + 699 367 273 151 0 0 1680 1028 43 - 516 528 337 116 0 0 1680 1028 + 383 476 337 116 0 0 1680 1028 50 - 662 662 355 96 0 0 1680 1028 + 769 536 355 96 0 0 1680 1028 58 - 634 659 411 101 0 0 1680 1028 + 385 271 411 101 0 0 1680 1028 85 - 614 652 452 116 0 0 1680 1028 + 596 829 452 116 0 0 1680 1028 IBFramework Version 446.1 IBOpenObjects 10 - 58 85 43 50 + 58 11 IBSystem Version diff --git a/Preferences/General/English.lproj/Preferences.nib/keyedobjects.nib b/Preferences/General/English.lproj/Preferences.nib/keyedobjects.nib index 347df17916fcc6879a0e113fb16686c6851967db..f6858831377e58766b773daa6e483314a3ce35f7 100644 GIT binary patch delta 6752 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 delta 6668 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 B8?XQX