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