Fixes and tidy up.
[clinton/xbmc-groove.git] / resources / lib / GroovesharkAPI.py
CommitLineData
2d388879 1# Copyright 2011 Stephen Denham
2
3# This file is part of xbmc-groove.
4#
5# xbmc-groove is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# xbmc-groove is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with xbmc-groove. If not, see <http://www.gnu.org/licenses/>.
17
db1b97c2 18import urllib2, pprint, md5, os, pickle, tempfile, time, re, simplejson, base64
5b941088 19from blowfish import Blowfish
1413d357 20
f95afae7 21SESSION_EXPIRY = 1209600 # 2 weeks
44dcc6f4 22
c52e3923 23# Web app
24WEB_APP_URL = "http://xbmc-groove.appspot.com/"
25
1413d357 26# GrooveAPI constants
6842a53b 27THUMB_URL = 'http://beta.grooveshark.com/static/amazonart/m'
1413d357 28SONG_LIMIT = 25
29ALBUM_LIMIT = 15
30ARTIST_LIMIT = 15
7ce01be6 31SONG_SUFFIX = '.mp3'
1413d357 32
1413d357 33# Main API
34class GrooveAPI:
35
f95afae7 36 _ip = '0.0.0.0'
37 _country = ''
38 _sessionID = ''
39 _userID = 0
40 _lastSessionTime = 0
41 _lastStreamKey = ''
42 _lastStreamServerID = ''
5b941088 43 _key = md5.new(os.path.basename("GroovesharkAPI.py")).hexdigest()
5e72534b 44 _apiDebug = False
1413d357 45
46 # Constructor
7ce01be6 47 def __init__(self):
48
1413d357 49 self.simplejson = simplejson
f95afae7 50 self.cacheDir = os.path.join(tempfile.gettempdir(), 'groovesharkapi')
7ce01be6 51 if os.path.isdir(self.cacheDir) == False:
52 os.makedirs(self.cacheDir)
5e72534b 53 if self._apiDebug:
54 print "Made " + self.cacheDir
44dcc6f4 55 self._getSavedSession()
f95afae7 56 # session ids last 2 weeks
57 if self._sessionID == '' or time.time()- self._lastSessionTime >= SESSION_EXPIRY:
58 self._sessionID = self._getSessionID()
f95afae7 59 if self._sessionID == '':
1413d357 60 raise StandardError('Failed to get session id')
61 else:
5e72534b 62 if self._apiDebug:
63 print "New GrooveAPI session id: " + self._sessionID
5b941088 64 self._ip = self._getIP()
65 self._country = self._getCountry()
44dcc6f4 66 self._setSavedSession()
0e7dbdf7 67
f95afae7 68 # Call to API
69 def _callRemote(self, method, params):
c52e3923 70 try:
71 res = self._getRemote(method, params)
72 url = res['url']
73 postData = res['postData']
5b941088 74 except:
75 print "Failed to get request URL and post data"
76 return []
77 try:
c52e3923 78 req = urllib2.Request(url, postData)
79 response = urllib2.urlopen(req)
80 result = response.read()
5e72534b 81 if self._apiDebug:
82 print "Response..."
83 pprint.pprint(result)
c52e3923 84 response.close()
85 result = simplejson.loads(result)
86 return result
5b941088 87 except urllib2.HTTPError, e:
88 print "HTTP error " + e.code
89 except urllib2.URLError, e:
90 print "URL error " + e.reason
c52e3923 91 except:
5b941088 92 print "Request to Grooveshark API failed"
c52e3923 93 return []
94
5b941088 95
c52e3923 96 # Get the API call
97 def _getRemote(self, method, params = {}):
98 postData = { "method": method, "sessionid": self._sessionID, "parameters": params }
99 postData = simplejson.dumps(postData)
5b941088 100
101 cipher = Blowfish(self._key)
102 cipher.initCTR()
103 encryptedPostData = cipher.encryptCTR(postData)
104 encryptedPostData = base64.urlsafe_b64encode(encryptedPostData)
105 url = WEB_APP_URL + "?postData=" + encryptedPostData
c52e3923 106 req = urllib2.Request(url)
107 response = urllib2.urlopen(req)
108 result = response.read()
5e72534b 109 if self._apiDebug:
110 print "Request..."
111 pprint.pprint(result)
c52e3923 112 response.close()
113 try:
114 result = simplejson.loads(result)
115 return result
116 except:
117 return []
118
1413d357 119 # Get a session id
120 def _getSessionID(self):
121 params = {}
122 result = self._callRemote('startSession', params)
f95afae7 123 if 'result' in result:
5b941088 124 self._lastSessionTime = time.time()
f95afae7 125 return result['result']['sessionID']
126 else:
127 return ''
1413d357 128
44dcc6f4 129 def _getSavedSession(self):
7ce01be6 130 path = os.path.join(self.cacheDir, 'session.dmp')
1413d357 131 try:
132 f = open(path, 'rb')
0e7dbdf7 133 session = pickle.load(f)
f95afae7 134 self._sessionID = session['sessionID']
135 self._lastSessionTime = session['lastSessionTime']
136 self._userID = session['userID']
137 self._ip = session['ip']
138 self._country = session['country']
1413d357 139 f.close()
140 except:
f95afae7 141 self._sessionID = ''
142 self._lastSessionTime = 0
143 self._userID = 0
144 self._ip = '0.0.0.0'
145 self._country = ''
1413d357 146 pass
1413d357 147
44dcc6f4 148 def _setSavedSession(self):
1413d357 149 try:
7ce01be6 150 # Create the directory if it doesn't exist.
151 if not os.path.exists(self.cacheDir):
152 os.makedirs(self.cacheDir)
153 path = os.path.join(self.cacheDir, 'session.dmp')
1413d357 154 f = open(path, 'wb')
f95afae7 155 session = { 'sessionID' : self._sessionID, 'lastSessionTime' : self._lastSessionTime, 'userID': self._userID, 'ip' : self._ip, 'country' : self._country }
0e7dbdf7 156 pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
1413d357 157 f.close()
158 except:
44dcc6f4 159 print "An error occurred during save session"
1413d357 160 pass
161
f95afae7 162 # Get IP
163 def _getIP(self):
164 try:
165 myip = urllib2.urlopen('http://whatismyip.org').read()
166 if re.match("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", myip):
5e72534b 167 if self._apiDebug:
168 print "IP is " + myip
f95afae7 169 return myip
170 except:
171 return '0.0.0.0'
1413d357 172
f95afae7 173 # Get country
174 def _getCountry(self):
175 params = { 'ip' : self._ip }
176 response = self._callRemote("getCountry", params)
177 return response['result']
178
179 # Get userid from name
180 def _getUserIDFromUsername(self, username):
181 result = self._callRemote('getUserIDFromUsername', {'username' : username})
182 if 'result' in result and result['result']['UserID'] > 0:
183 return result['result']['UserID']
184 else:
185 return 0
186
1413d357 187 # Authenticates the user for current API session
f95afae7 188 def _authenticate(self, login, password):
189 md5pwd = md5.new(password).hexdigest()
190 params = {'login': login, 'password': md5pwd}
191
192 result = self._callRemote('authenticate', params)
c52e3923 193 try:
194 uid = result['result']['UserID']
195 except:
196 uid = 0
f95afae7 197 if (uid > 0):
198 return uid
199 else:
200 return 0
201
202 # Check the service
203 def pingService(self,):
204 result = self._callRemote('pingService', {});
205 if 'result' in result and result['result'] != '':
206 return True
207 else:
208 return False
1413d357 209
210 # Login
211 def login(self, username, password):
f95afae7 212 if self._userID <= 0:
44dcc6f4 213 # Check cache
214 self._getSavedSession()
f95afae7 215 if self._userID <= 0:
216 self._userID = self._authenticate(username, password)
217 if self._userID > 0:
44dcc6f4 218 self._setSavedSession()
f95afae7 219 return self._userID
1413d357 220
221 # Logs the user out
222 def logout(self):
f95afae7 223 result = self._callRemote('logout', {'sessionID' : self._sessionID})
44dcc6f4 224 if 'result' in result and result['result']['success'] == True:
f95afae7 225 self._userID = 0
44dcc6f4 226 self._setSavedSession()
227 return True
228 return False
1413d357 229
f95afae7 230 # Gets a stream key and host to get song content
231 def getSubscriberStreamKey(self, songID):
232 params = { "songID": songID, "country": self._country }
233 response = self._callRemote("getSubscriberStreamKey", params)
234 try:
235 self._lastStreamKey = response["result"]["StreamKey"]
236 self._lastStreamServerID = response["result"]["StreamServerID"]
237 res = response["result"]
238 return res
239 except:
240 return False
241
1413d357 242 # Search for albums
243 def getArtistSearchResults(self, query, limit=ARTIST_LIMIT):
244 result = self._callRemote('getArtistSearchResults', {'query' : query,'limit' : limit})
245 if 'result' in result:
246 return self._parseArtists(result)
247 else:
248 return []
249
250 # Search for albums
251 def getAlbumSearchResults(self, query, limit=ALBUM_LIMIT):
252 result = self._callRemote('getAlbumSearchResults', {'query' : query,'limit' : limit})
253 if 'result' in result:
254 return self._parseAlbums(result)
255 else:
256 return []
257
258 # Search for songs
259 def getSongSearchResults(self, query, limit=SONG_LIMIT):
f95afae7 260 result = self._callRemote('getSongSearchResults', {'query' : query, 'country' : self._country, 'limit' : limit})
1413d357 261 if 'result' in result:
262 return self._parseSongs(result)
263 else:
264 return []
7ce01be6 265
266 # Get artists albums
267 def getArtistAlbums(self, artistID, limit=ALBUM_LIMIT):
5e72534b 268 result = self._callRemote('getArtistVerifiedAlbums', {'artistID' : artistID})
7ce01be6 269 if 'result' in result:
270 return self._parseAlbums(result, limit)
271 else:
272 return []
273
274 # Get album songs
275 def getAlbumSongs(self, albumID, limit=SONG_LIMIT):
f95afae7 276 result = self._callRemote('getAlbumSongs', {'albumID' : albumID, 'limit' : limit})
7ce01be6 277 if 'result' in result:
278 return self._parseSongs(result)
279 else:
280 return []
97289139 281
282 # Get artist's popular songs
283 def getArtistPopularSongs(self, artistID, limit = SONG_LIMIT):
284 result = self._callRemote('getArtistPopularSongs', {'artistID' : artistID})
285 if 'result' in result:
286 return self._parseSongs(result, limit)
287 else:
288 return []
289
1413d357 290 # Gets the popular songs
291 def getPopularSongsToday(self, limit=SONG_LIMIT):
292 result = self._callRemote('getPopularSongsToday', {'limit' : limit})
293 if 'result' in result:
7ce01be6 294 # Note limit is broken in the Grooveshark getPopularSongsToday method
295 return self._parseSongs(result, limit)
1413d357 296 else:
297 return []
298
299 # Gets the favorite songs of the logged-in user
300 def getUserFavoriteSongs(self):
f95afae7 301 if (self._userID == 0):
1413d357 302 return [];
f95afae7 303 result = self._callRemote('getUserFavoriteSongs', {})
1413d357 304 if 'result' in result:
305 return self._parseSongs(result)
306 else:
307 return []
7ce01be6 308
f95afae7 309 # Get song info
310 def getSongsInfo(self, songIDs):
311 result = self._callRemote('getSongsInfo', {'songIDs' : songIDs})
1413d357 312 if 'result' in result and 'SongID' in result['result']:
313 info = result['result']
314 if 'CoverArtFilename' in info and info['CoverArtFilename'] != None:
315 info['CoverArtFilename'] = THUMB_URL+info['CoverArtFilename'].encode('ascii', 'ignore')
316 else:
7ce01be6 317 info['CoverArtFilename'] = 'None'
1413d357 318 return info
319 else:
7ce01be6 320 return 'None'
f95afae7 321
322 # Add song to user favorites
323 def addUserFavoriteSong(self, songID):
324 if (self._userID == 0):
325 return False;
326 result = self._callRemote('addUserFavoriteSong', {'songID' : songID})
327 return result['result']['success']
328
329 # Remove songs from user favorites
330 def removeUserFavoriteSongs(self, songIDs):
331 if (self._userID == 0):
332 return False;
333 result = self._callRemote('removeUserFavoriteSongs', {'songIDs' : songIDs})
334 return result['result']['success']
1413d357 335
336 # Gets the playlists of the logged-in user
337 def getUserPlaylists(self):
f95afae7 338 if (self._userID == 0):
1413d357 339 return [];
f95afae7 340 result = self._callRemote('getUserPlaylists', {})
1413d357 341 if 'result' in result:
342 return self._parsePlaylists(result)
343 else:
344 return []
86f629ea 345
86f629ea 346 # Gets the playlists of the logged-in user
f95afae7 347 def getUserPlaylistsByUsername(self, username):
86f629ea 348 userID = self._getUserIDFromUsername(username)
349 if (userID > 0):
f95afae7 350 result = self._callRemote('getUserPlaylistsByUserID', {'userID' : userID})
86f629ea 351 if 'result' in result and result['result']['playlists'] != None:
352 playlists = result['result']['playlists']
353 return self._parsePlaylists(playlists)
354 else:
355 return []
1413d357 356
357 # Creates a playlist with songs
358 def createPlaylist(self, name, songIDs):
f95afae7 359 result = self._callRemote('createPlaylist', {'name' : name, 'songIDs' : songIDs})
40ac5d22 360 if 'result' in result and result['result']['success'] == True:
7ce01be6 361 return result['result']['playlistID']
1413d357 362 elif 'errors' in result:
363 return 0
364
365 # Sets the songs for a playlist
366 def setPlaylistSongs(self, playlistID, songIDs):
f95afae7 367 result = self._callRemote('setPlaylistSongs', {'playlistID' : playlistID, 'songIDs' : songIDs})
40ac5d22 368 if 'result' in result and result['result']['success'] == True:
7ce01be6 369 return True
1413d357 370 else:
7ce01be6 371 return False
1413d357 372
373 # Gets the songs of a playlist
374 def getPlaylistSongs(self, playlistID):
375 result = self._callRemote('getPlaylistSongs', {'playlistID' : playlistID});
376 if 'result' in result:
377 return self._parseSongs(result)
378 else:
379 return []
f95afae7 380
381
382 def playlistDelete(self, playlistId):
383 result = self._callRemote("deletePlaylist", {"playlistID": playlistId})
384 if 'fault' in result:
385 return 0
386 else:
387 return 1
7ce01be6 388
f95afae7 389 def playlistRename(self, playlistId, name):
390 result = self._callRemote("renamePlaylist", {"playlistID": playlistId, "name": name})
391 if 'fault' in result:
392 return 0
7ce01be6 393 else:
f95afae7 394 return 1
395
396 def getSimilarArtists(self, artistId, limit):
397 items = self._callRemote("getSimilarArtists", {"artistID": artistId, "limit": limit})
398 if 'result' in items:
399 i = 0
400 list = []
401 artists = items['result']['artists']
402 while(i < len(artists)):
403 s = artists[i]
404 list.append([s['artistName'].encode('ascii', 'ignore'),\
405 s['artistID']])
406 i = i + 1
407 return list
408 else:
409 return []
1413d357 410
5e72534b 411 def getDoesArtistExist(self, artistId):
412 response = self._callRemote("getDoesArtistExist", {"artistID": artistId})
413 if 'result' in response and response['result'] == True:
414 return True
415 else:
416 return False
417
418 def getDoesAlbumExist(self, albumId):
419 response = self._callRemote("getDoesAlbumExist", {"albumID": albumId})
420 if 'result' in response and response['result'] == True:
421 return True
422 else:
423 return False
424
425 def getDoesSongExist(self, songId):
426 response = self._callRemote("getDoesSongExist", {"songID": songId})
427 if 'result' in response and response['result'] == True:
428 return True
429 else:
430 return False
431
f95afae7 432 # After 30s play time
433 def markStreamKeyOver30Secs(self):
434 params = { "streamKey" : self._lastStreamKey, "streamServerID" : self._lastStreamServerID }
435 self._callRemote("markStreamKeyOver30Secs", params)
436
437 # Song complete
438 def markSongComplete(self, songid):
439 params = { "songID" : songid, "streamKey" : self._lastStreamKey, "streamServerID" : self._lastStreamServerID }
440 self._callRemote("markSongComplete", params)
441
5e72534b 442 # Debug on off
443 def setDebug(self, state):
444 self._apiDebug = state
445 if (self._apiDebug):
446 print "API debug is on"
447
448
1413d357 449 # Extract song data
7ce01be6 450 def _parseSongs(self, items, limit=0):
1413d357 451 if 'result' in items:
452 i = 0
453 list = []
97289139 454 index = ''
455 l = -1
456 try:
457 if 'songs' in items['result'][0]:
458 l = len(items['result'][0]['songs'])
459 index = 'songs[]'
460 except: pass
461 try:
462 if l < 0 and 'songs' in items['result']:
463 l = len(items['result']['songs'])
464 index = 'songs'
465 except: pass
466 try:
467 if l < 0 and 'song' in items['result']:
468 l = 1
469 index = 'song'
470 except: pass
471 try:
472 if l < 0:
473 l = len(items['result'])
474 except: pass
475
7ce01be6 476 if limit > 0 and l > limit:
477 l = limit
1413d357 478 while(i < l):
97289139 479 if index == 'songs[]':
480 s = items['result'][0]['songs'][i]
481 elif index == 'songs':
1413d357 482 s = items['result'][index][i]
483 elif index == 'song':
484 s = items['result'][index]
485 else:
486 s = items['result'][i]
487 if 'CoverArtFilename' not in s:
f95afae7 488 info = self.getSongsInfo(s['SongID'])
1413d357 489 coverart = info['CoverArtFilename']
490 elif s['CoverArtFilename'] != None:
491 coverart = THUMB_URL+s['CoverArtFilename'].encode('ascii', 'ignore')
492 else:
7ce01be6 493 coverart = 'None'
1413d357 494 list.append([s['SongName'].encode('ascii', 'ignore'),\
495 s['SongID'],\
496 s['AlbumName'].encode('ascii', 'ignore'),\
497 s['AlbumID'],\
498 s['ArtistName'].encode('ascii', 'ignore'),\
499 s['ArtistID'],\
500 coverart])
501 i = i + 1
502 return list
503 else:
504 return []
505
506 # Extract artist data
507 def _parseArtists(self, items):
508 if 'result' in items:
509 i = 0
510 list = []
511 artists = items['result']['artists']
512 while(i < len(artists)):
513 s = artists[i]
514 list.append([s['ArtistName'].encode('ascii', 'ignore'),\
515 s['ArtistID']])
516 i = i + 1
517 return list
518 else:
519 return []
520
521 # Extract album data
7ce01be6 522 def _parseAlbums(self, items, limit=0):
1413d357 523 if 'result' in items:
524 i = 0
525 list = []
7ce01be6 526 try:
527 albums = items['result']['albums']
528 except:
529 res = items['result'][0]
530 albums = res['albums']
531 l = len(albums)
532 if limit > 0 and l > limit:
533 l = limit
534 while(i < l):
1413d357 535 s = albums[i]
536 if 'CoverArtFilename' in s and s['CoverArtFilename'] != None:
537 coverart = THUMB_URL+s['CoverArtFilename'].encode('ascii', 'ignore')
538 else:
7ce01be6 539 coverart = 'None'
1413d357 540 list.append([s['ArtistName'].encode('ascii', 'ignore'),\
541 s['ArtistID'],\
542 s['AlbumName'].encode('ascii', 'ignore'),\
543 s['AlbumID'],\
544 coverart])
545 i = i + 1
546 return list
547 else:
548 return []
549
550 def _parsePlaylists(self, items):
86f629ea 551 i = 0
552 list = []
1413d357 553 if 'result' in items:
f95afae7 554 playlists = items['result']['playlists']
86f629ea 555 elif len(items) > 0:
556 playlists = items
1413d357 557 else:
558 return []
f95afae7 559
86f629ea 560 while (i < len(playlists)):
561 s = playlists[i]
f95afae7 562 list.append([str(s['PlaylistName']).encode('ascii', 'ignore'), s['PlaylistID']])
86f629ea 563 i = i + 1
564 return list