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 requests import Session, Request, Timeout, ConnectionError
30 from sima import ECH, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
31 from sima.lib.meta import Artist
32 from sima.lib.track import Track
33 from sima.lib.http import CacheController
34 from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
35 from sima.utils.utils import getws, Throttle
36 if len(ECH.get('apikey')) == 23: # simple hack allowing imp.reload
41 """EchoNest http client
43 root_url = 'http://{host}/api/{version}'.format(**ECH)
53 self.controller = CacheController(self.cache)
55 def _fetch(self, ressource, payload):
58 Use cached elements or proceed http request
60 req = Request('GET', ressource, params=payload,
62 SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
64 cached_response = self.controller.cached_request(req.url, req.headers)
66 SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
67 return cached_response.json()
69 return self._fetch_ws(req)
71 raise WSTimeout('Failed to reach server within {0}s'.format(
73 except ConnectionError as err:
76 @Throttle(WAIT_BETWEEN_REQUESTS)
77 def _fetch_ws(self, prepreq):
78 """fetch from web service"""
80 resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
81 if resp.status_code == 304:
82 SimaEch.stats.update(etag=SimaEch.stats.get('etag')+1)
83 resp = self.controller.update_cached_response(prepreq, resp)
84 elif resp.status_code != 200:
85 raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
87 self._controls_answer(ans)
88 SimaEch.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
89 minrl = min(int(SimaEch.ratelimit), SimaEch.stats.get('minrl'))
90 SimaEch.stats.update(minrl=minrl)
92 self.controller.cache_response(resp.request, resp)
95 def _controls_answer(self, ans):
98 status = ans.get('response').get('status')
99 code = status.get('code')
103 raise WSNotFound('Artist not found')
104 raise WSError(status.get('message'))
106 def _forge_payload(self, artist, top=False):
109 payload = {'api_key': ECH.get('apikey')}
110 if not isinstance(artist, Artist):
111 raise TypeError('"{0!r}" not an Artist object'.format(artist))
114 id='musicbrainz:artist:{0}'.format(artist.mbid))
116 payload.update(name=artist.name)
117 payload.update(bucket='id:musicbrainz')
118 payload.update(results=100)
121 aid = payload.pop('id')
122 payload.update(artist_id=aid)
124 name = payload.pop('name')
125 payload.update(artist=name)
126 payload.update(results=100)
127 payload.update(sort='song_hotttnesss-desc')
128 # > hashing the URL into a cache key
129 # return a sorted list of 2-tuple to have consistent cache
130 return sorted(payload.items(), key=lambda param: param[0])
132 def get_similar(self, artist=None):
133 """Fetch similar artists
135 payload = self._forge_payload(artist)
137 ressource = '{0}/artist/similar'.format(SimaEch.root_url)
138 ans = self._fetch(ressource, payload)
139 for art in ans.get('response').get('artists'):
141 if 'foreign_ids' in art:
142 for frgnid in art.get('foreign_ids'):
143 if frgnid.get('catalog') == 'musicbrainz':
144 mbid = frgnid.get('foreign_id'
145 ).lstrip('musicbrainz:artist:')
146 yield Artist(mbid=mbid, name=art.get('name'))
148 def get_toptrack(self, artist=None):
149 """Fetch artist top tracks
151 payload = self._forge_payload(artist, top=True)
153 ressource = '{0}/song/search'.format(SimaEch.root_url)
154 ans = self._fetch(ressource, payload)
157 'artist': artist.name,
158 'musicbrainz_artistid': artist.mbid,
160 for song in ans.get('response').get('songs'):
161 title = song.get('title')
162 if title not in titles:
164 yield Track(title=title, **art)
168 # vim: ai ts=4 sw=4 sts=4 expandtab