1 # -*- coding: utf-8 -*-
3 # Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
5 # This program 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.
10 # This program 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.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 Consume Last.fm web service
25 __author__ = 'Jack Kaliko'
30 from sima.lib.meta import Artist
31 from sima.lib.track import Track
33 from sima.lib.http import HttpClient
34 from sima.utils.utils import WSError, WSNotFound
35 from sima.utils.utils import getws
36 if len(LFM.get('apikey')) == 43: # simple hack allowing imp.reload
41 """Last.fm http client
43 root_url = 'http://{host}/{version}/'.format(**LFM)
51 self.http = HttpClient(cache=self.cache, stats=self.stats)
54 def _controls_answer(self, ans):
58 code = ans.get('error')
59 mess = ans.get('message')
61 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
65 def _forge_payload(self, artist, method='similar', track=None):
68 payloads = dict({'similar': {'method':'artist.getsimilar',},
69 'top': {'method':'artist.gettoptracks',},
70 'track': {'method':'track.getsimilar',},
71 'info': {'method':'artist.getinfo',},
73 payload = payloads.get(method)
74 payload.update(api_key=LFM.get('apikey'), format='json')
75 if not isinstance(artist, Artist):
76 raise TypeError('"{0!r}" not an Artist object'.format(artist))
79 payload.update(mbid='{0}'.format(artist.mbid))
81 payload.update(artist=artist.name,
83 payload.update(results=100)
85 payload.update(track=track)
86 # > hashing the URL into a cache key
87 # return a sorted list of 2-tuple to have consistent cache
88 return sorted(payload.items(), key=lambda param: param[0])
90 def get_similar(self, artist):
91 """Fetch similar artists
93 :param Artist artist: :class:`Artist` to fetch similar artists from
94 :returns: generator of :class:`sima.lib.meta.Artist`
96 payload = self._forge_payload(artist)
98 ans = self.http(self.root_url, payload)
99 self._controls_answer(ans.json()) # pylint: disable=no-member
100 # Artist might be found be return no 'artist' list…
101 # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
102 # json format is broken IMHO, xml is more consistent IIRC
104 # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
105 # autocorrect=1 should fix it, checking anyway.
106 simarts = ans.json().get('similarartists').get('artist') # pylint: disable=no-member
107 if not isinstance(simarts, list):
108 raise WSError('Artist found but no similarities returned')
109 for art in ans.json().get('similarartists').get('artist'): # pylint: disable=no-member
110 yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
112 def get_toptrack(self, artist):
113 """Fetch artist top tracks
115 :param Artist artist: :class:`Artist` to fetch top tracks from
116 :returns: generator of :class:`sima.lib.track.Track`
118 payload = self._forge_payload(artist, method='top')
119 ans = self.http(self.root_url, payload)
120 self._controls_answer(ans.json()) # pylint: disable=no-member
121 tops = ans.json().get('toptracks').get('track') # pylint: disable=no-member
122 art = {'artist': artist.name,
123 'musicbrainz_artistid': artist.mbid,}
125 for key in ['artist', 'streamable', 'listeners',
126 'url', 'image', '@attr']:
130 song.update(title=song.pop('name'))
131 song.update(time=song.pop('duration', 0))
135 # vim: ai ts=4 sw=4 sts=4 expandtab