Fixes.
[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
30 self.radioEnabled = 0
31 self.userId = 0
32 self.seedArtists = []
33 self.frowns = []
34 self.songIDsAlreadySeen = []
35 self.recentArtists = []
b738088f 36 self.removeDuplicates = False
37 self.dataDir = 'plugin_data/music/'
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:
155 return result['header']['sessionID']
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
287
288 def playlistGetSongs(self, playlistId, limit=25):
289 result = self.callRemote("playlist.getSongs", {"playlistID": playlistId})
290 list = self.parseSongs(result)
291 return list
292
293 def playlistDelete(self, playlistId):
294 if self.loggedIn == 1:
295 return self.callRemote("playlist.delete", {"playlistID": playlistId})
296
297 def playlistRename(self, playlistId, name):
298 if self.loggedIn == 1:
299 result = self.callRemote("playlist.rename", {"playlistID": playlistId, "name": name})
300 if 'fault' in result:
301 return 0
302 else:
303 return 1
304 else:
305 return 0
306
307 def playlistClearSongs(self, playlistId):
308 if self.loggedIn == 1:
309 return self.callRemote("playlist.clearSongs", {"playlistID": playlistId})
310
311 def playlistAddSong(self, playlistId, songId, position):
312 if self.loggedIn == 1:
313 result = self.callRemote("playlist.addSong", {"playlistID": playlistId, "songID": songId, "position": position})
314 if 'fault' in result:
315 return 0
316 else:
317 return 1
318 else:
319 return 0
320
321 def playlistReplace(self, playlistId, songIds):
322 if self.loggedIn == 1:
323 result = self.callRemote("playlist.replace", {"playlistID": playlistId, "songIDs": songIds})
324 if 'fault' in result:
325 return 0
326 else:
327 return 1
328 else:
329 return 0
330
331 def autoplayStartWithArtistIDs(self, artistIds):
332 result = self.callRemote("autoplay.startWithArtistIDs", {"artistIDs": artistIds})
333 if 'fault' in result:
334 self.radioEnabled = 0
335 return 0
336 else:
337 self.radioEnabled = 1
338 return 1
339
340 def autoplayStart(self, songIds):
341 result = self.callRemote("autoplay.start", {"songIDs": songIds})
342 if 'fault' in result:
343 self.radioEnabled = 0
344 return 0
345 else:
346 self.radioEnabled = 1
347 return 1
348
8817bb2e 349 def autoplayGetNextSongEx(self, seedArtists = [], frowns = [], songIDsAlreadySeen = [], recentArtists = []):
350 result = self.callRemote("autoplay.getNextSongEx", {"seedArtists": seedArtists, "frowns": frowns, "songIDsAlreadySeen": songIDsAlreadySeen, "recentArtists": recentArtists})
351 if 'fault' in result:
352 return []
353 else:
354 return result
355
356 def radioGetNextSong(self):
b738088f 357 radio = self.getSavedRadio()
358 if radio == None:
359 return None
8817bb2e 360 else:
b738088f 361 seedArtists = []
362 for song in radio['seedArtists']:
363 seedArtists.append(song[7])
364 result = self.autoplayGetNextSongEx(seedArtists, radio['frowns'], radio['songIDsAlreadySeen'], radio['recentArtists'])
8817bb2e 365 if 'fault' in result:
366 return []
367 else:
368 song = self.parseSongs(result)
b738088f 369 self.radioSetAlreadyListenedSong(songId = song[0][1])
8817bb2e 370 return song
371
372 def radioFrown(self, songId):
373 self.frown.append(songId)
374
375 def radioAlreadySeen(self, songId):
376 self.songIDsAlreadySeen.append(songId)
377
b738088f 378 def radioAddArtist(self, song = None, radioName = None):
379 radio = self.getSavedRadio(name = radioName)
380 if radio != None and song != None:
381 radio['seedArtists'].append(song)
382 return self.saveRadio(radio = radio)
383 else:
384 return 0
8817bb2e 385
386 def radioStart(self, artists = [], frowns = []):
387 for artist in artists:
388 self.seedArtists.append(artist)
389 for artist in frowns:
390 self.frowns.append(artist)
391 if self.autoplayStartWithArtistIDs(self.seedArtists) == 1:
392 self.radioEnabled = 1
393 return 1
394 else:
395 self.radioEnabled = 0
396 return 0
397
398 def radioStop(self):
399 self.seedArtists = []
400 self.frowns = []
401 self.songIDsAlreadySeen = []
402 self.recentArtists = []
403 self.radioEnabled = 0
404
405 def radioTurnedOn(self):
406 return self.radioEnabled
407
b738088f 408 def radioSetAlreadyListenedSong(self, name = None, songId = ''):
409 radio = self.getSavedRadio(name = name)
410 if radio != None and songId != '':
411 radio['songIDsAlreadySeen'].append(songId)
412 while len(radio['songIDsAlreadySeen']) > 20:
413 radio['songIDsAlreadySeen'].pop(0) # Trim
414 return self.saveRadio(radio = radio)
415 else:
416 return 0
417
418 def getSavedRadio(self, name = None):
419 if name == None:
420 path = os.path.join(self.confDir, 'radio', 'default.txt')
421 else:
422 path = os.path.join(self.confDir, 'radio', 'saved', name)
423 try:
424 f = open(path, 'rb')
425 radio = pickle.load(f)
426 f.close()
427 except:
428 radio = None
429 return radio
430
431 def saveRadio(self, name = None, radio = {}): #blaher
432 try:
433 dir = os.path.join(self.confDir, 'radio')
434 # Create the 'data' directory if it doesn't exist.
435 if not os.path.exists(dir):
436 os.mkdir(dir)
437 os.mkdir(os.path.join(dir, 'saved'))
438 if name == None:
439 path = os.path.join(dir, 'default.txt')
440 else:
441 path = os.path.join(dir, 'saved', name)
442 f = open(path, 'wb')
443 pickle.dump(radio, f, protocol=pickle.HIGHEST_PROTOCOL)
444 f.close()
445 return 1
446 except IOError, e:
447 print 'There was an error while saving the radio pickle (%s)' % e
448 return 0
449 except:
450 print "An unknown error occured during save radio: " + str(sys.exc_info()[0])
451 return 0
452
8817bb2e 453 def favoriteSong(self, songID):
454 return self.callRemote("song.favorite", {"songID": songID})
455
456 def unfavoriteSong(self, songID):
457 return self.callRemote("song.unfavorite", {"songID": songID})
458
459 def getMethods(self):
460 return self.callRemote("service.getMethods")
461
462 def searchSongsExactMatch(self, songName, artistName, albumName):
463 result = self.callRemote("search.songExactMatch", {"songName": songName, "artistName": artistName, "albumName": albumName})
464 list = self.parseSongs(result)
465 return list
466
467 def searchSongs(self, query, limit, page=0, sortKey=6):
468 result = self.callRemote("search.songs", {"query": query, "limit": limit, "page:": page, "streamableOnly": 1})
469 list = self.parseSongs(result)
470 return list
471 #return sorted(list, key=itemgetter(sortKey))
472
473 def searchArtists(self, query, limit, sortKey=0):
474 result = self.callRemote("search.artists", {"query": query, "limit": limit, "streamableOnly": 1})
475 list = self.parseArtists(result)
476 return list
477 #return sorted(list, key=itemgetter(sortKey))
478
479 def searchAlbums(self, query, limit, sortKey=2):
480 result = self.callRemote("search.albums", {"query": query, "limit": limit, "streamableOnly": 1})
481 list = self.parseAlbums(result)
482 return list
483 #return sorted(list, key=itemgetter(sortKey))
484
485 def searchPlaylists(self, query, limit):
486 result = self.callRemote("search.playlists", {"query": query, "limit": limit, "streamableOnly": 1})
487 list = self.parsePlaylists(result)
488 return list
489
490 def popularGetSongs(self, limit):
491 result = self.callRemote("popular.getSongs", {"limit": limit})
492 list = self.parseSongs(result)
493 return list
494
495 def popularGetArtists(self, limit):
496 result = self.callRemote("popular.getArtists", {"limit": limit})
497 list = self.parseArtists(result)
498 return list
499
500 def popularGetAlbums(self, limit):
501 result = self.callRemote("popular.getAlbums", {"limit": limit})
502 list = self.parseAlbums(result)
503 return list
504
b738088f 505 def artistAbout(self, artistId):
506 result = self.callRemote("artist.about", {"artistID": artistId})
507 return result
508
8817bb2e 509 def artistGetAlbums(self, artistId, limit, sortKey=2):
510 result = self.callRemote("artist.getAlbums", {"artistID": artistId, "limit": limit})
511 list = self.parseAlbums(result)
512 return list
513 #return sorted(list, key=itemgetter(sortKey))
514
515 def artistGetVerifiedAlbums(self, artistId, limit):
516 result = self.callRemote("artist.getVerifiedAlbums", {"artistID": artistId, "limit": limit})
517 list = self.parseSongs(result)
518 return list
519
520 def albumGetSongs(self, albumId, limit):
521 result = self.callRemote("album.getSongs", {"albumID": albumId, "limit": limit})
522 list = self.parseSongs(result)
523 return list
524
525 def songGetSimilar(self, songId, limit):
526 result = self.callRemote("song.getSimilar", {"songID": songId, "limit": limit})
527 list = self.parseSongs(result)
528 return list
529
b738088f 530 def artistGetSimilar(self, artistId, limit):
531 result = self.callRemote("artist.getSimilar", {"artistID": artistId, "limit": limit})
532 list = self.parseArtists(result)
533 return list
534
535 def songAbout(self, songId):
536 result = self.callRemote("song.about", {"songID": songId})
537 return result['result']['song']
538
539 def getVersion(self):
540 result = self.callRemote("service.getVersion", {})
541 return result
542
8817bb2e 543 def parseSongs(self, items):
544 try:
545 if 'result' in items:
546 i = 0
547 list = []
548 if 'songs' in items['result']:
549 l = len(items['result']['songs'])
550 index = 'songs'
551 elif 'song' in items['result']:
552 l = 1
553 index = 'song'
554 else:
555 l = 0
556 index = ''
557 while(i < l):
558 if index == 'songs':
559 s = items['result'][index][i]
560 else:
561 s = items['result'][index]
562 if 'estDurationSecs' in s:
563 dur = s['estDurationSecs']
564 else:
565 dur = 0
566 try:
567 notIn = True
568 for entry in list:
569 songName = s['songName'].encode('ascii', 'ignore')
570 albumName = s['albumName'].encode('ascii', 'ignore')
571 artistName = s['artistName'].encode('ascii', 'ignore')
b738088f 572 if self.removeDuplicates == True:
573 if (entry[0].lower() == songName.lower()) and (entry[3].lower() == albumName.lower()) and (entry[6].lower() == artistName.lower()):
574 notIn = False
8817bb2e 575 if notIn == True:
576 list.append([s['songName'].encode('ascii', 'ignore'),\
577 s['songID'],\
578 dur,\
579 s['albumName'].encode('ascii', 'ignore'),\
580 s['albumID'],\
581 s['image']['tiny'].encode('ascii', 'ignore'),\
582 s['artistName'].encode('ascii', 'ignore'),\
583 s['artistID'],\
584 s['image']['small'].encode('ascii', 'ignore'),\
585 s['image']['medium'].encode('ascii', 'ignore')])
586 except:
587 print 'GrooveShark: Could not parse song number: ' + str(i)
588 traceback.print_exc()
589 i = i + 1
590 return list
591 else:
592 return []
593 pass
594 except:
595 print 'GrooveShark: Could not parse songs. Got this:'
596 traceback.print_exc()
597 return []
598
599 def parseArtists(self, items):
b738088f 600 try:
601 if 'result' in items:
602 i = 0
603 list = []
604 artists = items['result']['artists']
605 while(i < len(artists)):
606 s = artists[i]
607 try:
608 list.append([s['artistName'].encode('ascii', 'ignore'),\
609 s['artistID']])
610 except:
611 print 'GrooveShark: Could not parse album number: ' + str(i)
612 traceback.print_exc()
613 i = i + 1
614 return list
615 else:
616 return []
617 except:
618 print 'GrooveShark: Could not parse artists. Got this:'
619 traceback.print_exc()
8817bb2e 620 return []
621
622 def parseAlbums(self, items):
b738088f 623 try:
624 if 'result' in items:
625 i = 0
626 list = []
627 albums = items['result']['albums']
628 while(i < len(albums)):
629 s = albums[i]
630 try: # Avoid ascii ancoding errors
631 list.append([s['artistName'].encode('ascii', 'ignore'),\
632 s['artistID'],\
633 s['albumName'].encode('ascii', 'ignore'),\
634 s['albumID'],\
635 s['image']['tiny'].encode('ascii', 'ignore')])
636 except:
637 print 'GrooveShark: Could not parse album number: ' + str(i)
638 traceback.print_exc()
639 i = i + 1
640 return list
641 else:
642 return []
643 except:
644 print 'GrooveShark: Could not parse albums. Got this'
645 traceback.print_exc()
8817bb2e 646 return []
647
648 def parsePlaylists(self, items):
b738088f 649 try:
650 if 'result' in items:
651 i = 0
652 list = []
653 playlists = items['result']['playlists']
654 while(i < len(playlists)):
655 s = playlists[i]
656 try: # Avoid ascii ancoding errors
657 list.append([s['playlistID'],\
658 s['playlistName'].encode('ascii', 'ignore'),\
659 s['username'].encode('ascii', 'ignore')])
660 except:
661 print 'GrooveShark: Could not parse playlist number: ' + str(i)
662 traceback.print_exc()
663 i = i + 1
664 return list
665 else:
666 return []
667 except:
668 print 'GrooveShark: Could not parse playlists. Got this:'
669 print items
8817bb2e 670 return []