]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
Improved ETag support, add some stats
[mpd-sima.git] / sima / lib / simaecho.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2014 Jack Kaliko <kaliko@azylum.org>
4 #
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.
9 #
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.
14 #
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/>.
17 #
18 #
19
20 """
21 Consume EchoNest web service
22 """
23
24 __version__ = '0.0.2'
25 __author__ = 'Jack Kaliko'
26
27
28 from datetime import timedelta
29
30 from requests import Session, Request, Timeout, ConnectionError
31
32 from sima import ECH
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
39     getws(ECH)
40
41 # Some definitions
42 WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
43 SOCKET_TIMEOUT = 6
44
45
46 class SimaEch:
47     """EchoNest http client
48     """
49     root_url = 'http://{host}/api/{version}'.format(**ECH)
50     ratelimit = None
51     name = 'EchoNest'
52     cache = False
53     stats = {'etag':0,
54             'ccontrol':0,
55             'minrl':120,
56             'total':0}
57
58     def __init__(self):
59         self.controller = CacheController(self.cache)
60
61     def _fetch(self, ressource, payload):
62         """
63         Prepare http request
64         Use cached elements or proceed http request
65         """
66         req = Request('GET', ressource, params=payload,
67                       ).prepare()
68         SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
69         if self.cache:
70             cached_response = self.controller.cached_request(req.url, req.headers)
71             if cached_response:
72                 SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
73                 return cached_response.json()
74         try:
75             return self._fetch_ws(req)
76         except Timeout:
77             raise WSTimeout('Failed to reach server within {0}s'.format(
78                                SOCKET_TIMEOUT))
79         except ConnectionError as err:
80             raise WSError(err)
81
82     @Throttle(WAIT_BETWEEN_REQUESTS)
83     def _fetch_ws(self, prepreq):
84         """fetch from web service"""
85         sess = Session()
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))
92         ans = resp.json()
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)
97         if self.cache:
98             self.controller.cache_response(resp.request, resp)
99         return ans
100
101     def _controls_answer(self, ans):
102         """Controls answer.
103         """
104         status = ans.get('response').get('status')
105         code = status.get('code')
106         if code is 0:
107             return True
108         if code is 5:
109             raise WSNotFound('Artist not found')
110         raise WSError(status.get('message'))
111
112     def _forge_payload(self, artist, top=False):
113         """Build payload
114         """
115         payload = {'api_key': ECH.get('apikey')}
116         if not isinstance(artist, Artist):
117             raise TypeError('"{0!r}" not an Artist object'.format(artist))
118         if artist.mbid:
119             payload.update(
120                     id='musicbrainz:artist:{0}'.format(artist.mbid))
121         else:
122             payload.update(name=artist.name)
123         payload.update(bucket='id:musicbrainz')
124         payload.update(results=100)
125         if top:
126             if artist.mbid:
127                 aid = payload.pop('id')
128                 payload.update(artist_id=aid)
129             else:
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])
137
138     def get_similar(self, artist=None):
139         """Fetch similar artists
140         """
141         payload = self._forge_payload(artist)
142         # Construct URL
143         ressource = '{0}/artist/similar'.format(SimaEch.root_url)
144         ans = self._fetch(ressource, payload)
145         for art in ans.get('response').get('artists'):
146             mbid = None
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'))
153
154     def get_toptrack(self, artist=None):
155         """Fetch artist top tracks
156         """
157         payload = self._forge_payload(artist, top=True)
158         # Construct URL
159         ressource = '{0}/song/search'.format(SimaEch.root_url)
160         ans = self._fetch(ressource, payload)
161         titles = list()
162         art = {
163                 'artist': artist.name,
164                 'musicbrainz_artistid': artist.mbid,
165                 }
166         for song in ans.get('response').get('songs'):
167             title = song.get('title')
168             if title not in titles:
169                 titles.append(title)
170                 yield Track(title=title, **art)
171
172
173 # VIM MODLINE
174 # vim: ai ts=4 sw=4 sts=4 expandtab