Add XML encoding.
[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
ff38aa42 18import urllib2, pprint, os, pickle, tempfile, time, re, simplejson, base64, sys, socket, hashlib, getpass
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
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()
0e7dbdf7 69
f95afae7 70 # Call to API
71 def _callRemote(self, method, params):
c52e3923 72 try:
73 res = self._getRemote(method, params)
74 url = res['url']
75 postData = res['postData']
5b941088 76 except:
77 print "Failed to get request URL and post data"
78 return []
79 try:
c52e3923 80 req = urllib2.Request(url, postData)
81 response = urllib2.urlopen(req)
82 result = response.read()
9f9eadcd 83 if self._debugging:
5e72534b 84 print "Response..."
85 pprint.pprint(result)
c52e3923 86 response.close()
87 result = simplejson.loads(result)
88 return result
5b941088 89 except urllib2.HTTPError, e:
90 print "HTTP error " + e.code
91 except urllib2.URLError, e:
92 print "URL error " + e.reason
c52e3923 93 except:
5b941088 94 print "Request to Grooveshark API failed"
c52e3923 95 return []
96
5b941088 97
c52e3923 98 # Get the API call
99 def _getRemote(self, method, params = {}):
100 postData = { "method": method, "sessionid": self._sessionID, "parameters": params }
101 postData = simplejson.dumps(postData)
5b941088 102
103 cipher = Blowfish(self._key)
104 cipher.initCTR()
105 encryptedPostData = cipher.encryptCTR(postData)
106 encryptedPostData = base64.urlsafe_b64encode(encryptedPostData)
107 url = WEB_APP_URL + "?postData=" + encryptedPostData
c52e3923 108 req = urllib2.Request(url)
109 response = urllib2.urlopen(req)
110 result = response.read()
9f9eadcd 111 if self._debugging:
5e72534b 112 print "Request..."
113 pprint.pprint(result)
c52e3923 114 response.close()
115 try:
116 result = simplejson.loads(result)
117 return result
118 except:
119 return []
120
1413d357 121 # Get a session id
122 def _getSessionID(self):
123 params = {}
124 result = self._callRemote('startSession', params)
f95afae7 125 if 'result' in result:
5b941088 126 self._lastSessionTime = time.time()
f95afae7 127 return result['result']['sessionID']
128 else:
129 return ''
1413d357 130
44dcc6f4 131 def _getSavedSession(self):
b244bfd6 132 path = os.path.join(self.cacheDir, 'groovesharksession.dmp')
1413d357 133 try:
134 f = open(path, 'rb')
0e7dbdf7 135 session = pickle.load(f)
f95afae7 136 self._sessionID = session['sessionID']
137 self._lastSessionTime = session['lastSessionTime']
138 self._userID = session['userID']
139 self._ip = session['ip']
140 self._country = session['country']
1413d357 141 f.close()
142 except:
f95afae7 143 self._sessionID = ''
144 self._lastSessionTime = 0
145 self._userID = 0
146 self._ip = '0.0.0.0'
147 self._country = ''
1413d357 148 pass
1413d357 149
44dcc6f4 150 def _setSavedSession(self):
1413d357 151 try:
7ce01be6 152 # Create the directory if it doesn't exist.
153 if not os.path.exists(self.cacheDir):
154 os.makedirs(self.cacheDir)
b244bfd6 155 path = os.path.join(self.cacheDir, 'groovesharksession.dmp')
1413d357 156 f = open(path, 'wb')
f95afae7 157 session = { 'sessionID' : self._sessionID, 'lastSessionTime' : self._lastSessionTime, 'userID': self._userID, 'ip' : self._ip, 'country' : self._country }
0e7dbdf7 158 pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
1413d357 159 f.close()
160 except:
44dcc6f4 161 print "An error occurred during save session"
1413d357 162 pass
163
f95afae7 164 # Get IP
165 def _getIP(self):
166 try:
28ca437a 167 myip = urllib2.urlopen('http://ipecho.net/plain').read()
f95afae7 168 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 169 if self._debugging:
5e72534b 170 print "IP is " + myip
f95afae7 171 return myip
172 except:
173 return '0.0.0.0'
1413d357 174
f95afae7 175 # Get country
176 def _getCountry(self):
177 params = { 'ip' : self._ip }
178 response = self._callRemote("getCountry", params)
179 return response['result']
180
181 # Get userid from name
182 def _getUserIDFromUsername(self, username):
183 result = self._callRemote('getUserIDFromUsername', {'username' : username})
184 if 'result' in result and result['result']['UserID'] > 0:
185 return result['result']['UserID']
186 else:
187 return 0
188
1413d357 189 # Authenticates the user for current API session
f95afae7 190 def _authenticate(self, login, password):
54b424d2 191 md5pwd = hashlib.md5(password).hexdigest()
f95afae7 192 params = {'login': login, 'password': md5pwd}
193
194 result = self._callRemote('authenticate', params)
c52e3923 195 try:
196 uid = result['result']['UserID']
197 except:
198 uid = 0
f95afae7 199 if (uid > 0):
200 return uid
201 else:
202 return 0
203
204 # Check the service
205 def pingService(self,):
206 result = self._callRemote('pingService', {});
207 if 'result' in result and result['result'] != '':
208 return True
209 else:
210 return False
1413d357 211
212 # Login
213 def login(self, username, password):
f95afae7 214 if self._userID <= 0:
44dcc6f4 215 # Check cache
216 self._getSavedSession()
f95afae7 217 if self._userID <= 0:
218 self._userID = self._authenticate(username, password)
219 if self._userID > 0:
44dcc6f4 220 self._setSavedSession()
f95afae7 221 return self._userID
1413d357 222
223 # Logs the user out
224 def logout(self):
f95afae7 225 result = self._callRemote('logout', {'sessionID' : self._sessionID})
44dcc6f4 226 if 'result' in result and result['result']['success'] == True:
f95afae7 227 self._userID = 0
44dcc6f4 228 self._setSavedSession()
229 return True
230 return False
1413d357 231
f95afae7 232 # Gets a stream key and host to get song content
233 def getSubscriberStreamKey(self, songID):
234 params = { "songID": songID, "country": self._country }
235 response = self._callRemote("getSubscriberStreamKey", params)
236 try:
f95afae7 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:
72f3ef3c 315 info['CoverArtFilename'] = THUMB_URL+info['CoverArtFilename'].encode('utf8', 'ignore')
1413d357 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
3b4634df 400 itemList = []
f95afae7 401 artists = items['result']['artists']
402 while(i < len(artists)):
403 s = artists[i]
72f3ef3c 404 itemList.append([s['artistName'].encode('utf8', 'ignore'),\
f95afae7 405 s['artistID']])
406 i = i + 1
3b4634df 407 return itemList
f95afae7 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
9f9eadcd 433 def markStreamKeyOver30Secs(self, streamKey, streamServerID):
434 params = { "streamKey" : streamKey, "streamServerID" : streamServerID }
f95afae7 435 self._callRemote("markStreamKeyOver30Secs", params)
436
437 # Song complete
9f9eadcd 438 def markSongComplete(self, songid, streamKey, streamServerID):
439 params = { "songID" : songid, "streamKey" : streamKey, "streamServerID" : streamServerID }
f95afae7 440 self._callRemote("markSongComplete", params)
5e72534b 441
1413d357 442 # Extract song data
7ce01be6 443 def _parseSongs(self, items, limit=0):
1413d357 444 if 'result' in items:
445 i = 0
3b4634df 446 itemList = []
97289139 447 index = ''
448 l = -1
449 try:
450 if 'songs' in items['result'][0]:
451 l = len(items['result'][0]['songs'])
452 index = 'songs[]'
453 except: pass
454 try:
455 if l < 0 and 'songs' in items['result']:
456 l = len(items['result']['songs'])
457 index = 'songs'
458 except: pass
459 try:
460 if l < 0 and 'song' in items['result']:
461 l = 1
462 index = 'song'
463 except: pass
464 try:
465 if l < 0:
466 l = len(items['result'])
467 except: pass
468
7ce01be6 469 if limit > 0 and l > limit:
470 l = limit
1413d357 471 while(i < l):
97289139 472 if index == 'songs[]':
473 s = items['result'][0]['songs'][i]
474 elif index == 'songs':
1413d357 475 s = items['result'][index][i]
476 elif index == 'song':
477 s = items['result'][index]
478 else:
479 s = items['result'][i]
480 if 'CoverArtFilename' not in s:
f95afae7 481 info = self.getSongsInfo(s['SongID'])
1413d357 482 coverart = info['CoverArtFilename']
483 elif s['CoverArtFilename'] != None:
72f3ef3c 484 coverart = THUMB_URL+s['CoverArtFilename'].encode('utf8', 'ignore')
1413d357 485 else:
7ce01be6 486 coverart = 'None'
7b17af93 487 if 'Name' in s:
488 name = s['Name']
489 else:
490 name = s['SongName']
72f3ef3c 491 if 'AlbumName' in s:
492 albumName = s['AlbumName']
493 else:
494 albumName = ""
495 itemList.append([name.encode('utf8', 'ignore'),\
1413d357 496 s['SongID'],\
72f3ef3c 497 albumName.encode('utf8', 'ignore'),\
1413d357 498 s['AlbumID'],\
72f3ef3c 499 s['ArtistName'].encode('utf8', 'ignore'),\
1413d357 500 s['ArtistID'],\
501 coverart])
502 i = i + 1
3b4634df 503 return itemList
1413d357 504 else:
505 return []
506
507 # Extract artist data
508 def _parseArtists(self, items):
509 if 'result' in items:
510 i = 0
3b4634df 511 itemList = []
1413d357 512 artists = items['result']['artists']
513 while(i < len(artists)):
514 s = artists[i]
72f3ef3c 515 itemList.append([s['ArtistName'].encode('utf8', 'ignore'),\
1413d357 516 s['ArtistID']])
517 i = i + 1
3b4634df 518 return itemList
1413d357 519 else:
520 return []
521
522 # Extract album data
7ce01be6 523 def _parseAlbums(self, items, limit=0):
1413d357 524 if 'result' in items:
525 i = 0
3b4634df 526 itemList = []
7ce01be6 527 try:
528 albums = items['result']['albums']
529 except:
530 res = items['result'][0]
531 albums = res['albums']
532 l = len(albums)
533 if limit > 0 and l > limit:
534 l = limit
535 while(i < l):
1413d357 536 s = albums[i]
3b4634df 537 if 'Name' in s:
72f3ef3c 538 name = s['Name'].encode('utf8', 'ignore')
3b4634df 539 else:
72f3ef3c 540 name = s['AlbumName'].encode('utf8', 'ignore')
1413d357 541 if 'CoverArtFilename' in s and s['CoverArtFilename'] != None:
72f3ef3c 542 coverart = THUMB_URL+s['CoverArtFilename'].encode('utf8', 'ignore')
1413d357 543 else:
7ce01be6 544 coverart = 'None'
72f3ef3c 545 itemList.append([s['ArtistName'].encode('utf8', 'ignore'),\
1413d357 546 s['ArtistID'],\
3b4634df 547 name,\
1413d357 548 s['AlbumID'],\
549 coverart])
550 i = i + 1
3b4634df 551 return itemList
1413d357 552 else:
553 return []
554
555 def _parsePlaylists(self, items):
86f629ea 556 i = 0
3b4634df 557 itemList = []
1413d357 558 if 'result' in items:
f95afae7 559 playlists = items['result']['playlists']
86f629ea 560 elif len(items) > 0:
561 playlists = items
1413d357 562 else:
563 return []
f95afae7 564
86f629ea 565 while (i < len(playlists)):
566 s = playlists[i]
3b4634df 567 itemList.append([unicode(s['PlaylistName']).encode('utf8', 'ignore'), s['PlaylistID']])
86f629ea 568 i = i + 1
3b4634df 569 return itemList