]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
Integrate persistent cache with "var_dir" conf option
[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 datetime, 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, 1)
43 SOCKET_TIMEOUT = 4
44
45
46 class SimaEch:
47     """EchoNest http client
48     """
49     root_url = 'http://{host}/api/{version}'.format(**ECH)
50     timestamp = datetime.utcnow()
51     ratelimit = None
52     name = 'EchoNest'
53     cache = False
54
55     def __init__(self):
56         self._ressource = None
57         self.current_element = None
58         self.controller = CacheController(self.cache)
59
60     def _fetch(self, payload):
61         """Use cached elements or proceed http request"""
62         req = Request('GET', self._ressource, params=payload,
63                       ).prepare()
64         if self.cache:
65             cached_response = self.controller.cached_request(req.url, req.headers)
66             if cached_response:
67                 return cached_response.json()
68
69         try:
70             return self._fetch_ws(req)
71         except Timeout:
72             raise WSTimeout('Failed to reach server within {0}s'.format(
73                                SOCKET_TIMEOUT))
74         except ConnectionError as err:
75             raise WSError(err)
76
77     @Throttle(WAIT_BETWEEN_REQUESTS)
78     def _fetch_ws(self, prepreq):
79         """fetch from web service"""
80         sess = Session()
81         resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
82         self.__class__.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
83         if resp.status_code is not 200:
84             raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
85         ans = resp.json()
86         self._controls_answer(ans)
87         if self.cache:
88             self.controller.cache_response(resp.request, resp)
89         return ans
90
91     def _controls_answer(self, ans):
92         """Controls answer.
93         """
94         status = ans.get('response').get('status')
95         code = status.get('code')
96         if code is 0:
97             return True
98         if code is 5:
99             raise WSNotFound('Artist not found')
100         raise WSError(status.get('message'))
101
102     def _forge_payload(self, artist, top=False):
103         """Build payload
104         """
105         payload = {'api_key': ECH.get('apikey')}
106         if not isinstance(artist, Artist):
107             raise TypeError('"{0!r}" not an Artist object'.format(artist))
108         if artist.mbid:
109             payload.update(
110                     id='musicbrainz:artist:{0}'.format(artist.mbid))
111         else:
112             payload.update(name=artist.name)
113         payload.update(bucket='id:musicbrainz')
114         payload.update(results=100)
115         if top:
116             if artist.mbid:
117                 aid = payload.pop('id')
118                 payload.update(artist_id=aid)
119             else:
120                 name = payload.pop('name')
121                 payload.update(artist=name)
122             payload.update(results=100)
123             payload.update(sort='song_hotttnesss-desc')
124         return payload
125
126     def get_similar(self, artist=None):
127         """Fetch similar artists
128         """
129         payload = self._forge_payload(artist)
130         # Construct URL
131         self._ressource = '{0}/artist/similar'.format(SimaEch.root_url)
132         ans = self._fetch(payload)
133         for art in ans.get('response').get('artists'):
134             artist = {}
135             mbid = None
136             if 'foreign_ids' in art:
137                 for frgnid in art.get('foreign_ids'):
138                     if frgnid.get('catalog') == 'musicbrainz':
139                         mbid = frgnid.get('foreign_id'
140                                           ).lstrip('musicbrainz:artist:')
141             yield Artist(mbid=mbid, name=art.get('name'))
142
143     def get_toptrack(self, artist=None):
144         """Fetch artist top tracks
145         """
146         payload = self._forge_payload(artist, top=True)
147         # Construct URL
148         self._ressource = '{0}/song/search'.format(SimaEch.root_url)
149         ans = self._fetch(payload)
150         titles = list()
151         artist = {
152                 'artist': artist.name,
153                 'musicbrainz_artistid': artist.mbid,
154                 }
155         for song in ans.get('response').get('songs'):
156             title = song.get('title')
157             if title not in titles:
158                 titles.append(title)
159                 yield Track(title=title, **artist)
160
161
162 # VIM MODLINE
163 # vim: ai ts=4 sw=4 sts=4 expandtab