1 # -*- coding: utf-8 -*-
3 # Copyright (c) 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 EchoNest web service
25 __author__ = 'Jack Kaliko'
28 from datetime import timedelta
30 from requests import Session, Request, Timeout, ConnectionError
33 from sima.lib.meta import Artist
34 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(ECH.get('apikey')) == 23: # simple hack allowing imp.reload
42 WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
47 """EchoNest http client
49 root_url = 'http://{host}/api/{version}'.format(**ECH)
59 self.controller = CacheController(self.cache)
61 def _fetch(self, ressource, payload):
64 Use cached elements or proceed http request
66 req = Request('GET', ressource, params=payload,
68 SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
70 cached_response = self.controller.cached_request(req.url, req.headers)
72 SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
73 return cached_response.json()
75 return self._fetch_ws(req)
77 raise WSTimeout('Failed to reach server within {0}s'.format(
79 except ConnectionError as err:
82 @Throttle(WAIT_BETWEEN_REQUESTS)
83 def _fetch_ws(self, prepreq):
84 """fetch from web service"""
86 resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
87 if resp.status_code == 304:
88 SimaEch.stats.update(etag=SimaEch.stats.get('etag')+1)
89 resp = self.controller.update_cached_response(prepreq, resp)
90 elif resp.status_code != 200:
91 raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
93 self._controls_answer(ans)
94 SimaEch.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
95 minrl = min(int(SimaEch.ratelimit), SimaEch.stats.get('minrl'))
96 SimaEch.stats.update(minrl=minrl)
98 self.controller.cache_response(resp.request, resp)
101 def _controls_answer(self, ans):
104 status = ans.get('response').get('status')
105 code = status.get('code')
109 raise WSNotFound('Artist not found')
110 raise WSError(status.get('message'))
112 def _forge_payload(self, artist, top=False):
115 payload = {'api_key': ECH.get('apikey')}
116 if not isinstance(artist, Artist):
117 raise TypeError('"{0!r}" not an Artist object'.format(artist))
120 id='musicbrainz:artist:{0}'.format(artist.mbid))
122 payload.update(name=artist.name)
123 payload.update(bucket='id:musicbrainz')
124 payload.update(results=100)
127 aid = payload.pop('id')
128 payload.update(artist_id=aid)
130 name = payload.pop('name')
131 payload.update(artist=name)
132 payload.update(results=100)
133 payload.update(sort='song_hotttnesss-desc')
134 # > hashing the URL into a cache key
135 # return a sorted list of 2-tuple to have consistent cache
136 return sorted(payload.items(), key=lambda param: param[0])
138 def get_similar(self, artist=None):
139 """Fetch similar artists
141 payload = self._forge_payload(artist)
143 ressource = '{0}/artist/similar'.format(SimaEch.root_url)
144 ans = self._fetch(ressource, payload)
145 for art in ans.get('response').get('artists'):
147 if 'foreign_ids' in art:
148 for frgnid in art.get('foreign_ids'):
149 if frgnid.get('catalog') == 'musicbrainz':
150 mbid = frgnid.get('foreign_id'
151 ).lstrip('musicbrainz:artist:')
152 yield Artist(mbid=mbid, name=art.get('name'))
154 def get_toptrack(self, artist=None):
155 """Fetch artist top tracks
157 payload = self._forge_payload(artist, top=True)
159 ressource = '{0}/song/search'.format(SimaEch.root_url)
160 ans = self._fetch(ressource, payload)
163 'artist': artist.name,
164 'musicbrainz_artistid': artist.mbid,
166 for song in ans.get('response').get('songs'):
167 title = song.get('title')
168 if title not in titles:
170 yield Track(title=title, **art)
174 # vim: ai ts=4 sw=4 sts=4 expandtab