-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathAudioscrobblerController.m
376 lines (318 loc) · 14.2 KB
/
AudioscrobblerController.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
#import "AudioscrobblerController.h"
#import "PreferencesController.h"
#import <openssl/evp.h>
#import <ITFoundation/ITDebug.h>
#define AUDIOSCROBBLER_ID @"mtu"
#define AUDIOSCROBBLER_VERSION [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]
static AudioscrobblerController *_sharedController = nil;
@implementation AudioscrobblerController
+ (AudioscrobblerController *)sharedController
{
if (!_sharedController) {
_sharedController = [[AudioscrobblerController alloc] init];
}
return _sharedController;
}
- (id)init
{
if ( (self = [super init]) ) {
_handshakeCompleted = NO;
_md5Challenge = nil;
_postURL = nil;
/*_handshakeCompleted = YES;
_md5Challenge = @"rawr";
_postURL = [NSURL URLWithString:@"http://audioscrobbler.com/"];*/
_delayDate = [[NSDate date] retain];
_responseData = [[NSMutableData alloc] init];
_tracks = [[NSMutableArray alloc] init];
_submitTracks = [[NSMutableArray alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioscrobblerNotification:) name:@"AudioscrobblerHandshakeComplete" object:self];
}
return self;
}
- (void)dealloc
{
[_lastStatus release];
[_md5Challenge release];
[_postURL release];
[_responseData release];
[_submitTracks release];
[_tracks release];
[_delayDate release];
[super dealloc];
}
- (NSString *)lastStatus
{
return _lastStatus;
}
- (void)attemptHandshake
{
[self attemptHandshake:NO];
}
- (void)attemptHandshake:(BOOL)force
{
if (_handshakeCompleted && !force) {
return;
}
//If we've already tried to handshake three times in a row unsuccessfully, set the attempt count to -3
if (_handshakeAttempts > 3) {
ITDebugLog(@"Audioscrobbler: Maximum handshake limit reached (3). Retrying when handshake attempts reach zero.");
_handshakeAttempts = -3;
//Remove any tracks we were trying to submit, just to be safe
[_submitTracks removeAllObjects];
return;
}
//Increment the number of times we've tried to handshake
_handshakeAttempts++;
//We're still on our self-imposed cooldown time.
if (_handshakeAttempts < 0) {
ITDebugLog(@"Audioscrobbler: Handshake timeout. Retrying when handshake attempts reach zero.");
return;
}
//Delay if we haven't met the interval time limit
NSTimeInterval interval = [_delayDate timeIntervalSinceNow];
if (interval > 0) {
ITDebugLog(@"Audioscrobbler: Delaying handshake attempt for %i seconds", interval);
[self performSelector:@selector(attemptHandshake) withObject:nil afterDelay:interval + 1];
return;
}
NSString *user = [[NSUserDefaults standardUserDefaults] stringForKey:@"audioscrobblerUser"];
if (!_handshakeCompleted && user) {
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://post.audioscrobbler.com/?hs=true&p=1.1&c=%@&v=%@&u=%@", AUDIOSCROBBLER_ID, AUDIOSCROBBLER_VERSION, user]];
[_lastStatus release];
_lastStatus = [NSLocalizedString(@"audioscrobbler_handshaking", @"Attempting to handshake with server") retain];
[[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerStatusChanged" object:nil userInfo:[NSDictionary dictionaryWithObject:_lastStatus forKey:@"StatusString"]];
_currentStatus = AudioscrobblerRequestingHandshakeStatus;
//_responseData = [[NSMutableData alloc] init];
[NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15] delegate:self];
}
}
- (BOOL)handshakeCompleted
{
return _handshakeCompleted;
}
- (void)submitTrack:(NSString *)title artist:(NSString *)artist album:(NSString *)album length:(int)length
{
ITDebugLog(@"Audioscrobbler: Adding a new track to the submission queue.");
NSDictionary *newTrack = [NSDictionary dictionaryWithObjectsAndKeys:title,
@"title",
artist,
@"artist",
(album == nil) ? @"" : album,
@"album",
[NSString stringWithFormat:@"%i", length],
@"length",
[[NSDate date] descriptionWithCalendarFormat:@"%Y-%m-%d %H:%M:%S" timeZone:nil locale:nil],
@"time",
nil, nil];
[_tracks addObject:newTrack];
[self submitTracks];
}
- (void)submitTracks
{
if (!_handshakeCompleted) {
[self attemptHandshake:NO];
return;
}
ITDebugLog(@"Audioscrobbler: Submitting queued tracks");
if ([_tracks count] == 0) {
ITDebugLog(@"Audioscrobbler: No queued tracks to submit.");
return;
}
NSString *user = [[NSUserDefaults standardUserDefaults] stringForKey:@"audioscrobblerUser"], *passString = [PreferencesController getKeychainItemPasswordForUser:user];
char *pass = (char *)[passString UTF8String];
if (passString == nil) {
ITDebugLog(@"Audioscrobbler: Access denied to user password");
return;
}
NSTimeInterval interval = [_delayDate timeIntervalSinceNow];
if (interval > 0) {
ITDebugLog(@"Audioscrobbler: Delaying track submission for %f seconds", interval);
[self performSelector:@selector(submitTracks) withObject:nil afterDelay:interval + 1];
return;
}
int i;
NSMutableString *requestString;
NSString *authString, *responseHash = @"";
unsigned char *buffer;
EVP_MD_CTX ctx;
//Build the MD5 response string we send along with the request
buffer = malloc(EVP_MD_size(EVP_md5()));
EVP_DigestInit(&ctx, EVP_md5());
EVP_DigestUpdate(&ctx, pass, strlen(pass));
EVP_DigestFinal(&ctx, buffer, NULL);
for (i = 0; i < 16; i++) {
responseHash = [responseHash stringByAppendingFormat:@"%0.2x", buffer[i]];
}
free(buffer);
buffer = malloc(EVP_MD_size(EVP_md5()));
char *cat = (char *)[[responseHash stringByAppendingString:_md5Challenge] UTF8String];
EVP_DigestInit(&ctx, EVP_md5());
EVP_DigestUpdate(&ctx, cat, strlen(cat));
EVP_DigestFinal(&ctx, buffer, NULL);
responseHash = @"";
for (i = 0; i < 16; i++) {
responseHash = [responseHash stringByAppendingFormat:@"%0.2x", buffer[i]];
}
free(buffer);
authString = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[NSString stringWithFormat:@"u=%@&s=%@", user, responseHash], NULL, NULL, kCFStringEncodingUTF8);
requestString = [[NSMutableString alloc] initWithString:authString];
[authString release];
//We can only submit ten tracks at a time
for (i = 0; (i < [_tracks count]) && (i < 10); i++) {
NSDictionary *nextTrack = [_tracks objectAtIndex:i];
NSString *artistEscaped, *titleEscaped, *albumEscaped, *timeEscaped, *ampersand = @"&";
//Escape each of the individual parameters we're sending
artistEscaped = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[nextTrack objectForKey:@"artist"], NULL, (CFStringRef)ampersand, kCFStringEncodingUTF8);
titleEscaped = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[nextTrack objectForKey:@"title"], NULL, (CFStringRef)ampersand, kCFStringEncodingUTF8);
albumEscaped = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[nextTrack objectForKey:@"album"], NULL, (CFStringRef)ampersand, kCFStringEncodingUTF8);
timeEscaped = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[nextTrack objectForKey:@"time"], NULL, (CFStringRef)ampersand, kCFStringEncodingUTF8);
[requestString appendString:[NSString stringWithFormat:@"&a[%i]=%@&t[%i]=%@&b[%i]=%@&m[%i]=&l[%i]=%@&i[%i]=%@", i, artistEscaped,
i, titleEscaped,
i, albumEscaped,
i,
i, [nextTrack objectForKey:@"length"],
i, timeEscaped]];
//Release the escaped strings
[artistEscaped release];
[titleEscaped release];
[albumEscaped release];
[timeEscaped release];
[_submitTracks addObject:nextTrack];
}
ITDebugLog(@"Audioscrobbler: Sending track submission request");
[_lastStatus release];
_lastStatus = [NSLocalizedString(@"audioscrobbler_submitting", @"Submitting tracks to server") retain];
[[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerStatusChanged" object:nil userInfo:[NSDictionary dictionaryWithObject:_lastStatus forKey:@"StatusString"]];
//Create and send the request
NSMutableURLRequest *request = [[NSURLRequest requestWithURL:_postURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15] mutableCopy];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[requestString dataUsingEncoding:NSUTF8StringEncoding]];
_currentStatus = AudioscrobblerSubmittingTracksStatus;
//_responseData = [[NSMutableData alloc] init];
[_responseData setData:nil];
[NSURLConnection connectionWithRequest:request delegate:self];
[requestString release];
[request release];
//For now we're not going to cache results, as it is less of a headache
//[_tracks removeObjectsInArray:_submitTracks];
[_tracks removeAllObjects];
//[_submitTracks removeAllObjects];
//If we have tracks left, submit again after the interval seconds
}
- (void)handleAudioscrobblerNotification:(NSNotification *)note
{
if ([_tracks count] > 0) {
[self performSelector:@selector(submitTracks) withObject:nil afterDelay:2];
}
}
#pragma mark -
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[_responseData setData:nil];
[_lastStatus release];
_lastStatus = [[NSString stringWithFormat:NSLocalizedString(@"audioscrobbler_error", @"Error - %@"), [error localizedDescription]] retain];
[[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerStatusChanged" object:self userInfo:[NSDictionary dictionaryWithObject:_lastStatus forKey:@"StatusString"]];
ITDebugLog(@"Audioscrobbler: Connection error \"%@\"", error);
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[_responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *string = [[NSString alloc] initWithData:_responseData encoding:NSASCIIStringEncoding];
NSArray *lines = [string componentsSeparatedByString:@"\n"];
NSString *responseAction = nil, *key = nil, *comment = nil;
if ([lines count] > 0) {
responseAction = [lines objectAtIndex:0];
}
ITDebugLog(@"Audioscrobbler: Response %@", string);
if (_currentStatus == AudioscrobblerRequestingHandshakeStatus) {
if ([lines count] < 2) {
//We have a protocol error
}
if ([responseAction isEqualToString:@"UPTODATE"] || (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"UPDATE"])) {
if ([lines count] >= 4) {
_md5Challenge = [[lines objectAtIndex:1] retain];
_postURL = [[NSURL alloc] initWithString:[lines objectAtIndex:2]];
_handshakeCompleted = YES;
[[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerHandshakeComplete" object:self];
key = @"audioscrobbler_handshake_complete";
comment = @"Handshake complete";
_handshakeAttempts = 0;
} else {
//We have a protocol error
}
} else if (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"FAILED"]) {
ITDebugLog(@"Audioscrobbler: Handshake failed (%@)", [responseAction substringFromIndex:6]);
key = @"audioscrobbler_handshake_failed";
comment = @"Handshake failed";
//We have a error
} else if ([responseAction isEqualToString:@"BADUSER"]) {
ITDebugLog(@"Audioscrobbler: Bad user name");
key = @"audioscrobbler_bad_user";
comment = @"Handshake failed - invalid user name";
//We have a bad user
//Don't count this as a bad handshake attempt
_handshakeAttempts = 0;
} else {
ITDebugLog(@"Audioscrobbler: Handshake failed, protocol error");
key = @"audioscrobbler_protocol_error";
comment = @"Internal protocol error";
//We have a protocol error
}
} else if (_currentStatus == AudioscrobblerSubmittingTracksStatus) {
if ([responseAction isEqualToString:@"OK"]) {
ITDebugLog(@"Audioscrobbler: Submission successful, clearing queue.");
/*[_tracks removeObjectsInArray:_submitTracks];
[_submitTracks removeAllObjects];*/
[_submitTracks removeAllObjects];
if ([_tracks count] > 0) {
ITDebugLog(@"Audioscrobbler: Tracks remaining in queue, submitting remaining tracks");
[self performSelector:@selector(submitTracks) withObject:nil afterDelay:2];
}
key = @"audioscrobbler_submission_ok";
comment = @"Last track submission successful";
} else if ([responseAction isEqualToString:@"BADAUTH"]) {
ITDebugLog(@"Audioscrobbler: Bad password");
key = @"audioscrobbler_bad_password";
comment = @"Last track submission failed - invalid password";
//Bad auth
//Add the tracks we were trying to submit back into the submission queue
[_tracks addObjectsFromArray:_submitTracks];
_handshakeCompleted = NO;
//If we were previously valid with the same login name, try reauthenticating and sending again
[self attemptHandshake:YES];
} else if (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"FAILED"]) {
ITDebugLog(@"Audioscrobbler: Submission failed (%@)", [responseAction substringFromIndex:6]);
key = @"audioscrobbler_submission_failed";
comment = @"Last track submission failed - see console for error";
//Failed
//We got an unknown error. To be safe we're going to remove the tracks we tried to submit
[_submitTracks removeAllObjects];
_handshakeCompleted = NO;
}
}
//Handle the final INTERVAL response
if (([[lines objectAtIndex:[lines count] - 2] length] > 9) && [[[lines objectAtIndex:[lines count] - 2] substringToIndex:8] isEqualToString:@"INTERVAL"]) {
int seconds = [[[lines objectAtIndex:[lines count] - 2] substringFromIndex:9] intValue];
ITDebugLog(@"Audioscrobbler: INTERVAL %i", seconds);
[_delayDate release];
_delayDate = [[NSDate dateWithTimeIntervalSinceNow:seconds] retain];
} else {
ITDebugLog(@"No interval response.");
//We have a protocol error
}
[_lastStatus release];
_lastStatus = [NSLocalizedString(key, comment) retain];
[[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerStatusChanged" object:nil userInfo:[NSDictionary dictionaryWithObject:_lastStatus forKey:@"StatusString"]];
[string release];
[_responseData setData:nil];
}
-(NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
//Don't cache any Audioscrobbler communication
return nil;
}
@end