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