New API.
[clinton/xbmc-groove.git] / resources / lib / GrooveAPI.py
CommitLineData
e278f474 1import urllib2, md5, os, traceback, sys, pickle, socket, xbmc
2from operator import itemgetter
8817bb2e 3
4class LoginTokensExceededError(Exception):
5 def __init__(self):
6 self.value = 'You have created to many tokens. Only 12 are allowed'
7 def __str__(self):
8 return repr(self.value)
9
10class LoginUnknownError(Exception):
11 def __init__(self):
12 self.value = 'Unable to get a new session ID. Wait a few minutes and try again'
13 def __str__(self):
14 return repr(self.value)
15
16class SessionIDTryAgainError(Exception):
17 def __init__(self):
18 self.value = 'Unable to get a new session ID. Wait a few minutes and try again'
19 def __str__(self):
20 return repr(self.value)
21
22class GrooveAPI:
23 def __init__(self, enableDebug = False, isXbox = False):
b738088f 24 import simplejson
25 self.simplejson = simplejson
8817bb2e 26 timeout = 40
27 socket.setdefaulttimeout(timeout)
28 self.enableDebug = enableDebug
29 self.loggedIn = 0
8817bb2e 30 self.userId = 0
b738088f 31 self.removeDuplicates = False
3fcef5ba 32
33 self.radioRecentSongs = []
34 self.radioRecentArtists = []
35 self.radioEnabled = False
36
973b4c6c 37 self.dataDir = 'addon_data'
b738088f 38 self.confDir = xbmc.translatePath(os.path.join('special://masterprofile/' + self.dataDir, os.path.basename(os.getcwd())))
8817bb2e 39 self.sessionID = self.getSavedSession()
40 self.debug('Saved sessionID: ' + self.sessionID)
b738088f 41 #self.sessionID = self.getSessionFromAPI()
42 #self.debug('API sessionID: ' + self.sessionID)
8817bb2e 43 if self.sessionID == '':
44 self.sessionID = self.startSession()
45 self.debug('Start() sessionID: ' + self.sessionID)
46 if self.sessionID == '':
47 self.debug('Could not get a sessionID. Try again in a few minutes')
48 raise SessionIDTryAgainError()
49 else:
50 self.saveSession()
51
52 self.debug('sessionID: ' + self.sessionID)
53
54 def __del__(self):
55 try:
56 if self.loggedIn == 1:
57 self.logout()
58 except:
59 pass
60
61 def debug(self, msg):
62 if self.enableDebug == True:
63 print msg
b738088f 64
65 def setRemoveDuplicates(self, enable):
66 if enable == True or enable == 'true' or enable == 'True':
67 self.removeDuplicates = True
68 else:
69 self.removeDuplicates = False
8817bb2e 70
71 def getSavedSession(self):
72 sessionID = ''
b738088f 73 path = os.path.join(self.confDir, 'session', 'session.txt')
8817bb2e 74
75 try:
76 f = open(path, 'rb')
77 sessionID = pickle.load(f)
78 f.close()
79 except:
80 sessionID = ''
81 pass
82
83 return sessionID
84
85 def saveSession(self):
86 try:
b738088f 87 dir = os.path.join(self.confDir, 'session')
8817bb2e 88 # Create the 'data' directory if it doesn't exist.
89 if not os.path.exists(dir):
b738088f 90 os.makedirs(dir)
8817bb2e 91 path = os.path.join(dir, 'session.txt')
92 f = open(path, 'wb')
93 pickle.dump(self.sessionID, f, protocol=pickle.HIGHEST_PROTOCOL)
94 f.close()
95 except IOError, e:
96 print 'There was an error while saving the session pickle (%s)' % e
97 pass
98 except:
99 print "An unknown error occured during save session: " + str(sys.exc_info()[0])
100 pass
101
102 def saveSettings(self):
103 try:
b738088f 104 dir = os.path.join(self.confDir, 'data')
8817bb2e 105 # Create the 'data' directory if it doesn't exist.
106 if not os.path.exists(dir):
b738088f 107 os.makedirs(dir)
8817bb2e 108 path = os.path.join(dir, 'settings1.txt')
109 f = open(path, 'wb')
110 pickle.dump(self.settings, f, protocol=pickle.HIGHEST_PROTOCOL)
111 f.close()
112 except IOError, e:
113 print 'There was an error while saving the settings pickle (%s)' % e
114 pass
115 except:
116 print "An unknown error occured during save settings\n"
117 pass
118
119 def callRemote(self, method, params={}):
120 data = {'header': {'sessionID': self.sessionID}, 'method': method, 'parameters': params}
8817bb2e 121 data = self.simplejson.dumps(data)
8817bb2e 122 req = urllib2.Request("http://api.grooveshark.com/ws/1.0/?json")
123 req.add_header('Host', 'api.grooveshark.com')
124 req.add_header('Content-type', 'text/json')
125 req.add_header('Content-length', str(len(data)))
126 req.add_data(data)
127 response = urllib2.urlopen(req)
128 result = response.read()
129 response.close()
130 try:
131 result = self.simplejson.loads(result)
132 if 'fault' in result:
133 self.debug(result)
b738088f 134 if result['fault']['code'] == 8: #Session ID has expired. Get a new and try again if possible.
135 self.debug(result['fault']['message'])
136 self.sessionID = self.startSession()
137 if self.sessionID != '':
138 self.saveSession()
139 return self.callRemote(method, params)
140 else:
141 self.debug('GrooveShark: SessionID expired, but unable to get new')
142 return []
8817bb2e 143 return result
144 except:
145 return []
146
147 def startSession(self):
a3ad8f73 148 try:
149 response = urllib2.urlopen("http://www.moovida.com/services/grooveshark/session_start")
150 result = response.read()
151 result = self.simplejson.loads(result)
152 response.close()
153 except:
154 return ''
155
8817bb2e 156 if 'fault' in result:
157 return ''
158 else:
3fcef5ba 159 return result['result']['sessionID']
8817bb2e 160
161 def sessionDestroy(self):
162 return self.callRemote("session.destroy")
163
164 def getSessionFromAPI(self):
165 result = self.callRemote("session.get")
166 if 'fault' in result:
167 return ''
168 else:
169 return result['header']['sessionID']
170
171 def getStreamURL(self, songID):
172 result = self.callRemote("song.getStreamUrlEx", {"songID": songID})
173 if 'result' in result:
174 return result['result']['url']
175 else:
176 return ''
177
178 def createUserAuthToken(self, username, password):
179 hashpass = md5.new(password).hexdigest()
180 hashpass = username + hashpass
181 hashpass = md5.new(hashpass).hexdigest()
182 result = self.callRemote("session.createUserAuthToken", {"username": username, "hashpass": hashpass})
183 if 'result' in result:
184 return result['result']['token'], result['result']['userID']
185 elif 'fault' in result:
186 if result['fault']['code'] == 256:
187 return -1 # Exceeded the number of allowed tokens. Should not happen
188 else:
189 return -2 # Unknown error
190 else:
191 return -2 # Unknown error
192
193 def destroyUserAuthToken(self, token):
194 self.callRemote("session.destroyAuthToken", {"token": token})
195
196 def loginViaAuthToken(self, token):
197 result = self.callRemote("session.loginViaAuthToken", {"token": token})
198 self.destroyUserAuthToken(token)
199 if 'result' in result:
200 self.userID = result['result']['userID']
201 return result['result']['userID']
202 else:
203 return 0
204
205 def login(self, username, password):
206 if self.loggedIn == 1:
207 return self.userId
208 result = self.createUserAuthToken(username, password)
209 if result == -1:
210 raise LoginTokensExceededError()
211 elif result == -2:
212 raise LoginUnknownError()
213 else:
214 self.token = result[0]
215 self.debug('Token:' + self.token)
216 self.userId = self.loginViaAuthToken(self.token)
217 if self.userId == 0:
218 raise LoginUnknownError()
219 else:
220 self.loggedIn = 1
221 return self.userId
b738088f 222
8817bb2e 223
224 def loginExt(self, username, password):
225 if self.loggedIn == 1:
226 return self.userId
227 token = md5.new(username.lower() + md5.new(password).hexdigest()).hexdigest()
228 result = self.callRemote("session.loginExt", {"username": username, "token": token})
229 if 'result' in result:
230 if 'userID' in result['result']:
231 self.loggedIn = 1
232 self.userId = result['result']['userID']
233 return result['result']['userID']
234 else:
235 return 0
236
237
238 def loginBasic(self, username, password):
239 if self.loggedIn == 1:
240 return self.userId
241 result = self.callRemote("session.login", {"username": username, "password": password})
242 if 'result' in result:
243 if 'userID' in result['result']:
244 self.loggedIn = 1
245 self.userId = result['result']['userID']
246 return result['result']['userID']
247 else:
248 return 0
249
250 def loggedInStatus(self):
251 return self.loggedIn
252
253 def logout(self):
254 self.callRemote("session.logout", {})
255 self.loggedIn = 0
256
257 def getSongInfo(self, songID):
258 return self.callRemote("song.about", {"songID": songID})['result']['song']
259
260 def userGetFavoriteSongs(self, userID):
261 result = self.callRemote("user.getFavoriteSongs", {"userID": userID})
262 list = self.parseSongs(result)
263 return list
264
265 def userGetPlaylists(self, limit=25):
266 if self.loggedIn == 1:
267 result = self.callRemote("user.getPlaylists", {"userID": self.userId, "limit": limit})
268 if 'result' in result:
269 playlists = result['result']['playlists']
270 else:
271 return []
272 i = 0
273 list = []
274 while(i < len(playlists)):
275 p = playlists[i]
276 list.append([p['playlistName'].encode('ascii', 'ignore'), p['playlistID']])
277 i = i + 1
278 return sorted(list, key=itemgetter(0))
279 else:
280 return []
281
282 def playlistCreate(self, name, about):
283 if self.loggedIn == 1:
284 result = self.callRemote("playlist.create", {"name": name, "about": about})
285 if 'result' in result:
286 return result['result']['playlistID']
287 else:
288 return 0
289 else:
290 return 0
36cc00d7 291
292 def playlistCreateUnique(self, name, songIds):
293 if self.loggedIn == 1:
294 result = self.callRemote("playlist.createunique", {"name": name, "songIDs": songIds})
295 if 'result' in result:
296 return result['result']['playlistID']
297 else:
298 return 0
299 else:
300 return 0
8817bb2e 301
302 def playlistGetSongs(self, playlistId, limit=25):
303 result = self.callRemote("playlist.getSongs", {"playlistID": playlistId})
304 list = self.parseSongs(result)
305 return list
306
307 def playlistDelete(self, playlistId):
308 if self.loggedIn == 1:
e278f474 309 result = self.callRemote("playlist.delete", {"playlistID": playlistId})
310 if 'fault' in result:
311 return 0
312 else:
313 return 1
314 else:
315 return 0
8817bb2e 316
317 def playlistRename(self, playlistId, name):
318 if self.loggedIn == 1:
319 result = self.callRemote("playlist.rename", {"playlistID": playlistId, "name": name})
320 if 'fault' in result:
321 return 0
322 else:
323 return 1
324 else:
325 return 0
326
327 def playlistClearSongs(self, playlistId):
328 if self.loggedIn == 1:
329 return self.callRemote("playlist.clearSongs", {"playlistID": playlistId})
330
331 def playlistAddSong(self, playlistId, songId, position):
332 if self.loggedIn == 1:
333 result = self.callRemote("playlist.addSong", {"playlistID": playlistId, "songID": songId, "position": position})
4be42357 334 if 'fault' in result:
335 return 0
336 else:
337 return 1
338 else:
339 return 0
340
341 def playlistDeleteSong(self, playlistId, position):
342 if self.loggedIn == 1:
343 result = self.callRemote("playlist.removeSong", {"playlistID": playlistId, "position": position})
8817bb2e 344 if 'fault' in result:
345 return 0
346 else:
347 return 1
348 else:
349 return 0
350
351 def playlistReplace(self, playlistId, songIds):
352 if self.loggedIn == 1:
353 result = self.callRemote("playlist.replace", {"playlistID": playlistId, "songIDs": songIds})
354 if 'fault' in result:
355 return 0
356 else:
357 return 1
358 else:
359 return 0
360
3fcef5ba 361 def radioStartArtists(self):
362 radio = self.getSavedRadio()
363 if radio == None:
364 return False
365 result = self.callRemote("autoplay.startWithArtistIDs", {"artistIDs": radio['seedArtists']})
8817bb2e 366 if 'fault' in result:
e6ccfeca 367 print "Cannot autoplay artists"
3fcef5ba 368 self.radioEnabled = False
8817bb2e 369 else:
3fcef5ba 370 self.radioEnabled = True
371 return self.radioEnabled
8817bb2e 372
3fcef5ba 373 def radioStartSongs(self):
374 radio = self.getSavedRadio()
375 if radio == None:
376 return False
377 result = self.callRemote("autoplay.start", {"songIDs": radio['seedSongs']})
8817bb2e 378 if 'fault' in result:
e6ccfeca 379 print "Cannot autoplay songs"
3fcef5ba 380 self.radioEnabled = False
8817bb2e 381 else:
3fcef5ba 382 self.radioEnabled = True
383 return self.radioEnabled
8817bb2e 384
3fcef5ba 385 def radioNextSong(self):
b738088f 386 radio = self.getSavedRadio()
387 if radio == None:
388 return None
8817bb2e 389 else:
3fcef5ba 390 result = self.callRemote("autoplay.getNextSongEx", {"seedArtists": radio['seedArtists'], "frowns": radio['frowns'], "songIDsAlreadySeen": self.radioRecentSongs, "recentArtists": self.radioRecentArtists})
8817bb2e 391 if 'fault' in result:
392 return []
393 else:
394 song = self.parseSongs(result)
3fcef5ba 395 self.radioRecentSongs.append(song[0][1])
396 self.radioRecentArtists.append(song[0][7])
8817bb2e 397 return song
398
3fcef5ba 399 def radioFrown(self, songId = None):
400 radio = self.getSavedRadio()
401 if radio != None and songId != None:
402 try:
403 radio['frowns'].remove(songId)
404 except: pass
405 radio['frowns'].append(songId)
b738088f 406 return self.saveRadio(radio = radio)
407 else:
3fcef5ba 408 return False
8817bb2e 409
3fcef5ba 410 def radioArtist(self, artistId = None):
411 radio = self.getSavedRadio()
412 if radio != None and artistId != None:
413 try:
414 radio['seedArtists'].remove(artistId)
415 except: pass
416 radio['seedArtists'].append(artistId)
e6ccfeca 417 print "Saved radio"
3fcef5ba 418 return self.saveRadio(radio = radio)
8817bb2e 419 else:
e6ccfeca 420 print "Failed to get radio"
3fcef5ba 421 return False
8817bb2e 422
3fcef5ba 423 def radioSong(self, songId = None):
424 radio = self.getSavedRadio()
425 if radio != None and songId != None:
426 try:
427 radio['seedSongs'].remove(songId)
428 except: pass
429 radio['seedSongs'].append(songId)
e6ccfeca 430 print "Saved radio"
3fcef5ba 431 return self.saveRadio(radio = radio)
432 else:
e6ccfeca 433 print "Failed to get radio"
3fcef5ba 434 return False
8817bb2e 435
436 def radioTurnedOn(self):
437 return self.radioEnabled
438
3fcef5ba 439 def getSavedRadio(self):
440 path = os.path.join(self.confDir, 'radio', 'radio.dmp')
b738088f 441 try:
442 f = open(path, 'rb')
443 radio = pickle.load(f)
444 f.close()
445 except:
e6ccfeca 446 print "Failed to open " + path
3fcef5ba 447 radio = {}
448 radio['seedSongs'] = []
449 radio['seedArtists'] = []
450 radio['frowns'] = []
451 if self.saveRadio(radio) == False:
452 return None
b738088f 453 return radio
454
3fcef5ba 455 def saveRadio(self, radio): #blaher
456 if radio == {}:
457 print 'Invalid radio'
458 return False
b738088f 459 try:
460 dir = os.path.join(self.confDir, 'radio')
461 # Create the 'data' directory if it doesn't exist.
462 if not os.path.exists(dir):
463 os.mkdir(dir)
3fcef5ba 464 path = os.path.join(dir, 'radio.dmp')
b738088f 465 f = open(path, 'wb')
466 pickle.dump(radio, f, protocol=pickle.HIGHEST_PROTOCOL)
467 f.close()
3fcef5ba 468 return True
b738088f 469 except IOError, e:
470 print 'There was an error while saving the radio pickle (%s)' % e
3fcef5ba 471 return False
b738088f 472 except:
3fcef5ba 473 print "An unknown error occurred during save radio: " + str(sys.exc_info()[0])
474 return False
b738088f 475
8817bb2e 476 def favoriteSong(self, songID):
477 return self.callRemote("song.favorite", {"songID": songID})
478
479 def unfavoriteSong(self, songID):
480 return self.callRemote("song.unfavorite", {"songID": songID})
481
482 def getMethods(self):
483 return self.callRemote("service.getMethods")
484
485 def searchSongsExactMatch(self, songName, artistName, albumName):
486 result = self.callRemote("search.songExactMatch", {"songName": songName, "artistName": artistName, "albumName": albumName})
487 list = self.parseSongs(result)
488 return list
489
490 def searchSongs(self, query, limit, page=0, sortKey=6):
491 result = self.callRemote("search.songs", {"query": query, "limit": limit, "page:": page, "streamableOnly": 1})
492 list = self.parseSongs(result)
493 return list
494 #return sorted(list, key=itemgetter(sortKey))
495
496 def searchArtists(self, query, limit, sortKey=0):
497 result = self.callRemote("search.artists", {"query": query, "limit": limit, "streamableOnly": 1})
498 list = self.parseArtists(result)
499 return list
500 #return sorted(list, key=itemgetter(sortKey))
501
502 def searchAlbums(self, query, limit, sortKey=2):
503 result = self.callRemote("search.albums", {"query": query, "limit": limit, "streamableOnly": 1})
504 list = self.parseAlbums(result)
505 return list
506 #return sorted(list, key=itemgetter(sortKey))
507
508 def searchPlaylists(self, query, limit):
509 result = self.callRemote("search.playlists", {"query": query, "limit": limit, "streamableOnly": 1})
510 list = self.parsePlaylists(result)
511 return list
512
513 def popularGetSongs(self, limit):
514 result = self.callRemote("popular.getSongs", {"limit": limit})
515 list = self.parseSongs(result)
516 return list
517
518 def popularGetArtists(self, limit):
519 result = self.callRemote("popular.getArtists", {"limit": limit})
520 list = self.parseArtists(result)
521 return list
522
523 def popularGetAlbums(self, limit):
524 result = self.callRemote("popular.getAlbums", {"limit": limit})
525 list = self.parseAlbums(result)
526 return list
527
b738088f 528 def artistAbout(self, artistId):
529 result = self.callRemote("artist.about", {"artistID": artistId})
530 return result
531
8817bb2e 532 def artistGetAlbums(self, artistId, limit, sortKey=2):
533 result = self.callRemote("artist.getAlbums", {"artistID": artistId, "limit": limit})
534 list = self.parseAlbums(result)
535 return list
536 #return sorted(list, key=itemgetter(sortKey))
537
538 def artistGetVerifiedAlbums(self, artistId, limit):
539 result = self.callRemote("artist.getVerifiedAlbums", {"artistID": artistId, "limit": limit})
540 list = self.parseSongs(result)
541 return list
542
543 def albumGetSongs(self, albumId, limit):
544 result = self.callRemote("album.getSongs", {"albumID": albumId, "limit": limit})
545 list = self.parseSongs(result)
546 return list
547
548 def songGetSimilar(self, songId, limit):
549 result = self.callRemote("song.getSimilar", {"songID": songId, "limit": limit})
550 list = self.parseSongs(result)
551 return list
552
b738088f 553 def artistGetSimilar(self, artistId, limit):
554 result = self.callRemote("artist.getSimilar", {"artistID": artistId, "limit": limit})
555 list = self.parseArtists(result)
556 return list
557
558 def songAbout(self, songId):
559 result = self.callRemote("song.about", {"songID": songId})
560 return result['result']['song']
561
562 def getVersion(self):
563 result = self.callRemote("service.getVersion", {})
564 return result
565
8817bb2e 566 def parseSongs(self, items):
567 try:
568 if 'result' in items:
569 i = 0
570 list = []
571 if 'songs' in items['result']:
572 l = len(items['result']['songs'])
573 index = 'songs'
574 elif 'song' in items['result']:
575 l = 1
576 index = 'song'
577 else:
578 l = 0
579 index = ''
580 while(i < l):
581 if index == 'songs':
582 s = items['result'][index][i]
583 else:
584 s = items['result'][index]
585 if 'estDurationSecs' in s:
586 dur = s['estDurationSecs']
587 else:
588 dur = 0
589 try:
590 notIn = True
591 for entry in list:
592 songName = s['songName'].encode('ascii', 'ignore')
593 albumName = s['albumName'].encode('ascii', 'ignore')
594 artistName = s['artistName'].encode('ascii', 'ignore')
b738088f 595 if self.removeDuplicates == True:
596 if (entry[0].lower() == songName.lower()) and (entry[3].lower() == albumName.lower()) and (entry[6].lower() == artistName.lower()):
597 notIn = False
8817bb2e 598 if notIn == True:
599 list.append([s['songName'].encode('ascii', 'ignore'),\
600 s['songID'],\
601 dur,\
602 s['albumName'].encode('ascii', 'ignore'),\
603 s['albumID'],\
604 s['image']['tiny'].encode('ascii', 'ignore'),\
605 s['artistName'].encode('ascii', 'ignore'),\
606 s['artistID'],\
607 s['image']['small'].encode('ascii', 'ignore'),\
608 s['image']['medium'].encode('ascii', 'ignore')])
609 except:
610 print 'GrooveShark: Could not parse song number: ' + str(i)
611 traceback.print_exc()
612 i = i + 1
613 return list
614 else:
615 return []
616 pass
617 except:
618 print 'GrooveShark: Could not parse songs. Got this:'
619 traceback.print_exc()
620 return []
621
622 def parseArtists(self, items):
b738088f 623 try:
624 if 'result' in items:
625 i = 0
626 list = []
627 artists = items['result']['artists']
628 while(i < len(artists)):
629 s = artists[i]
630 try:
631 list.append([s['artistName'].encode('ascii', 'ignore'),\
632 s['artistID']])
633 except:
634 print 'GrooveShark: Could not parse album number: ' + str(i)
635 traceback.print_exc()
636 i = i + 1
637 return list
638 else:
639 return []
640 except:
641 print 'GrooveShark: Could not parse artists. Got this:'
642 traceback.print_exc()
8817bb2e 643 return []
644
645 def parseAlbums(self, items):
b738088f 646 try:
647 if 'result' in items:
648 i = 0
649 list = []
650 albums = items['result']['albums']
651 while(i < len(albums)):
652 s = albums[i]
653 try: # Avoid ascii ancoding errors
654 list.append([s['artistName'].encode('ascii', 'ignore'),\
655 s['artistID'],\
656 s['albumName'].encode('ascii', 'ignore'),\
657 s['albumID'],\
406ab447 658 s['image']['medium'].encode('ascii', 'ignore')])
b738088f 659 except:
660 print 'GrooveShark: Could not parse album number: ' + str(i)
661 traceback.print_exc()
662 i = i + 1
663 return list
664 else:
665 return []
666 except:
667 print 'GrooveShark: Could not parse albums. Got this'
668 traceback.print_exc()
8817bb2e 669 return []
670
671 def parsePlaylists(self, items):
b738088f 672 try:
673 if 'result' in items:
674 i = 0
675 list = []
676 playlists = items['result']['playlists']
677 while(i < len(playlists)):
678 s = playlists[i]
679 try: # Avoid ascii ancoding errors
680 list.append([s['playlistID'],\
681 s['playlistName'].encode('ascii', 'ignore'),\
682 s['username'].encode('ascii', 'ignore')])
683 except:
684 print 'GrooveShark: Could not parse playlist number: ' + str(i)
685 traceback.print_exc()
686 i = i + 1
687 return list
688 else:
689 return []
690 except:
691 print 'GrooveShark: Could not parse playlists. Got this:'
692 print items
8817bb2e 693 return []