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