fix bluescreen with getTrack of Playlist
[vuplus_dvbapp-plugin] / lastfm / src / LastFM.py
1 from urllib import unquote_plus
2 from twisted.web.client import getPage\r
3 from md5 import md5 # to encode password\r
4 from string import split, rstrip
5
6 from xml.dom.minidom import parseString
7 \r
8 \r
9 \r
10 class LastFMEventRegister:\r
11     def __init__(self):\r
12         self.onMetadataChangedList = []\r
13     \r
14     def addOnMetadataChanged(self,callback):\r
15         self.onMetadataChangedList.append(callback)\r
16 \r
17     def removeOnMetadataChanged(self,callback):\r
18         self.onMetadataChangedList.remove(callback)\r
19     \r
20     def onMetadataChanged(self,metad):\r
21         for i in self.onMetadataChangedList:\r
22             i(metadata=metad)\r
23 \r
24 lastfm_event_register = LastFMEventRegister()\r
25             \r
26 class LastFMHandler:\r
27     def __init__(self):\r
28         pass\r
29     def onPlaylistLoaded(self,reason):
30         pass
31     def onConnectSuccessful(self,reason):
32         pass
33     def onConnectFailed(self,reason):\r
34         pass\r
35     def onCommandFailed(self,reason):\r
36         pass\r
37     def onTrackSkiped(self,reason):\r
38         pass\r
39     def onTrackLoved(self,reason):\r
40         pass\r
41     def onTrackBanned(self,reason):\r
42         pass\r
43     def onGlobalTagsLoaded(self,tags):\r
44         pass\r
45     def onTopTracksLoaded(self,tracks):\r
46         pass\r
47     def onRecentTracksLoaded(self,tracks):\r
48         pass\r
49     def onRecentBannedTracksLoaded(self,tracks):\r
50         pass\r
51     def onRecentLovedTracksLoaded(self,tracks):\r
52         pass\r
53     def onNeighboursLoaded(self,user):\r
54         pass\r
55     def onFriendsLoaded(self,user):\r
56         pass\r
57     def onStationChanged(self,reason):\r
58         pass    \r
59     def onMetadataLoaded(self,metadata):\r
60         pass\r
61 \r
62 class LastFM(LastFMHandler):\r
63     DEFAULT_NAMESPACES = (\r
64         None, # RSS 0.91, 0.92, 0.93, 0.94, 2.0\r
65         'http://purl.org/rss/1.0/', # RSS 1.0\r
66         'http://my.netscape.com/rdf/simple/0.9/' # RSS 0.90\r
67     )\r
68     DUBLIN_CORE = ('http://purl.org/dc/elements/1.1/',)\r
69     \r
70     version = "1.0.1"\r
71     platform = "linux"\r
72     host = "ws.audioscrobbler.com"\r
73     port = 80\r
74     metadata = {}\r
75     info={}\r
76     cache_toptags= "/tmp/toptags"\r
77     playlist = None
78     \r
79     def __init__(self):\r
80         LastFMHandler.__init__(self)\r
81         self.state = False # if logged in\r
82                     \r
83     def connect(self,username,password):\r
84 #        getPage(self.host,self.port\r
85 #                            ,"/radio/handshake.php?version=" + self.version + "&platform=" + self.platform + "&username=" + username + "&passwordmd5=" + self.hexify(md5(password).digest())\r
86 #                            ,callback=self.connectCB,errorback=self.onConnectFailed)\r
87         url = "http://"+self.host+":"+str(self.port)+"/radio/handshake.php?version=" + self.version + "&platform=" + self.platform + "&username=" + username + "&passwordmd5=" + self.hexify(md5(password).digest())
88         getPage(url).addCallback(self.connectCB).addErrback(self.onConnectFailed)\r
89
90     def connectCB(self,data):\r
91         self.info = self._parselines(data)\r
92         if self.info.has_key("session"):\r
93             self.lastfmsession = self.info["session"]\r
94             if self.lastfmsession.startswith("FAILED"):\r
95                 self.onConnectFailed(self.info["msg"])\r
96             else:\r
97                 self.streamurl = self.info["stream_url"]\r
98                 self.baseurl = self.info["base_url"]\r
99                 self.basepath = self.info["base_path"]\r
100                 self.subscriber = self.info["subscriber"]\r
101                 self.framehack = self.info["base_path"]\r
102                 self.state = True\r
103                 self.onConnectSuccessful("loggedin")
104                 \r
105         else:\r
106             self.onConnectFailed("login failed")\r
107         \r
108     def _parselines(self, str):\r
109         res = {}\r
110         vars = split(str, "\n")\r
111         for v in vars:\r
112             x = split(rstrip(v), "=", 1)\r
113             if len(x) == 2:
114                 try:\r
115                     res[x[0]] = x[1].encode("utf-8")
116                 except UnicodeDecodeError:
117                     res[x[0]] = "unicodeproblem"\r
118             elif x != [""]:\r
119                 print "(urk?", x, ")"\r
120         return res\r
121
122     def loadPlaylist(self):
123         print "LOADING PLAYLIST"
124         if self.state is not True:
125             self.onCommandFailed("not logged in")
126         else:
127 #            getPage(self.info["base_url"],80
128 #                            ,self.info["base_path"] + "/xspf.php?sk=" + self.info["session"]+"&discovery=0&desktop=1.3.1.1"
129 #                            ,callback=self.loadPlaylistCB,errorback=self.onCommandFailed)
130             url = "http://"+self.info["base_url"]+":80"+self.info["base_path"] + "/xspf.php?sk=" + self.info["session"]+"&discovery=0&desktop=1.3.1.1"
131             getPage(url).addCallback(self.loadPlaylistCB).addErrback(self.onCommandFailed)
132
133     def loadPlaylistCB(self,xmlsource):
134         self.playlist = LastFMPlaylist(xmlsource)
135         self.onPlaylistLoaded("playlist loaded")
136     \r
137     def getPersonalURL(self,username,level=50):\r
138         return "lastfm://user/%s/recommended/32"%username\r
139     \r
140     def getNeighboursURL(self,username):\r
141         return "lastfm://user/%s/neighbours"%username\r
142 \r
143     def getLovedURL(self,username):\r
144         return "lastfm://user/%s/loved"%username\r
145     \r
146     def getSimilarArtistsURL(self,artist=None):\r
147         if artist is None and self.metadata.has_key('artist'):\r
148             return "lastfm://artist/%s/similarartists"%self.metadata['artist'].replace(" ","%20")\r
149         else:\r
150             return "lastfm://artist/%s/similarartists"%artist.replace(" ","%20")\r
151 \r
152     def getArtistsLikedByFans(self,artist=None):\r
153         if artist is None and self.metadata.has_key('artist'):\r
154             return "lastfm://artist/%s/fans"%self.metadata['artist'].replace(" ","%20")\r
155         else:\r
156             return "lastfm://artist/%s/fans"%artist.replace(" ","%20")\r
157     \r
158     def getArtistGroup(self,artist=None):\r
159         if artist is None and self.metadata.has_key('artist'):\r
160             return "lastfm://group/%s"%self.metadata['artist'].replace(" ","%20")\r
161         else:\r
162             return "lastfm://group/%s"%artist.replace(" ","%20")\r
163     def command(self, cmd,callback):\r
164         # commands = skip, love, ban, rtp, nortp\r
165         if self.state is not True:\r
166             self.onCommandFailed("not logged in")\r
167         else:\r
168 #            getPage(self.info["base_url"],80\r
169 #                            ,self.info["base_path"] + "/control.php?command=" + cmd + "&session=" + self.info["session"]\r
170 #                            ,callback=callback,errorback=self.onCommandFailed)\r
171             url = "http://"+self.info["base_url"]+":80"+self.info["base_path"] + "/control.php?command=" + cmd + "&session=" + self.info["session"]
172             getPage(url).addCallback(callback).addErrback(self.onCommandFailed)\r
173  
174     def onTrackLovedCB(self,response):\r
175         res = self._parselines(response)\r
176         if res["response"] == "OK":\r
177             self.onTrackLoved("Track loved")\r
178         else:\r
179             self.onCommandFailed("Server returned FALSE")\r
180 \r
181     def onTrackBannedCB(self,response):\r
182         res = self._parselines(response)\r
183         if res["response"] == "OK":\r
184             self.onTrackBanned("Track baned")\r
185         else:\r
186             self.onCommandFailed("Server returned FALSE")\r
187 \r
188     def onTrackSkipedCB(self,response):\r
189         res = self._parselines(response)\r
190         if res["response"] == "OK":\r
191             self.onTrackSkiped("Track skiped")\r
192         else:\r
193             self.onCommandFailed("Server returned FALSE")\r
194                         \r
195     def love(self):\r
196         return self.command("love",self.onTrackLovedCB)\r
197 \r
198     def ban(self):\r
199         return self.command("ban",self.onTrackBannedCB)\r
200 \r
201     def skip(self):
202         """unneeded"""\r
203         return self.command("skip",self.onTrackSkipedCB)\r
204     \r
205     def hexify(self,s):\r
206         result = ""\r
207         for c in s:\r
208             result = result + ("%02x" % ord(c))\r
209         return result\r
210     \r
211 \r
212     def XMLgetElementsByTagName( self, node, tagName, possibleNamespaces=DEFAULT_NAMESPACES ):\r
213         for namespace in possibleNamespaces:\r
214             children = node.getElementsByTagNameNS(namespace, tagName)\r
215             if len(children): return children\r
216         return []\r
217 \r
218     def XMLnode_data( self, node, tagName, possibleNamespaces=DEFAULT_NAMESPACES):\r
219         children = self.XMLgetElementsByTagName(node, tagName, possibleNamespaces)\r
220         node = len(children) and children[0] or None\r
221         return node and "".join([child.data.encode("utf-8") for child in node.childNodes]) or None\r
222 \r
223     def XMLget_txt( self, node, tagName, default_txt="" ):\r
224         return self.XMLnode_data( node, tagName ) or self.XMLnode_data( node, tagName, self.DUBLIN_CORE ) or default_txt\r
225 \r
226     def getGlobalTags( self ,force_reload=False):\r
227         if self.state is not True:\r
228             self.onCommandFailed("not logged in")\r
229         else:\r
230 #            getPage(self.info["base_url"],80\r
231 #                            ,"/1.0/tag/toptags.xml"\r
232 #                            ,callback=self.getGlobalTagsCB,errorback=self.onCommandFailed)\r
233             url = "http://"+self.info["base_url"]+":80"+"/1.0/tag/toptags.xml"
234             getPage(url).addCallback(self.getGlobalTagsCB).addErrback(self.onCommandFailed)
235 \r
236     def getGlobalTagsCB(self,result):\r
237         try:\r
238             rssDocument = parseString(result)\r
239             data =[]\r
240             for node in self.XMLgetElementsByTagName(rssDocument, 'tag'):\r
241                 nodex={}\r
242                 nodex['_display'] = nodex['name'] = node.getAttribute("name").encode("utf-8")\r
243                 nodex['count'] =  node.getAttribute("count").encode("utf-8")\r
244                 nodex['stationurl'] = "lastfm://globaltags/"+node.getAttribute("name").encode("utf-8").replace(" ","%20")\r
245                 nodex['url'] =  node.getAttribute("url").encode("utf-8")\r
246                 data.append(nodex)\r
247             self.onGlobalTagsLoaded(data)\r
248         except xml.parsers.expat.ExpatError,e:\r
249             self.onCommandFailed(e)\r
250 \r
251     def getTopTracks(self,username):\r
252         if self.state is not True:\r
253             self.onCommandFailed("not logged in")\r
254         else:\r
255 #            getPage(self.info["base_url"],80\r
256 #                            ,"/1.0/user/%s/toptracks.xml"%username\r
257 #                            ,callback=self.getTopTracksCB,errorback=self.onCommandFailed)\r
258             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/toptracks.xml"
259             getPage(url).addCallback(self.getTopTracksCB).addErrback(self.onCommandFailed)
260            \r
261     def getTopTracksCB(self,result):\r
262         re,rdata = self._parseTracks(result)\r
263         if re:\r
264             self.onTopTracksLoaded(rdata)\r
265         else:\r
266             self.onCommandFailed(rdata)\r
267             \r
268     def getRecentTracks(self,username):\r
269         if self.state is not True:\r
270             self.onCommandFailed("not logged in")\r
271         else:\r
272 #            getPage(self.info["base_url"],80\r
273 #                            ,"/1.0/user/%s/recenttracks.xml"%username\r
274 #                            ,callback=self.getRecentTracksCB,errorback=self.onCommandFailed)\r
275             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/recenttracks.xml"
276             getPage(url).addCallback(self.getRecentTracksCB).addErrback(self.onCommandFailed)
277            \r
278     def getRecentTracksCB(self,result):\r
279         re,rdata = self._parseTracks(result)\r
280         if re:\r
281             self.onRecentTracksLoaded(rdata)\r
282         else:\r
283             self.onCommandFailed(rdata)\r
284     \r
285     def getRecentLovedTracks(self,username):\r
286         if self.state is not True:\r
287             self.onCommandFailed("not logged in")\r
288         else:\r
289 #            getPage(self.info["base_url"],80\r
290 #                            ,"/1.0/user/%s/recentlovedtracks.xml"%username\r
291 #                            ,callback=self.getRecentLovedTracksCB,errorback=self.onCommandFailed)\r
292             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/recentlovedtracks.xml"
293             getPage(url).addCallback(self.getRecentLovedTracksCB).addErrback(self.onCommandFailed)
294            \r
295     def getRecentLovedTracksCB(self,result):\r
296         re,rdata = self._parseTracks(result)\r
297         if re:\r
298             self.onRecentLovedTracksLoaded(rdata)\r
299         else:\r
300             self.onCommandFailed(rdata)\r
301 \r
302     def getRecentBannedTracks(self,username):\r
303         if self.state is not True:\r
304             self.onCommandFailed("not logged in")\r
305         else:\r
306 #            getPage(self.info["base_url"],80\r
307 #                            ,"/1.0/user/%s/recentbannedtracks.xml"%username\r
308 #                            ,callback=self.getRecentBannedTracksCB,errorback=self.onCommandFailed)\r
309             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/recentbannedtracks.xml"
310             getPage(url).addCallback(self.getRecentBannedTracksCB).addErrback(self.onCommandFailed)
311            \r
312     def getRecentBannedTracksCB(self,result):\r
313         re,rdata = self._parseTracks(result)\r
314         if re:\r
315             self.onRecentBannedTracksLoaded(rdata)\r
316         else:\r
317             self.onCommandFailed(rdata)\r
318 \r
319     def _parseTracks(self,xmlrawdata):\r
320         #print xmlrawdata\r
321         try:\r
322             rssDocument = parseString(xmlrawdata)\r
323             data =[]\r
324             for node in self.XMLgetElementsByTagName(rssDocument, 'track'):\r
325                 nodex={}\r
326                 nodex['name'] = self.XMLget_txt(node, "name", "N/A" )\r
327                 nodex['artist'] =  self.XMLget_txt(node, "artist", "N/A" )\r
328                 nodex['playcount'] = self.XMLget_txt(node, "playcount", "N/A" )\r
329                 nodex['stationurl'] =  "lastfm://artist/"+nodex['artist'].replace(" ","%20")+"/similarartists"#+nodex['name'].replace(" ","%20")\r
330                 nodex['url'] =  self.XMLget_txt(node, "url", "N/A" )\r
331                 nodex['_display'] = nodex['artist']+" - "+nodex['name']\r
332                 data.append(nodex)\r
333             return True,data\r
334         except xml.parsers.expat.ExpatError,e:\r
335             print e\r
336             return False,e\r
337 \r
338     def getNeighbours(self,username):\r
339         if self.state is not True:\r
340             self.onCommandFailed("not logged in")\r
341         else:\r
342 #            getPage(self.info["base_url"],80\r
343 #                            ,"/1.0/user/%s/neighbours.xml"%username\r
344 #                            ,callback=self.getNeighboursCB,errorback=self.onCommandFailed)\r
345             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/neighbours.xml"
346             getPage(url).addCallback(self.getNeighboursCB).addErrback(self.onCommandFailed)
347            \r
348     def getNeighboursCB(self,result):\r
349         re,rdata = self._parseUser(result)\r
350         if re:\r
351             self.onNeighboursLoaded(rdata)\r
352         else:\r
353             self.onCommandFailed(rdata)\r
354 \r
355     def getFriends(self,username):\r
356         if self.state is not True:\r
357             self.onCommandFailed("not logged in")\r
358         else:\r
359 #            getPage(self.info["base_url"],80\r
360 #                            ,"/1.0/user/%s/friends.xml"%username\r
361 #                            ,callback=self.getFriendsCB,errorback=self.onCommandFailed)\r
362             url = "http://"+self.info["base_url"]+"/1.0/user/"+username+"/friends.xml"
363             getPage(url).addCallback(self.getFriendsCB).addErrback(self.onCommandFailed)
364            \r
365     def getFriendsCB(self,result):\r
366         re,rdata = self._parseUser(result)\r
367         if re:\r
368             self.onFriendsLoaded(rdata)\r
369         else:\r
370             self.onCommandFailed(rdata)\r
371 \r
372 \r
373     def _parseUser(self,xmlrawdata):\r
374         #print xmlrawdata\r
375         try:\r
376             rssDocument = parseString(xmlrawdata)\r
377             data =[]\r
378             for node in self.XMLgetElementsByTagName(rssDocument, 'user'):\r
379                 nodex={}\r
380                 nodex['name'] = node.getAttribute("username").encode("utf-8")\r
381                 nodex['url'] =  self.XMLget_txt(node, "url", "N/A" )\r
382                 nodex['stationurl'] =  "lastfm://user/"+nodex['name']+"/personal"\r
383                 nodex['_display'] = nodex['name']\r
384                 data.append(nodex)\r
385             return True,data\r
386         except xml.parsers.expat.ExpatError,e:\r
387             print e\r
388             return False,e\r
389 \r
390     def changeStation(self,url):\r
391         if self.state is not True:\r
392             self.onCommandFailed("not logged in")\r
393         else:\r
394 #            getPage(self.info["base_url"],80\r
395 #                            ,self.info["base_path"] + "/adjust.php?session=" + self.info["session"] + "&url=" + url\r
396 #                            ,callback=self.changeStationCB,errorback=self.onCommandFailed)\r
397             url = "http://"+self.info["base_url"]+":80"+self.info["base_path"] + "/adjust.php?session=" + self.info["session"] + "&url=" + url
398             getPage(url).addCallback(self.changeStationCB).addErrback(self.onCommandFailed)
399            \r
400     def changeStationCB(self,result):\r
401         res = self._parselines(result)\r
402         if res["response"] == "OK":\r
403             self.onStationChanged("Station changed")\r
404         else:\r
405             self.onCommandFailed("Server returned "+res["response"])\r
406 \r
407 ############
408 class LastFMPlaylist:
409     """
410         this is the new way last.fm handles streams with metadata
411     """
412     DEFAULT_NAMESPACES = (None,)
413     DUBLIN_CORE = ('http://purl.org/dc/elements/1.1/',) #why do i need this?
414     
415     name = "N/A"
416     creator = "N/A"
417     tracks = []
418     length = 0
419     
420     def __init__(self,xmlsource):
421         self.xmldoc = parseString(xmlsource)
422         self.name = unquote_plus(self._get_txt( self.xmldoc, "title", "no playlistname" ))
423         self.creator =self._get_txt( self.xmldoc, "creator", "no playlistcreator" )
424         self.parseTracks()
425
426     def getTracks(self):
427         return self.tracks
428
429     def getTrack(self,tracknumber):
430         try:
431             return self.tracks[tracknumber]
432         except IndexError:
433             return False
434     
435     def parseTracks(self):
436         try:
437             self.tracks = []
438             for node in self._getElementsByTagName(self.xmldoc, 'track'):
439                 nodex={}
440                 nodex['station'] =  self.name
441                 nodex['location'] =  self._get_txt( node, "location", "no location" )
442                 nodex['title'] =  self._get_txt( node, "title", "no title" )
443                 nodex['id'] =  self._get_txt( node, "id", "no id" )
444                 nodex['album'] =  self._get_txt( node, "album", "no album" )
445                 nodex['creator'] =  self._get_txt( node, "creator", "no creator" )
446                 nodex['duration'] =  int(self._get_txt( node, "duration", "0" ))
447                 nodex['image'] =  self._get_txt( node, "image", "no image" )
448                 self.tracks.append(nodex)
449             self.length = len(self.tracks)
450             return True
451         except:
452             return False
453     
454     def _getElementsByTagName( self, node, tagName, possibleNamespaces=DEFAULT_NAMESPACES ):
455         for namespace in possibleNamespaces:
456             children = node.getElementsByTagNameNS(namespace, tagName)
457             if len(children): return children
458         return []
459
460     def _node_data( self, node, tagName, possibleNamespaces=DEFAULT_NAMESPACES):
461         children = self._getElementsByTagName(node, tagName, possibleNamespaces)
462         node = len(children) and children[0] or None
463         return node and "".join([child.data.encode("utf-8") for child in node.childNodes]) or None
464
465     def _get_txt( self, node, tagName, default_txt="" ):
466         return self._node_data( node, tagName ) or self._node_data( node, tagName, self.DUBLIN_CORE ) or default_txt