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
6 from xml.dom.minidom import parseString
10 class LastFMEventRegister:
12 self.onMetadataChangedList = []
14 def addOnMetadataChanged(self,callback):
15 self.onMetadataChangedList.append(callback)
17 def removeOnMetadataChanged(self,callback):
18 self.onMetadataChangedList.remove(callback)
20 def onMetadataChanged(self,metad):
21 for i in self.onMetadataChangedList:
24 lastfm_event_register = LastFMEventRegister()
29 def onPlaylistLoaded(self,reason):
31 def onConnectSuccessful(self,reason):
33 def onConnectFailed(self,reason):
35 def onCommandFailed(self,reason):
37 def onTrackSkiped(self,reason):
39 def onTrackLoved(self,reason):
41 def onTrackBanned(self,reason):
43 def onGlobalTagsLoaded(self,tags):
45 def onTopTracksLoaded(self,tracks):
47 def onRecentTracksLoaded(self,tracks):
49 def onRecentBannedTracksLoaded(self,tracks):
51 def onRecentLovedTracksLoaded(self,tracks):
53 def onNeighboursLoaded(self,user):
55 def onFriendsLoaded(self,user):
57 def onStationChanged(self,reason):
59 def onMetadataLoaded(self,metadata):
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
68 DUBLIN_CORE = ('http://purl.org/dc/elements/1.1/',)
72 host = "ws.audioscrobbler.com"
76 cache_toptags= "/tmp/toptags"
80 LastFMHandler.__init__(self)
81 self.state = False # if logged in
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)
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"])
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"]
103 self.onConnectSuccessful("loggedin")
106 self.onConnectFailed("login failed")
108 def _parselines(self, str):
110 vars = split(str, "\n")
112 x = split(rstrip(v), "=", 1)
115 res[x[0]] = x[1].encode("utf-8")
116 except UnicodeDecodeError:
117 res[x[0]] = "unicodeproblem"
119 print "(urk?", x, ")"
122 def loadPlaylist(self):
123 print "LOADING PLAYLIST"
124 if self.state is not True:
125 self.onCommandFailed("not logged in")
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)
133 def loadPlaylistCB(self,xmlsource):
134 self.playlist = LastFMPlaylist(xmlsource)
135 self.onPlaylistLoaded("playlist loaded")
137 def getPersonalURL(self,username,level=50):
138 return "lastfm://user/%s/recommended/32"%username
140 def getNeighboursURL(self,username):
141 return "lastfm://user/%s/neighbours"%username
143 def getLovedURL(self,username):
144 return "lastfm://user/%s/loved"%username
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")
150 return "lastfm://artist/%s/similarartists"%artist.replace(" ","%20")
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")
156 return "lastfm://artist/%s/fans"%artist.replace(" ","%20")
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")
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")
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)
174 def onTrackLovedCB(self,response):
175 res = self._parselines(response)
176 if res["response"] == "OK":
177 self.onTrackLoved("Track loved")
179 self.onCommandFailed("Server returned FALSE")
181 def onTrackBannedCB(self,response):
182 res = self._parselines(response)
183 if res["response"] == "OK":
184 self.onTrackBanned("Track baned")
186 self.onCommandFailed("Server returned FALSE")
188 def onTrackSkipedCB(self,response):
189 res = self._parselines(response)
190 if res["response"] == "OK":
191 self.onTrackSkiped("Track skiped")
193 self.onCommandFailed("Server returned FALSE")
196 return self.command("love",self.onTrackLovedCB)
199 return self.command("ban",self.onTrackBannedCB)
203 return self.command("skip",self.onTrackSkipedCB)
208 result = result + ("%02x" % ord(c))
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
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
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
226 def getGlobalTags( self ,force_reload=False):
227 if self.state is not True:
228 self.onCommandFailed("not logged in")
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)
236 def getGlobalTagsCB(self,result):
238 rssDocument = parseString(result)
240 for node in self.XMLgetElementsByTagName(rssDocument, 'tag'):
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")
247 self.onGlobalTagsLoaded(data)
248 except xml.parsers.expat.ExpatError,e:
249 self.onCommandFailed(e)
251 def getTopTracks(self,username):
252 if self.state is not True:
253 self.onCommandFailed("not logged in")
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)
261 def getTopTracksCB(self,result):
262 re,rdata = self._parseTracks(result)
264 self.onTopTracksLoaded(rdata)
266 self.onCommandFailed(rdata)
268 def getRecentTracks(self,username):
269 if self.state is not True:
270 self.onCommandFailed("not logged in")
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)
278 def getRecentTracksCB(self,result):
279 re,rdata = self._parseTracks(result)
281 self.onRecentTracksLoaded(rdata)
283 self.onCommandFailed(rdata)
285 def getRecentLovedTracks(self,username):
286 if self.state is not True:
287 self.onCommandFailed("not logged in")
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)
295 def getRecentLovedTracksCB(self,result):
296 re,rdata = self._parseTracks(result)
298 self.onRecentLovedTracksLoaded(rdata)
300 self.onCommandFailed(rdata)
302 def getRecentBannedTracks(self,username):
303 if self.state is not True:
304 self.onCommandFailed("not logged in")
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)
312 def getRecentBannedTracksCB(self,result):
313 re,rdata = self._parseTracks(result)
315 self.onRecentBannedTracksLoaded(rdata)
317 self.onCommandFailed(rdata)
319 def _parseTracks(self,xmlrawdata):
322 rssDocument = parseString(xmlrawdata)
324 for node in self.XMLgetElementsByTagName(rssDocument, 'track'):
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']
334 except xml.parsers.expat.ExpatError,e:
338 def getNeighbours(self,username):
339 if self.state is not True:
340 self.onCommandFailed("not logged in")
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)
348 def getNeighboursCB(self,result):
349 re,rdata = self._parseUser(result)
351 self.onNeighboursLoaded(rdata)
353 self.onCommandFailed(rdata)
355 def getFriends(self,username):
356 if self.state is not True:
357 self.onCommandFailed("not logged in")
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)
365 def getFriendsCB(self,result):
366 re,rdata = self._parseUser(result)
368 self.onFriendsLoaded(rdata)
370 self.onCommandFailed(rdata)
373 def _parseUser(self,xmlrawdata):
376 rssDocument = parseString(xmlrawdata)
378 for node in self.XMLgetElementsByTagName(rssDocument, 'user'):
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']
386 except xml.parsers.expat.ExpatError,e:
390 def changeStation(self,url):
391 if self.state is not True:
392 self.onCommandFailed("not logged in")
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)
400 def changeStationCB(self,result):
401 res = self._parselines(result)
402 if res["response"] == "OK":
403 self.onStationChanged("Station changed")
405 self.onCommandFailed("Server returned "+res["response"])
408 class LastFMPlaylist:
410 this is the new way last.fm handles streams with metadata
412 DEFAULT_NAMESPACES = (None,)
413 DUBLIN_CORE = ('http://purl.org/dc/elements/1.1/',) #why do i need this?
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" )
429 def getTrack(self,tracknumber):
431 return self.tracks[tracknumber]
435 def parseTracks(self):
438 for node in self._getElementsByTagName(self.xmldoc, 'track'):
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)
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
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
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