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