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'
29 from requests import Session, Request, Timeout, ConnectionError
31 from sima import LFM, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
32 from sima.lib.meta import Artist
33 from sima.lib.track import Track
35 from sima.lib.http import CacheController
36 from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
37 from sima.utils.utils import getws, Throttle
38 if len(LFM.get('apikey')) == 43: # simple hack allowing imp.reload
43 """Last.fm http client
45 root_url = 'http://{host}/{version}/'.format(**LFM)
54 self.controller = CacheController(self.cache)
57 def _fetch(self, payload):
60 Use cached elements or proceed http request
62 req = Request('GET', SimaFM.root_url, params=payload,
64 SimaFM.stats.update(total=SimaFM.stats.get('total')+1)
66 cached_response = self.controller.cached_request(req.url, req.headers)
68 SimaFM.stats.update(ccontrol=SimaFM.stats.get('ccontrol')+1)
69 return cached_response.json()
71 return self._fetch_ws(req)
73 raise WSTimeout('Failed to reach server within {0}s'.format(
75 except ConnectionError as err:
78 @Throttle(WAIT_BETWEEN_REQUESTS)
79 def _fetch_ws(self, prepreq):
80 """fetch from web service"""
82 resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
83 if resp.status_code == 304:
84 SimaFM.stats.update(etag=SimaFM.stats.get('etag')+1)
85 resp = self.controller.update_cached_response(prepreq, resp)
86 elif resp.status_code != 200:
87 raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
89 self._controls_answer(ans)
91 self.controller.cache_response(resp.request, resp)
94 def _controls_answer(self, ans):
98 code = ans.get('error')
99 mess = ans.get('message')
101 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
105 def _forge_payload(self, artist, method='similar', track=None):
108 payloads = dict({'similar': {'method':'artist.getsimilar',},
109 'top': {'method':'artist.gettoptracks',},
110 'track': {'method':'track.getsimilar',},
111 'info': {'method':'artist.getinfo',},
113 payload = payloads.get(method)
114 payload.update(api_key=LFM.get('apikey'), format='json')
115 if not isinstance(artist, Artist):
116 raise TypeError('"{0!r}" not an Artist object'.format(artist))
119 payload.update(mbid='{0}'.format(artist.mbid))
121 payload.update(artist=artist.name,
123 payload.update(results=100)
124 if method == 'track':
125 payload.update(track=track)
126 # > hashing the URL into a cache key
127 # return a sorted list of 2-tuple to have consistent cache
128 return sorted(payload.items(), key=lambda param: param[0])
130 def get_similar(self, artist=None):
131 """Fetch similar artists
133 payload = self._forge_payload(artist)
135 ans = self._fetch(payload)
136 # Artist might be found be return no 'artist' list…
137 # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
138 # json format is broken IMHO, xml is more consistent IIRC
140 # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
141 # autocorrect=1 should fix it, checking anyway.
142 simarts = ans.get('similarartists').get('artist')
143 if not isinstance(simarts, list):
144 raise WSError('Artist found but no similarities returned')
145 for art in ans.get('similarartists').get('artist'):
146 yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
148 def get_toptrack(self, artist=None):
149 """Fetch artist top tracks
151 payload = self._forge_payload(artist, method='top')
152 ans = self._fetch(payload)
153 tops = ans.get('toptracks').get('track')
155 'artist': artist.name,
156 'musicbrainz_artistid': artist.mbid,
159 for key in ['artist', 'streamable', 'listeners',
160 'url', 'image', '@attr']:
164 song.update(title=song.pop('name'))
165 song.update(time=song.pop('duration'))
169 # vim: ai ts=4 sw=4 sts=4 expandtab