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