Added audioscrobbler to the preferences. Haven't hooked it up to the keychain yet.
[MenuTunes.git] / AudioscrobblerController.m
1 /*
2  *      MenuTunes
3  *  AudioscrobblerController
4  *    Audioscrobbler Support Class
5  *
6  *  Original Author : Kent Sutherland <kent.sutherland@ithinksw.com>
7  *   Responsibility : Kent Sutherland <kent.sutherland@ithinksw.com>
8  *
9  *  Copyright (c) 2005 iThink Software.
10  *  All Rights Reserved
11  *
12  */
13
14 #import "AudioscrobblerController.h"
15 #import <openssl/evp.h>
16 #import <ITFoundation/ITDebug.h>
17
18 static AudioscrobblerController *_sharedController = nil;
19
20 @implementation AudioscrobblerController
21
22 + (void)load
23 {
24         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
25         [[AudioscrobblerController sharedController] submitTrack:@"Immigrant Song" artist:@"Led Zeppelin" album:@"How The West Was Won" length:221];
26         [[AudioscrobblerController sharedController] submitTrack:@"Comfortably Numb" artist:@"Pink Floyd" album:@"The Wall" length:384];
27         [[AudioscrobblerController sharedController] submitTracks];
28         [pool release];
29 }
30
31 + (AudioscrobblerController *)sharedController
32 {
33         if (!_sharedController) {
34                 _sharedController = [[AudioscrobblerController alloc] init];
35         }
36         return _sharedController;
37 }
38
39 - (id)init
40 {
41         if ( (self = [super init]) ) {
42                 /*_handshakeCompleted = NO;
43                 _md5Challenge = nil;
44                 _postURL = nil;*/
45                 
46                 _handshakeCompleted = YES;
47                 _md5Challenge = @"rawr";
48                 _postURL = [NSURL URLWithString:@"http://audioscrobbler.com/"];
49                 
50                 _delayDate = nil;
51                 _responseData = nil;
52                 _tracks = [[NSMutableArray alloc] init];
53                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAudioscrobblerNotification:) name:nil object:self];
54         }
55         return self;
56 }
57
58 - (void)dealloc
59 {
60         [_md5Challenge release];
61         [_postURL release];
62         [_responseData release];
63         [_tracks release];
64         [super dealloc];
65 }
66
67 - (void)attemptHandshake
68 {
69         //Delay if we haven't met the interval time limit
70         NSTimeInterval interval = [_delayDate timeIntervalSinceNow];
71         if (interval > 0) {
72                 ITDebugLog(@"Audioscrobbler: Delaying handshake attempt for %i seconds", interval);
73                 [self performSelector:@selector(attemptHandshake) withObject:nil afterDelay:interval + 1];
74                 return;
75         }
76         
77         if (!_handshakeCompleted) {
78                 NSString *version = [[[NSBundle bundleWithPath:[[NSWorkspace sharedWorkspace] fullPathForApplication:@"iTunes.app"]] infoDictionary] objectForKey:@"CFBundleVersion"], *user = @"Tristrex";
79                 NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://post.audioscrobbler.com/?hs=true&p=1.1&c=tst&v=%@&u=%@", version, user]];
80                 NSURLConnection *connection;
81                 
82                 _currentStatus = AudioscrobblerRequestingHandshakeStatus;
83                 connection = [NSURLConnection connectionWithRequest:[NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30] delegate:self];
84         }
85 }
86
87 - (BOOL)handshakeCompleted
88 {
89         return _handshakeCompleted;
90 }
91
92 - (void)submitTrack:(NSString *)title artist:(NSString *)artist album:(NSString *)album length:(int)length
93 {
94         if (!_handshakeCompleted) {
95                 [self attemptHandshake];
96                 return;
97         }
98         ITDebugLog(@"Audioscrobbler: Adding a new track to the submission queue.");
99         NSDictionary *newTrack = [NSDictionary dictionaryWithObjectsAndKeys:title,
100                                                                                                                                                 @"title",
101                                                                                                                                                 artist,
102                                                                                                                                                 @"artist",
103                                                                                                                                                 album,
104                                                                                                                                                 @"album",
105                                                                                                                                                 [NSString stringWithFormat:@"%i", length],
106                                                                                                                                                 @"length",
107                                                                                                                                                 [[NSDate date] descriptionWithCalendarFormat:@"%Y-%m-%d %H:%M:%S" timeZone:nil locale:nil],
108                                                                                                                                                 @"time",
109                                                                                                                                                 nil, nil];
110         [_tracks addObject:newTrack];
111 }
112
113 - (void)submitTracks
114 {
115         NSTimeInterval interval = [_delayDate timeIntervalSinceNow];
116         if (interval > 0) {
117                 ITDebugLog(@"Audioscrobbler: Delaying track submission for %i seconds", interval);
118                 [self performSelector:@selector(attemptHandshake) withObject:nil afterDelay:interval + 1];
119                 return;
120         }
121         
122         int i;
123         NSMutableString *requestString;
124         NSString *authString, *responseHash = @"";
125         char *pass = "waffles";
126         unsigned char *buffer;
127         EVP_MD_CTX ctx;
128         
129         if (!_handshakeCompleted) {
130                 [self attemptHandshake];
131                 return;
132         }
133         
134         ITDebugLog(@"Audioscrobbler: Submitting queued tracks");
135         
136         if ([_tracks count] == 0) {
137                 ITDebugLog(@"Audioscrobbler: No queued tracks to submit.");
138                 return;
139         }
140         
141         //Build the MD5 response string we send along with the request
142         buffer = malloc(EVP_MD_size(EVP_md5()));
143         EVP_DigestInit(&ctx, EVP_md5());
144         EVP_DigestUpdate(&ctx, pass, strlen(pass));
145         EVP_DigestFinal(&ctx, buffer, NULL);
146         
147         for (i = 0; i < 16; i++) {
148                 responseHash = [responseHash stringByAppendingFormat:@"%0.2x", buffer[i]];
149         }
150         
151         free(buffer);
152         buffer = malloc(EVP_MD_size(EVP_md5()));
153         char *cat = (char *)[[responseHash stringByAppendingString:_md5Challenge] UTF8String];
154         EVP_DigestInit(&ctx, EVP_md5());
155         EVP_DigestUpdate(&ctx, cat, strlen(cat));
156         EVP_DigestFinal(&ctx, buffer, NULL);
157         
158         responseHash = @"";
159         for (i = 0; i < 16; i++) {
160                 responseHash = [responseHash stringByAppendingFormat:@"%0.2x", buffer[i]];
161         }
162         free(buffer);
163         
164         authString = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[NSString stringWithFormat:@"u=%@&s=%@", @"Tristrex", responseHash], NULL, NULL, kCFStringEncodingUTF8);
165         requestString = [[NSMutableString alloc] initWithString:authString];
166         [authString release];
167         
168         //We can only submit ten tracks at a time
169         for (i = 0; (i < [_tracks count]) && (i < 10); i++) {
170                 NSDictionary *nextTrack = [_tracks objectAtIndex:i];
171                 NSString *trackString;
172                 
173                 trackString = (NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)[NSString stringWithFormat:@"&a[%i]=%@&t[%i]=%@&b[%i]=%@&m[%i]=&l[%i]=%@&i[%i]=%@", i, [nextTrack objectForKey:@"artist"], i, [nextTrack objectForKey:@"title"], i, [nextTrack objectForKey:@"album"], i, i, [nextTrack objectForKey:@"length"], i, [nextTrack objectForKey:@"time"]], NULL, NULL, kCFStringEncodingUTF8);
174                 [requestString appendString:trackString];
175                 [trackString release];
176         }
177         
178         //Create and send the request
179         NSMutableURLRequest *request = [[NSURLRequest requestWithURL:_postURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30] mutableCopy];
180         [request setHTTPMethod:@"POST"];
181         [request setHTTPBody:[requestString dataUsingEncoding:NSUTF8StringEncoding]];
182         _currentStatus = AudioscrobblerSubmittingTracksStatus;
183         //[NSURLConnection connectionWithRequest:request delegate:self];
184         [requestString release];
185         [request release];
186         
187         //If we have tracks left, submit again after the interval seconds
188 }
189
190 - (void)handleAudioscrobblerNotification:(NSNotification *)note
191 {
192         if ([[note name] isEqualToString:@"AudioscrobblerHandshakeComplete"]) {
193                 if ([_tracks count] > 0) {
194                         [self submitTracks];
195                 }
196                 [self submitTrack:@"Immigrant Song" artist:@"Led Zeppelin" album:@"How The West Was Won" length:221];
197                 [self submitTrack:@"Comfortably Numb" artist:@"Pink Floyd" album:@"The Wall" length:384];
198                 [self submitTracks];
199         }
200 }
201
202 #pragma mark -
203
204 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
205 {
206         ITDebugLog(@"Audioscrobbler: Connection error \"%@\"", error);
207 }
208
209 - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
210 {
211         [_responseData appendData:data];
212 }
213
214 - (void)connectionDidFinishLoading:(NSURLConnection *)connection
215 {
216         NSString *string = [[NSString alloc] initWithData:_responseData encoding:NSASCIIStringEncoding];
217         NSArray *lines = [string componentsSeparatedByString:@"\n"];
218         NSString *responseAction = nil;
219         
220         if ([lines count] > 0) {
221                 responseAction = [lines objectAtIndex:0];
222         }
223         
224         if (_currentStatus == AudioscrobblerRequestingHandshakeStatus) {
225                 if ([lines count] < 2) {
226                         //We have a protocol error
227                 }
228                 if ([responseAction isEqualToString:@"UPTODATE"]) {
229                         if ([lines count] >= 4) {
230                                 _md5Challenge = [[lines objectAtIndex:1] retain];
231                                 _postURL = [[NSURL alloc] initWithString:[lines objectAtIndex:2]];
232                                 _handshakeCompleted = YES;
233                                 [[NSNotificationCenter defaultCenter] postNotificationName:@"AudioscrobblerHandshakeComplete" object:self];
234                         } else {
235                                 //We have a protocol error
236                         }
237                         //Something
238                 } else if (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"UPDATE"]) {
239                         //Something plus update action
240                 } else if (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"FAILED"]) {
241                         //We have a error
242                 } else if ([responseAction isEqualToString:@"BADUSER"]) {
243                         //We have a bad user
244                 } else {
245                         //We have a protocol
246                 }
247         } else if (_currentStatus == AudioscrobblerSubmittingTracksStatus) {
248                 if ([responseAction isEqualToString:@"OK"]) {
249                 } else if ([responseAction isEqualToString:@"BADAUTH"]) {
250                         //Bad auth
251                 } else if (([responseAction length] > 5) && [[responseAction substringToIndex:5] isEqualToString:@"FAILED"]) {
252                         //Failed
253                 }
254                 NSLog(string);
255         }
256         
257         //Handle the final INTERVAL response
258         if (([responseAction length] > 9) && [[responseAction substringToIndex:7] isEqualToString:@"INTERVAL"]) {
259                 int seconds = [[[lines objectAtIndex:[lines count] - 1] substringFromIndex:9] intValue];
260                 ITDebugLog(@"Audioscrobbler: INTERVAL %i", seconds);
261                 _delayDate = [NSDate dateWithTimeIntervalSinceNow:seconds];
262         } else {
263                 //We have a protocol error
264         }
265         
266         [string release];
267         [_responseData release];
268 }
269
270 -(NSURLRequest *)connection:(NSURLConnection *)connection willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
271 {
272         ITDebugLog(@"Audioscrobbler: Sending URL request.");
273         NSLog(@"Sending request.");
274         _responseData = [[NSMutableData alloc] init];
275         return request;
276 }
277
278 @end