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'
28 from datetime import datetime, timedelta
30 from requests import Session, Request, Timeout, ConnectionError
33 from sima.lib.meta import Artist
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
42 WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
47 """Last.fm http client
49 root_url = 'http://{host}/{version}/'.format(**LFM)
54 def __init__(self, cache=True):
55 self.controller = CacheController(self.cache)
58 def _fetch(self, payload):
61 Use cached elements or proceed http request
63 req = Request('GET', SimaFM.root_url, params=payload,
66 cached_response = self.controller.cached_request(req.url, req.headers)
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 #self.__class__.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
83 if resp.status_code is not 200:
84 raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
86 self._controls_answer(ans)
88 self.controller.cache_response(resp.request, resp)
91 def _controls_answer(self, ans):
95 code = ans.get('error')
96 mess = ans.get('message')
98 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
102 def _forge_payload(self, artist, method='similar', track=None):
105 payloads = dict({'similar': {'method':'artist.getsimilar',},
106 'top': {'method':'artist.gettoptracks',},
107 'track': {'method':'track.getsimilar',},
108 'info': {'method':'artist.getinfo',},
110 payload = payloads.get(method)
111 payload.update(api_key=LFM.get('apikey'), format='json')
112 if not isinstance(artist, Artist):
113 raise TypeError('"{0!r}" not an Artist object'.format(artist))
116 payload.update(mbid='{0}'.format(artist.mbid))
118 payload.update(artist=artist.name,
120 payload.update(results=100)
121 if method == 'track':
122 payload.update(track=track)
125 def get_similar(self, artist=None):
126 """Fetch similar artists
128 payload = self._forge_payload(artist)
130 ans = self._fetch(payload)
131 # Artist might be found be return no 'artist' list…
132 # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
133 # json format is broken IMHO, xml is more consistent IIRC
135 # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
136 # autocorrect=1 should fix it, checking anyway.
137 simarts = ans.get('similarartists').get('artist')
138 if not isinstance(simarts, list):
139 raise WSError('Artist found but no similarities returned')
140 for art in ans.get('similarartists').get('artist'):
141 yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
145 # vim: ai ts=4 sw=4 sts=4 expandtab