Fixes.
[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 _key = md5.new(os.path.basename("GroovesharkAPI.py")).hexdigest()
42 _apiDebug = False
43
44 # Constructor
45 def __init__(self):
46
47 self.simplejson = simplejson
48 self.cacheDir = os.path.join(tempfile.gettempdir(), 'groovesharkapi')
49 if os.path.isdir(self.cacheDir) == False:
50 os.makedirs(self.cacheDir)
51 if self._apiDebug:
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 if self._apiDebug:
61 print "New GrooveAPI session id: " + self._sessionID
62 self._ip = self._getIP()
63 self._country = self._getCountry()
64 self._setSavedSession()
65
66 # Call to API
67 def _callRemote(self, method, params):
68 try:
69 res = self._getRemote(method, params)
70 url = res['url']
71 postData = res['postData']
72 except:
73 print "Failed to get request URL and post data"
74 return []
75 try:
76 req = urllib2.Request(url, postData)
77 response = urllib2.urlopen(req)
78 result = response.read()
79 if self._apiDebug:
80 print "Response..."
81 pprint.pprint(result)
82 response.close()
83 result = simplejson.loads(result)
84 return result
85 except urllib2.HTTPError, e:
86 print "HTTP error " + e.code
87 except urllib2.URLError, e:
88 print "URL error " + e.reason
89 except:
90 print "Request to Grooveshark API failed"
91 return []
92
93
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)
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
104 req = urllib2.Request(url)
105 response = urllib2.urlopen(req)
106 result = response.read()
107 if self._apiDebug:
108 print "Request..."
109 pprint.pprint(result)
110 response.close()
111 try:
112 result = simplejson.loads(result)
113 return result
114 except:
115 return []
116
117 # Get a session id
118 def _getSessionID(self):
119 params = {}
120 result = self._callRemote('startSession', params)
121 if 'result' in result:
122 self._lastSessionTime = time.time()
123 return result['result']['sessionID']
124 else:
125 return ''
126
127 def _getSavedSession(self):
128 path = os.path.join(self.cacheDir, 'session.dmp')
129 try:
130 f = open(path, 'rb')
131 session = pickle.load(f)
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']
137 f.close()
138 except:
139 self._sessionID = ''
140 self._lastSessionTime = 0
141 self._userID = 0
142 self._ip = '0.0.0.0'
143 self._country = ''
144 pass
145
146 def _setSavedSession(self):
147 try:
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')
152 f = open(path, 'wb')
153 session = { 'sessionID' : self._sessionID, 'lastSessionTime' : self._lastSessionTime, 'userID': self._userID, 'ip' : self._ip, 'country' : self._country }
154 pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
155 f.close()
156 except:
157 print "An error occurred during save session"
158 pass
159
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):
165 if self._apiDebug:
166 print "IP is " + myip
167 return myip
168 except:
169 return '0.0.0.0'
170
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
185 # Authenticates the user for current API session
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)
191 try:
192 uid = result['result']['UserID']
193 except:
194 uid = 0
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
207
208 # Login
209 def login(self, username, password):
210 if self._userID <= 0:
211 # Check cache
212 self._getSavedSession()
213 if self._userID <= 0:
214 self._userID = self._authenticate(username, password)
215 if self._userID > 0:
216 self._setSavedSession()
217 return self._userID
218
219 # Logs the user out
220 def logout(self):
221 result = self._callRemote('logout', {'sessionID' : self._sessionID})
222 if 'result' in result and result['result']['success'] == True:
223 self._userID = 0
224 self._setSavedSession()
225 return True
226 return False
227
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:
233 res = response["result"]
234 return res
235 except:
236 return False
237
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):
256 result = self._callRemote('getSongSearchResults', {'query' : query, 'country' : self._country, 'limit' : limit})
257 if 'result' in result:
258 return self._parseSongs(result)
259 else:
260 return []
261
262 # Get artists albums
263 def getArtistAlbums(self, artistID, limit=ALBUM_LIMIT):
264 result = self._callRemote('getArtistVerifiedAlbums', {'artistID' : artistID})
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):
272 result = self._callRemote('getAlbumSongs', {'albumID' : albumID, 'limit' : limit})
273 if 'result' in result:
274 return self._parseSongs(result)
275 else:
276 return []
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
286 # Gets the popular songs
287 def getPopularSongsToday(self, limit=SONG_LIMIT):
288 result = self._callRemote('getPopularSongsToday', {'limit' : limit})
289 if 'result' in result:
290 # Note limit is broken in the Grooveshark getPopularSongsToday method
291 return self._parseSongs(result, limit)
292 else:
293 return []
294
295 # Gets the favorite songs of the logged-in user
296 def getUserFavoriteSongs(self):
297 if (self._userID == 0):
298 return [];
299 result = self._callRemote('getUserFavoriteSongs', {})
300 if 'result' in result:
301 return self._parseSongs(result)
302 else:
303 return []
304
305 # Get song info
306 def getSongsInfo(self, songIDs):
307 result = self._callRemote('getSongsInfo', {'songIDs' : songIDs})
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:
313 info['CoverArtFilename'] = 'None'
314 return info
315 else:
316 return 'None'
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']
331
332 # Gets the playlists of the logged-in user
333 def getUserPlaylists(self):
334 if (self._userID == 0):
335 return [];
336 result = self._callRemote('getUserPlaylists', {})
337 if 'result' in result:
338 return self._parsePlaylists(result)
339 else:
340 return []
341
342 # Gets the playlists of the logged-in user
343 def getUserPlaylistsByUsername(self, username):
344 userID = self._getUserIDFromUsername(username)
345 if (userID > 0):
346 result = self._callRemote('getUserPlaylistsByUserID', {'userID' : userID})
347 if 'result' in result and result['result']['playlists'] != None:
348 playlists = result['result']['playlists']
349 return self._parsePlaylists(playlists)
350 else:
351 return []
352
353 # Creates a playlist with songs
354 def createPlaylist(self, name, songIDs):
355 result = self._callRemote('createPlaylist', {'name' : name, 'songIDs' : songIDs})
356 if 'result' in result and result['result']['success'] == True:
357 return result['result']['playlistID']
358 elif 'errors' in result:
359 return 0
360
361 # Sets the songs for a playlist
362 def setPlaylistSongs(self, playlistID, songIDs):
363 result = self._callRemote('setPlaylistSongs', {'playlistID' : playlistID, 'songIDs' : songIDs})
364 if 'result' in result and result['result']['success'] == True:
365 return True
366 else:
367 return False
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 []
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
384
385 def playlistRename(self, playlistId, name):
386 result = self._callRemote("renamePlaylist", {"playlistID": playlistId, "name": name})
387 if 'fault' in result:
388 return 0
389 else:
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 []
406
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
428 # After 30s play time
429 def markStreamKeyOver30Secs(self, lastStreamKey, lastStreamServerID):
430 params = { "streamKey" : lastStreamKey, "streamServerID" : lastStreamServerID }
431 self._callRemote("markStreamKeyOver30Secs", params)
432
433 # Song complete
434 def markSongComplete(self, songid, lastStreamKey, lastStreamServerID):
435 params = { "songID" : songid, "streamKey" : lastStreamKey, "streamServerID" : lastStreamServerID }
436 self._callRemote("markSongComplete", params)
437
438 # Debug on off
439 def setDebug(self, state):
440 if state == True:
441 self._apiDebug = True
442 else:
443 self._apiDebug = False
444 if self._apiDebug == True:
445 print "API debug is on"
446
447 # Extract song data
448 def _parseSongs(self, items, limit=0):
449 if 'result' in items:
450 i = 0
451 list = []
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
474 if limit > 0 and l > limit:
475 l = limit
476 while(i < l):
477 if index == 'songs[]':
478 s = items['result'][0]['songs'][i]
479 elif index == 'songs':
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:
486 info = self.getSongsInfo(s['SongID'])
487 coverart = info['CoverArtFilename']
488 elif s['CoverArtFilename'] != None:
489 coverart = THUMB_URL+s['CoverArtFilename'].encode('ascii', 'ignore')
490 else:
491 coverart = 'None'
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
520 def _parseAlbums(self, items, limit=0):
521 if 'result' in items:
522 i = 0
523 list = []
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):
533 s = albums[i]
534 if 'CoverArtFilename' in s and s['CoverArtFilename'] != None:
535 coverart = THUMB_URL+s['CoverArtFilename'].encode('ascii', 'ignore')
536 else:
537 coverart = 'None'
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):
549 i = 0
550 list = []
551 if 'result' in items:
552 playlists = items['result']['playlists']
553 elif len(items) > 0:
554 playlists = items
555 else:
556 return []
557
558 while (i < len(playlists)):
559 s = playlists[i]
560 list.append([str(s['PlaylistName']).encode('ascii', 'ignore'), s['PlaylistID']])
561 i = i + 1
562 return list