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
34 from sima.lib.http import CacheController
35 from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
36 from sima.utils.utils import getws, Throttle
37 if len(LFM.get('apikey')) == 43: # simple hack allowing imp.reload
42 """Last.fm http client
44 root_url = 'http://{host}/{version}/'.format(**LFM)
53 self.controller = CacheController(self.cache)
56 def _fetch(self, payload):
59 Use cached elements or proceed http request
61 req = Request('GET', SimaFM.root_url, params=payload,
63 SimaFM.stats.update(total=SimaFM.stats.get('total')+1)
65 cached_response = self.controller.cached_request(req.url, req.headers)
67 SimaFM.stats.update(ccontrol=SimaFM.stats.get('ccontrol')+1)
68 return cached_response.json()
70 return self._fetch_ws(req)
72 raise WSTimeout('Failed to reach server within {0}s'.format(
74 except ConnectionError as err:
77 @Throttle(WAIT_BETWEEN_REQUESTS)
78 def _fetch_ws(self, prepreq):
79 """fetch from web service"""
81 resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
82 if resp.status_code == 304:
83 SimaFM.stats.update(etag=SimaFM.stats.get('etag')+1)
84 resp = self.controller.update_cached_response(prepreq, resp)
85 elif resp.status_code != 200:
86 raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
88 self._controls_answer(ans)
90 self.controller.cache_response(resp.request, resp)
93 def _controls_answer(self, ans):
97 code = ans.get('error')
98 mess = ans.get('message')
100 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
104 def _forge_payload(self, artist, method='similar', track=None):
107 payloads = dict({'similar': {'method':'artist.getsimilar',},
108 'top': {'method':'artist.gettoptracks',},
109 'track': {'method':'track.getsimilar',},
110 'info': {'method':'artist.getinfo',},
112 payload = payloads.get(method)
113 payload.update(api_key=LFM.get('apikey'), format='json')
114 if not isinstance(artist, Artist):
115 raise TypeError('"{0!r}" not an Artist object'.format(artist))
118 payload.update(mbid='{0}'.format(artist.mbid))
120 payload.update(artist=artist.name,
122 payload.update(results=100)
123 if method == 'track':
124 payload.update(track=track)
125 # > hashing the URL into a cache key
126 # return a sorted list of 2-tuple to have consistent cache
127 return sorted(payload.items(), key=lambda param: param[0])
129 def get_similar(self, artist=None):
130 """Fetch similar artists
132 payload = self._forge_payload(artist)
134 ans = self._fetch(payload)
135 # Artist might be found be return no 'artist' list…
136 # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
137 # json format is broken IMHO, xml is more consistent IIRC
139 # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
140 # autocorrect=1 should fix it, checking anyway.
141 simarts = ans.get('similarartists').get('artist')
142 if not isinstance(simarts, list):
143 raise WSError('Artist found but no similarities returned')
144 for art in ans.get('similarartists').get('artist'):
145 yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
149 # vim: ai ts=4 sw=4 sts=4 expandtab