An audio player for macOS 10.8 and newer. https://kode54.net/cog
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

281 lines
7.8KB

  1. /*
  2. * $Id: AudioScrobbler.m 238 2007-01-26 22:55:20Z stephen_booth $
  3. *
  4. * Copyright (C) 2006 - 2007 Stephen F. Booth <me@sbooth.org>
  5. *
  6. * This program is free software; you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation; either version 2 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program; if not, write to the Free Software
  18. * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  19. */
  20. #import "AudioScrobbler.h"
  21. #import "AudioScrobblerClient.h"
  22. #import "PlaylistEntry.h"
  23. #import "Logging.h"
  24. // ========================================
  25. // Symbolic Constants
  26. // ========================================
  27. NSString * const AudioScrobblerRunLoopMode = @"org.cogx.Cog.AudioScrobbler.RunLoopMode";
  28. // ========================================
  29. // Helpers
  30. // ========================================
  31. static NSString *
  32. escapeForLastFM(NSString *string)
  33. {
  34. NSMutableString *result = [string mutableCopy];
  35. [result replaceOccurrencesOfString:@"&"
  36. withString:@"&&"
  37. options:NSLiteralSearch
  38. range:NSMakeRange(0, [result length])];
  39. return (nil == result ? @"" : result);
  40. }
  41. @interface AudioScrobbler (Private)
  42. - (NSMutableArray *) queue;
  43. - (NSString *) pluginID;
  44. - (void) sendCommand:(NSString *)command;
  45. - (BOOL) keepProcessingAudioScrobblerCommands;
  46. - (void) setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands;
  47. - (BOOL) audioScrobblerThreadCompleted;
  48. - (void) setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted;
  49. - (semaphore_t) semaphore;
  50. - (void) processAudioScrobblerCommands:(id)unused;
  51. @end
  52. @implementation AudioScrobbler
  53. + (BOOL) isRunning
  54. {
  55. NSArray *launchedApps = [[NSWorkspace sharedWorkspace] runningApplications];
  56. BOOL running = NO;
  57. for(NSRunningApplication *app in launchedApps) {
  58. if([[app bundleIdentifier] isEqualToString:@"fm.last.Last.fm"] ||
  59. [[app bundleIdentifier] isEqualToString:@"fm.last.Scrobbler"]) {
  60. running = YES;
  61. break;
  62. }
  63. }
  64. return running;
  65. }
  66. - (id) init
  67. {
  68. if((self = [super init])) {
  69. _pluginID = @"cog";
  70. if([[NSUserDefaults standardUserDefaults] boolForKey:@"automaticallyLaunchLastFM"]) {
  71. if(![AudioScrobbler isRunning]) {
  72. [[NSWorkspace sharedWorkspace] launchApplication:@"Last.fm.app"];
  73. }
  74. }
  75. _keepProcessingAudioScrobblerCommands = YES;
  76. kern_return_t result = semaphore_create(mach_task_self(), &_semaphore, SYNC_POLICY_FIFO, 0);
  77. if(KERN_SUCCESS != result) {
  78. ALog(@"Couldn't create semaphore (%s).", mach_error_type(result));
  79. self = nil;
  80. return nil;
  81. }
  82. [NSThread detachNewThreadSelector:@selector(processAudioScrobblerCommands:) toTarget:self withObject:nil];
  83. }
  84. return self;
  85. }
  86. - (void) dealloc
  87. {
  88. if([self keepProcessingAudioScrobblerCommands] || NO == [self audioScrobblerThreadCompleted])
  89. [self shutdown];
  90. _queue = nil;
  91. semaphore_destroy(mach_task_self(), _semaphore); _semaphore = 0;
  92. }
  93. - (void) start:(PlaylistEntry *)pe
  94. {
  95. [self sendCommand:[NSString stringWithFormat:@"START c=%@&a=%@&t=%@&b=%@&m=%@&l=%i&p=%@\n",
  96. [self pluginID],
  97. escapeForLastFM([pe artist]),
  98. escapeForLastFM([pe title]),
  99. escapeForLastFM([pe album]),
  100. @"", // TODO: MusicBrainz support
  101. [[pe length] intValue],
  102. escapeForLastFM([[pe URL] path])
  103. ]];
  104. }
  105. - (void) stop
  106. {
  107. [self sendCommand:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]];
  108. }
  109. - (void) pause
  110. {
  111. [self sendCommand:[NSString stringWithFormat:@"PAUSE c=%@\n", [self pluginID]]];
  112. }
  113. - (void) resume
  114. {
  115. [self sendCommand:[NSString stringWithFormat:@"RESUME c=%@\n", [self pluginID]]];
  116. }
  117. - (void) shutdown
  118. {
  119. [self setKeepProcessingAudioScrobblerCommands:NO];
  120. semaphore_signal([self semaphore]);
  121. // Wait for the thread to terminate
  122. while(NO == [self audioScrobblerThreadCompleted])
  123. [[NSRunLoop currentRunLoop] runMode:AudioScrobblerRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.2]];
  124. }
  125. @end
  126. @implementation AudioScrobbler (Private)
  127. - (NSMutableArray *) queue
  128. {
  129. if(nil == _queue)
  130. _queue = [[NSMutableArray alloc] init];
  131. return _queue;
  132. }
  133. - (NSString *) pluginID
  134. {
  135. return _pluginID;
  136. }
  137. - (void) sendCommand:(NSString *)command
  138. {
  139. @synchronized([self queue]) {
  140. [[self queue] addObject:command];
  141. }
  142. semaphore_signal([self semaphore]);
  143. }
  144. - (BOOL) keepProcessingAudioScrobblerCommands
  145. {
  146. return _keepProcessingAudioScrobblerCommands;
  147. }
  148. - (void) setKeepProcessingAudioScrobblerCommands:(BOOL)keepProcessingAudioScrobblerCommands
  149. {
  150. _keepProcessingAudioScrobblerCommands = keepProcessingAudioScrobblerCommands;
  151. }
  152. - (BOOL) audioScrobblerThreadCompleted
  153. {
  154. return _audioScrobblerThreadCompleted;
  155. }
  156. - (void) setAudioScrobblerThreadCompleted:(BOOL)audioScrobblerThreadCompleted
  157. {
  158. _audioScrobblerThreadCompleted = audioScrobblerThreadCompleted;
  159. }
  160. - (semaphore_t) semaphore
  161. {
  162. return _semaphore;
  163. }
  164. - (void) processAudioScrobblerCommands:(id)unused
  165. {
  166. @autoreleasepool {
  167. AudioScrobblerClient *client = [[AudioScrobblerClient alloc] init];
  168. mach_timespec_t timeout = { 5, 0 };
  169. NSString *command = nil;
  170. NSString *response = nil;
  171. in_port_t port = 33367;
  172. while([self keepProcessingAudioScrobblerCommands]) {
  173. @autoreleasepool {
  174. // Get the first command to be sent
  175. @synchronized([self queue]) {
  176. if ([[self queue] count]) {
  177. command = [[self queue] objectAtIndex:0];
  178. [[self queue] removeObjectAtIndex:0];
  179. }
  180. }
  181. if(nil != command) {
  182. @try {
  183. if([client connectToHost:@"localhost" port:port]) {
  184. port = [client connectedPort];
  185. [client send:command];
  186. command = nil;
  187. response = [client receive];
  188. if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)])
  189. ALog(@"AudioScrobbler error: %@", response);
  190. [client shutdown];
  191. }
  192. }
  193. @catch(NSException *exception) {
  194. command = nil;
  195. [client shutdown];
  196. // ALog(@"Exception: %@",exception);
  197. continue;
  198. }
  199. }
  200. semaphore_timedwait([self semaphore], timeout);
  201. }
  202. }
  203. // Send a final stop command to cleanup
  204. @try {
  205. if([client connectToHost:@"localhost" port:port]) {
  206. [client send:[NSString stringWithFormat:@"STOP c=%@\n", [self pluginID]]];
  207. response = [client receive];
  208. if(2 > [response length] || NSOrderedSame != [response compare:@"OK" options:NSLiteralSearch range:NSMakeRange(0,2)])
  209. ALog(@"AudioScrobbler error: %@", response);
  210. [client shutdown];
  211. }
  212. }
  213. @catch(NSException *exception) {
  214. [client shutdown];
  215. }
  216. [self setAudioScrobblerThreadCompleted:YES];
  217. }
  218. }
  219. @end