]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
e5fe4852566c41ca9fbc949f5c0351a21cf7f4ad
[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 requests import Session, Request, Timeout, ConnectionError
29
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
37     getws(ECH)
38
39
40 class SimaEch:
41     """EchoNest http client
42     """
43     root_url = 'http://{host}/api/{version}'.format(**ECH)
44     ratelimit = None
45     name = 'EchoNest'
46     cache = False
47     stats = {'etag':0,
48             'ccontrol':0,
49             'minrl':120,
50             'total':0}
51
52     def __init__(self):
53         self.controller = CacheController(self.cache)
54
55     def _fetch(self, ressource, payload):
56         """
57         Prepare http request
58         Use cached elements or proceed http request
59         """
60         req = Request('GET', ressource, params=payload,
61                       ).prepare()
62         SimaEch.stats.update(total=SimaEch.stats.get('total')+1)
63         if self.cache:
64             cached_response = self.controller.cached_request(req.url, req.headers)
65             if cached_response:
66                 SimaEch.stats.update(ccontrol=SimaEch.stats.get('ccontrol')+1)
67                 return cached_response.json()
68         try:
69             return self._fetch_ws(req)
70         except Timeout:
71             raise WSTimeout('Failed to reach server within {0}s'.format(
72                                SOCKET_TIMEOUT))
73         except ConnectionError as err:
74             raise WSError(err)
75
76     @Throttle(WAIT_BETWEEN_REQUESTS)
77     def _fetch_ws(self, prepreq):
78         """fetch from web service"""
79         sess = Session()
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))
86         ans = resp.json()
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)
91         if self.cache:
92             self.controller.cache_response(resp.request, resp)
93         return ans
94
95     def _controls_answer(self, ans):
96         """Controls answer.
97         """
98         status = ans.get('response').get('status')
99         code = status.get('code')
100         if code is 0:
101             return True
102         if code is 5:
103             raise WSNotFound('Artist not found')
104         raise WSError(status.get('message'))
105
106     def _forge_payload(self, artist, top=False):
107         """Build payload
108         """
109         payload = {'api_key': ECH.get('apikey')}
110         if not isinstance(artist, Artist):
111             raise TypeError('"{0!r}" not an Artist object'.format(artist))
112         if artist.mbid:
113             payload.update(
114                     id='musicbrainz:artist:{0}'.format(artist.mbid))
115         else:
116             payload.update(name=artist.name)
117         payload.update(bucket='id:musicbrainz')
118         payload.update(results=100)
119         if top:
120             if artist.mbid:
121                 aid = payload.pop('id')
122                 payload.update(artist_id=aid)
123             else:
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])
131
132     def get_similar(self, artist=None):
133         """Fetch similar artists
134         """
135         payload = self._forge_payload(artist)
136         # Construct URL
137         ressource = '{0}/artist/similar'.format(SimaEch.root_url)
138         ans = self._fetch(ressource, payload)
139         for art in ans.get('response').get('artists'):
140             mbid = None
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'))
147
148     def get_toptrack(self, artist=None):
149         """Fetch artist top tracks
150         """
151         payload = self._forge_payload(artist, top=True)
152         # Construct URL
153         ressource = '{0}/song/search'.format(SimaEch.root_url)
154         ans = self._fetch(ressource, payload)
155         titles = list()
156         art = {
157                 'artist': artist.name,
158                 'musicbrainz_artistid': artist.mbid,
159                 }
160         for song in ans.get('response').get('songs'):
161             title = song.get('title')
162             if title not in titles:
163                 titles.append(title)
164                 yield Track(title=title, **art)
165
166
167 # VIM MODLINE
168 # vim: ai ts=4 sw=4 sts=4 expandtab