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