Some playlist updates.
[clinton/xbmc-groove.git] / resources / lib / GrooveAPI.py
CommitLineData
b738088f 1import urllib2, md5, unicodedata, re, os, traceback, sys, pickle, socket, xbmc
8817bb2e 2from operator import itemgetter, attrgetter
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):
148 response = urllib2.urlopen("http://www.moovida.com/services/grooveshark/session_start")
149 result = response.read()
150 result = self.simplejson.loads(result)
151 response.close()
152 if 'fault' in result:
153 return ''
154 else:
3fcef5ba 155 return result['result']['sessionID']
8817bb2e 156
157 def sessionDestroy(self):
158 return self.callRemote("session.destroy")
159
160 def getSessionFromAPI(self):
161 result = self.callRemote("session.get")
162 if 'fault' in result:
163 return ''
164 else:
165 return result['header']['sessionID']
166
167 def getStreamURL(self, songID):
168 result = self.callRemote("song.getStreamUrlEx", {"songID": songID})
169 if 'result' in result:
170 return result['result']['url']
171 else:
172 return ''
173
174 def createUserAuthToken(self, username, password):
175 hashpass = md5.new(password).hexdigest()
176 hashpass = username + hashpass
177 hashpass = md5.new(hashpass).hexdigest()
178 result = self.callRemote("session.createUserAuthToken", {"username": username, "hashpass": hashpass})
179 if 'result' in result:
180 return result['result']['token'], result['result']['userID']
181 elif 'fault' in result:
182 if result['fault']['code'] == 256:
183 return -1 # Exceeded the number of allowed tokens. Should not happen
184 else:
185 return -2 # Unknown error
186 else:
187 return -2 # Unknown error
188
189 def destroyUserAuthToken(self, token):
190 self.callRemote("session.destroyAuthToken", {"token": token})
191
192 def loginViaAuthToken(self, token):
193 result = self.callRemote("session.loginViaAuthToken", {"token": token})
194 self.destroyUserAuthToken(token)
195 if 'result' in result:
196 self.userID = result['result']['userID']
197 return result['result']['userID']
198 else:
199 return 0
200
201 def login(self, username, password):
202 if self.loggedIn == 1:
203 return self.userId
204 result = self.createUserAuthToken(username, password)
205 if result == -1:
206 raise LoginTokensExceededError()
207 elif result == -2:
208 raise LoginUnknownError()
209 else:
210 self.token = result[0]
211 self.debug('Token:' + self.token)
212 self.userId = self.loginViaAuthToken(self.token)
213 if self.userId == 0:
214 raise LoginUnknownError()
215 else:
216 self.loggedIn = 1
217 return self.userId
b738088f 218
8817bb2e 219
220 def loginExt(self, username, password):
221 if self.loggedIn == 1:
222 return self.userId
223 token = md5.new(username.lower() + md5.new(password).hexdigest()).hexdigest()
224 result = self.callRemote("session.loginExt", {"username": username, "token": token})
225 if 'result' in result:
226 if 'userID' in result['result']:
227 self.loggedIn = 1
228 self.userId = result['result']['userID']
229 return result['result']['userID']
230 else:
231 return 0
232
233
234 def loginBasic(self, username, password):
235 if self.loggedIn == 1:
236 return self.userId
237 result = self.callRemote("session.login", {"username": username, "password": password})
238 if 'result' in result:
239 if 'userID' in result['result']:
240 self.loggedIn = 1
241 self.userId = result['result']['userID']
242 return result['result']['userID']
243 else:
244 return 0
245
246 def loggedInStatus(self):
247 return self.loggedIn
248
249 def logout(self):
250 self.callRemote("session.logout", {})
251 self.loggedIn = 0
252
253 def getSongInfo(self, songID):
254 return self.callRemote("song.about", {"songID": songID})['result']['song']
255
256 def userGetFavoriteSongs(self, userID):
257 result = self.callRemote("user.getFavoriteSongs", {"userID": userID})
258 list = self.parseSongs(result)
259 return list
260
261 def userGetPlaylists(self, limit=25):
262 if self.loggedIn == 1:
263 result = self.callRemote("user.getPlaylists", {"userID": self.userId, "limit": limit})
264 if 'result' in result:
265 playlists = result['result']['playlists']
266 else:
267 return []
268 i = 0
269 list = []
270 while(i < len(playlists)):
271 p = playlists[i]
272 list.append([p['playlistName'].encode('ascii', 'ignore'), p['playlistID']])
273 i = i + 1
274 return sorted(list, key=itemgetter(0))
275 else:
276 return []
277
278 def playlistCreate(self, name, about):
279 if self.loggedIn == 1:
280 result = self.callRemote("playlist.create", {"name": name, "about": about})
281 if 'result' in result:
282 return result['result']['playlistID']
283 else:
284 return 0
285 else:
286 return 0
36cc00d7 287
288 def playlistCreateUnique(self, name, songIds):
289 if self.loggedIn == 1:
290 result = self.callRemote("playlist.createunique", {"name": name, "songIDs": songIds})
291 if 'result' in result:
292 return result['result']['playlistID']
293 else:
294 return 0
295 else:
296 return 0
8817bb2e 297
298 def playlistGetSongs(self, playlistId, limit=25):
299 result = self.callRemote("playlist.getSongs", {"playlistID": playlistId})
300 list = self.parseSongs(result)
301 return list
302
303 def playlistDelete(self, playlistId):
304 if self.loggedIn == 1:
305 return self.callRemote("playlist.delete", {"playlistID": playlistId})
306
307 def playlistRename(self, playlistId, name):
308 if self.loggedIn == 1:
309 result = self.callRemote("playlist.rename", {"playlistID": playlistId, "name": name})
310 if 'fault' in result:
311 return 0
312 else:
313 return 1
314 else:
315 return 0
316
317 def playlistClearSongs(self, playlistId):
318 if self.loggedIn == 1:
319 return self.callRemote("playlist.clearSongs", {"playlistID": playlistId})
320
321 def playlistAddSong(self, playlistId, songId, position):
322 if self.loggedIn == 1:
323 result = self.callRemote("playlist.addSong", {"playlistID": playlistId, "songID": songId, "position": position})
4be42357 324 if 'fault' in result:
325 return 0
326 else:
327 return 1
328 else:
329 return 0
330
331 def playlistDeleteSong(self, playlistId, position):
332 if self.loggedIn == 1:
333 result = self.callRemote("playlist.removeSong", {"playlistID": playlistId, "position": position})
8817bb2e 334 if 'fault' in result:
335 return 0
336 else:
337 return 1
338 else:
339 return 0
340
341 def playlistReplace(self, playlistId, songIds):
342 if self.loggedIn == 1:
343 result = self.callRemote("playlist.replace", {"playlistID": playlistId, "songIDs": songIds})
344 if 'fault' in result:
345 return 0
346 else:
347 return 1
348 else:
349 return 0
350
3fcef5ba 351 def radioStartArtists(self):
352 radio = self.getSavedRadio()
353 if radio == None:
354 return False
355 result = self.callRemote("autoplay.startWithArtistIDs", {"artistIDs": radio['seedArtists']})
8817bb2e 356 if 'fault' in result:
e6ccfeca 357 print "Cannot autoplay artists"
3fcef5ba 358 self.radioEnabled = False
8817bb2e 359 else:
3fcef5ba 360 self.radioEnabled = True
361 return self.radioEnabled
8817bb2e 362
3fcef5ba 363 def radioStartSongs(self):
364 radio = self.getSavedRadio()
365 if radio == None:
366 return False
367 result = self.callRemote("autoplay.start", {"songIDs": radio['seedSongs']})
8817bb2e 368 if 'fault' in result:
e6ccfeca 369 print "Cannot autoplay songs"
3fcef5ba 370 self.radioEnabled = False
8817bb2e 371 else:
3fcef5ba 372 self.radioEnabled = True
373 return self.radioEnabled
8817bb2e 374
3fcef5ba 375 def radioNextSong(self):
b738088f 376 radio = self.getSavedRadio()
377 if radio == None:
378 return None
8817bb2e 379 else:
3fcef5ba 380 result = self.callRemote("autoplay.getNextSongEx", {"seedArtists": radio['seedArtists'], "frowns": radio['frowns'], "songIDsAlreadySeen": self.radioRecentSongs, "recentArtists": self.radioRecentArtists})
8817bb2e 381 if 'fault' in result:
382 return []
383 else:
384 song = self.parseSongs(result)
3fcef5ba 385 self.radioRecentSongs.append(song[0][1])
386 self.radioRecentArtists.append(song[0][7])
8817bb2e 387 return song
388
3fcef5ba 389 def radioFrown(self, songId = None):
390 radio = self.getSavedRadio()
391 if radio != None and songId != None:
392 try:
393 radio['frowns'].remove(songId)
394 except: pass
395 radio['frowns'].append(songId)
b738088f 396 return self.saveRadio(radio = radio)
397 else:
3fcef5ba 398 return False
8817bb2e 399
3fcef5ba 400 def radioArtist(self, artistId = None):
401 radio = self.getSavedRadio()
402 if radio != None and artistId != None:
403 try:
404 radio['seedArtists'].remove(artistId)
405 except: pass
406 radio['seedArtists'].append(artistId)
e6ccfeca 407 print "Saved radio"
3fcef5ba 408 return self.saveRadio(radio = radio)
8817bb2e 409 else:
e6ccfeca 410 print "Failed to get radio"
3fcef5ba 411 return False
8817bb2e 412
3fcef5ba 413 def radioSong(self, songId = None):
414 radio = self.getSavedRadio()
415 if radio != None and songId != None:
416 try:
417 radio['seedSongs'].remove(songId)
418 except: pass
419 radio['seedSongs'].append(songId)
e6ccfeca 420 print "Saved radio"
3fcef5ba 421 return self.saveRadio(radio = radio)
422 else:
e6ccfeca 423 print "Failed to get radio"
3fcef5ba 424 return False
8817bb2e 425
426 def radioTurnedOn(self):
427 return self.radioEnabled
428
3fcef5ba 429 def getSavedRadio(self):
430 path = os.path.join(self.confDir, 'radio', 'radio.dmp')
b738088f 431 try:
432 f = open(path, 'rb')
433 radio = pickle.load(f)
434 f.close()
3fcef5ba 435 print radio
b738088f 436 except:
e6ccfeca 437 print "Failed to open " + path
3fcef5ba 438 radio = {}
439 radio['seedSongs'] = []
440 radio['seedArtists'] = []
441 radio['frowns'] = []
442 if self.saveRadio(radio) == False:
443 return None
b738088f 444 return radio
445
3fcef5ba 446 def saveRadio(self, radio): #blaher
447 if radio == {}:
448 print 'Invalid radio'
449 return False
b738088f 450 try:
451 dir = os.path.join(self.confDir, 'radio')
452 # Create the 'data' directory if it doesn't exist.
453 if not os.path.exists(dir):
454 os.mkdir(dir)
3fcef5ba 455 path = os.path.join(dir, 'radio.dmp')
b738088f 456 f = open(path, 'wb')
457 pickle.dump(radio, f, protocol=pickle.HIGHEST_PROTOCOL)
458 f.close()
3fcef5ba 459 return True
b738088f 460 except IOError, e:
461 print 'There was an error while saving the radio pickle (%s)' % e
3fcef5ba 462 return False
b738088f 463 except:
3fcef5ba 464 print "An unknown error occurred during save radio: " + str(sys.exc_info()[0])
465 return False
b738088f 466
8817bb2e 467 def favoriteSong(self, songID):
468 return self.callRemote("song.favorite", {"songID": songID})
469
470 def unfavoriteSong(self, songID):
471 return self.callRemote("song.unfavorite", {"songID": songID})
472
473 def getMethods(self):
474 return self.callRemote("service.getMethods")
475
476 def searchSongsExactMatch(self, songName, artistName, albumName):
477 result = self.callRemote("search.songExactMatch", {"songName": songName, "artistName": artistName, "albumName": albumName})
478 list = self.parseSongs(result)
479 return list
480
481 def searchSongs(self, query, limit, page=0, sortKey=6):
482 result = self.callRemote("search.songs", {"query": query, "limit": limit, "page:": page, "streamableOnly": 1})
483 list = self.parseSongs(result)
484 return list
485 #return sorted(list, key=itemgetter(sortKey))
486
487 def searchArtists(self, query, limit, sortKey=0):
488 result = self.callRemote("search.artists", {"query": query, "limit": limit, "streamableOnly": 1})
489 list = self.parseArtists(result)
490 return list
491 #return sorted(list, key=itemgetter(sortKey))
492
493 def searchAlbums(self, query, limit, sortKey=2):
494 result = self.callRemote("search.albums", {"query": query, "limit": limit, "streamableOnly": 1})
495 list = self.parseAlbums(result)
496 return list
497 #return sorted(list, key=itemgetter(sortKey))
498
499 def searchPlaylists(self, query, limit):
500 result = self.callRemote("search.playlists", {"query": query, "limit": limit, "streamableOnly": 1})
501 list = self.parsePlaylists(result)
502 return list
503
504 def popularGetSongs(self, limit):
505 result = self.callRemote("popular.getSongs", {"limit": limit})
506 list = self.parseSongs(result)
507 return list
508
509 def popularGetArtists(self, limit):
510 result = self.callRemote("popular.getArtists", {"limit": limit})
511 list = self.parseArtists(result)
512 return list
513
514 def popularGetAlbums(self, limit):
515 result = self.callRemote("popular.getAlbums", {"limit": limit})
516 list = self.parseAlbums(result)
517 return list
518
b738088f 519 def artistAbout(self, artistId):
520 result = self.callRemote("artist.about", {"artistID": artistId})
521 return result
522
8817bb2e 523 def artistGetAlbums(self, artistId, limit, sortKey=2):
524 result = self.callRemote("artist.getAlbums", {"artistID": artistId, "limit": limit})
525 list = self.parseAlbums(result)
526 return list
527 #return sorted(list, key=itemgetter(sortKey))
528
529 def artistGetVerifiedAlbums(self, artistId, limit):
530 result = self.callRemote("artist.getVerifiedAlbums", {"artistID": artistId, "limit": limit})
531 list = self.parseSongs(result)
532 return list
533
534 def albumGetSongs(self, albumId, limit):
535 result = self.callRemote("album.getSongs", {"albumID": albumId, "limit": limit})
536 list = self.parseSongs(result)
537 return list
538
539 def songGetSimilar(self, songId, limit):
540 result = self.callRemote("song.getSimilar", {"songID": songId, "limit": limit})
541 list = self.parseSongs(result)
542 return list
543
b738088f 544 def artistGetSimilar(self, artistId, limit):
545 result = self.callRemote("artist.getSimilar", {"artistID": artistId, "limit": limit})
546 list = self.parseArtists(result)
547 return list
548
549 def songAbout(self, songId):
550 result = self.callRemote("song.about", {"songID": songId})
551 return result['result']['song']
552
553 def getVersion(self):
554 result = self.callRemote("service.getVersion", {})
555 return result
556
8817bb2e 557 def parseSongs(self, items):
558 try:
559 if 'result' in items:
560 i = 0
561 list = []
562 if 'songs' in items['result']:
563 l = len(items['result']['songs'])
564 index = 'songs'
565 elif 'song' in items['result']:
566 l = 1
567 index = 'song'
568 else:
569 l = 0
570 index = ''
571 while(i < l):
572 if index == 'songs':
573 s = items['result'][index][i]
574 else:
575 s = items['result'][index]
576 if 'estDurationSecs' in s:
577 dur = s['estDurationSecs']
578 else:
579 dur = 0
580 try:
581 notIn = True
582 for entry in list:
583 songName = s['songName'].encode('ascii', 'ignore')
584 albumName = s['albumName'].encode('ascii', 'ignore')
585 artistName = s['artistName'].encode('ascii', 'ignore')
b738088f 586 if self.removeDuplicates == True:
587 if (entry[0].lower() == songName.lower()) and (entry[3].lower() == albumName.lower()) and (entry[6].lower() == artistName.lower()):
588 notIn = False
8817bb2e 589 if notIn == True:
590 list.append([s['songName'].encode('ascii', 'ignore'),\
591 s['songID'],\
592 dur,\
593 s['albumName'].encode('ascii', 'ignore'),\
594 s['albumID'],\
595 s['image']['tiny'].encode('ascii', 'ignore'),\
596 s['artistName'].encode('ascii', 'ignore'),\
597 s['artistID'],\
598 s['image']['small'].encode('ascii', 'ignore'),\
599 s['image']['medium'].encode('ascii', 'ignore')])
600 except:
601 print 'GrooveShark: Could not parse song number: ' + str(i)
602 traceback.print_exc()
603 i = i + 1
604 return list
605 else:
606 return []
607 pass
608 except:
609 print 'GrooveShark: Could not parse songs. Got this:'
610 traceback.print_exc()
611 return []
612
613 def parseArtists(self, items):
b738088f 614 try:
615 if 'result' in items:
616 i = 0
617 list = []
618 artists = items['result']['artists']
619 while(i < len(artists)):
620 s = artists[i]
621 try:
622 list.append([s['artistName'].encode('ascii', 'ignore'),\
623 s['artistID']])
624 except:
625 print 'GrooveShark: Could not parse album number: ' + str(i)
626 traceback.print_exc()
627 i = i + 1
628 return list
629 else:
630 return []
631 except:
632 print 'GrooveShark: Could not parse artists. Got this:'
633 traceback.print_exc()
8817bb2e 634 return []
635
636 def parseAlbums(self, items):
b738088f 637 try:
638 if 'result' in items:
639 i = 0
640 list = []
641 albums = items['result']['albums']
642 while(i < len(albums)):
643 s = albums[i]
644 try: # Avoid ascii ancoding errors
645 list.append([s['artistName'].encode('ascii', 'ignore'),\
646 s['artistID'],\
647 s['albumName'].encode('ascii', 'ignore'),\
648 s['albumID'],\
406ab447 649 s['image']['medium'].encode('ascii', 'ignore')])
b738088f 650 except:
651 print 'GrooveShark: Could not parse album number: ' + str(i)
652 traceback.print_exc()
653 i = i + 1
654 return list
655 else:
656 return []
657 except:
658 print 'GrooveShark: Could not parse albums. Got this'
659 traceback.print_exc()
8817bb2e 660 return []
661
662 def parsePlaylists(self, items):
b738088f 663 try:
664 if 'result' in items:
665 i = 0
666 list = []
667 playlists = items['result']['playlists']
668 while(i < len(playlists)):
669 s = playlists[i]
670 try: # Avoid ascii ancoding errors
671 list.append([s['playlistID'],\
672 s['playlistName'].encode('ascii', 'ignore'),\
673 s['username'].encode('ascii', 'ignore')])
674 except:
675 print 'GrooveShark: Could not parse playlist number: ' + str(i)
676 traceback.print_exc()
677 i = i + 1
678 return list
679 else:
680 return []
681 except:
682 print 'GrooveShark: Could not parse playlists. Got this:'
683 print items
8817bb2e 684 return []