]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
27bbce2d0f122f8952438aafbf2979538a616f37
[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 = {'304':0, 'cached':0, 'minrl':'120'}
54
55     def __init__(self):
56         self.controller = CacheController(self.cache)
57
58     def _fetch(self, ressource, payload):
59         """
60         Prepare http request
61         Use cached elements or proceed http request
62         """
63         req = Request('GET', ressource, params=payload,
64                       ).prepare()
65         if self.cache:
66             cached_response = self.controller.cached_request(req.url, req.headers)
67             if cached_response:
68                 SimaEch.stat.update(cached=SimaEch.stat.get('cached')+1)
69                 return cached_response.json()
70         try:
71             return self._fetch_ws(req)
72         except Timeout:
73             raise WSTimeout('Failed to reach server within {0}s'.format(
74                                SOCKET_TIMEOUT))
75         except ConnectionError as err:
76             raise WSError(err)
77
78     @Throttle(WAIT_BETWEEN_REQUESTS)
79     def _fetch_ws(self, prepreq):
80         """fetch from web service"""
81         sess = Session()
82         resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
83         if resp.status_code == 304:
84             SimaEch.stats.update({'304':SimaEch.stats.get('304')+1})
85             resp = self.controller.update_cached_response(prepreq, resp)
86         elif resp.status_code != 200:
87             raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
88         ans = resp.json()
89         self._controls_answer(ans)
90         SimaEch.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
91         minrl = min(SimaEch.ratelimit, SimaEch.stats.get('minrl'))
92         SimaEch.stats.update(minrl=minrl)
93         if self.cache:
94             self.controller.cache_response(resp.request, resp)
95         return ans
96
97     def _controls_answer(self, ans):
98         """Controls answer.
99         """
100         status = ans.get('response').get('status')
101         code = status.get('code')
102         if code is 0:
103             return True
104         if code is 5:
105             raise WSNotFound('Artist not found')
106         raise WSError(status.get('message'))
107
108     def _forge_payload(self, artist, top=False):
109         """Build payload
110         """
111         payload = {'api_key': ECH.get('apikey')}
112         if not isinstance(artist, Artist):
113             raise TypeError('"{0!r}" not an Artist object'.format(artist))
114         if artist.mbid:
115             payload.update(
116                     id='musicbrainz:artist:{0}'.format(artist.mbid))
117         else:
118             payload.update(name=artist.name)
119         payload.update(bucket='id:musicbrainz')
120         payload.update(results=100)
121         if top:
122             if artist.mbid:
123                 aid = payload.pop('id')
124                 payload.update(artist_id=aid)
125             else:
126                 name = payload.pop('name')
127                 payload.update(artist=name)
128             payload.update(results=100)
129             payload.update(sort='song_hotttnesss-desc')
130         return payload
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