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