]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
Migrate simafm to persistent http caching
[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, 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
54     def __init__(self):
55         self.controller = CacheController(self.cache)
56
57     def _fetch(self, ressource, payload):
58         """
59         Prepare http request
60         Use cached elements or proceed http request
61         """
62         req = Request('GET', 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         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         self.__class__.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
82         if resp.status_code is not 200:
83             raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
84         ans = resp.json()
85         self._controls_answer(ans)
86         if self.cache:
87             self.controller.cache_response(resp.request, resp)
88         return ans
89
90     def _controls_answer(self, ans):
91         """Controls answer.
92         """
93         status = ans.get('response').get('status')
94         code = status.get('code')
95         if code is 0:
96             return True
97         if code is 5:
98             raise WSNotFound('Artist not found')
99         raise WSError(status.get('message'))
100
101     def _forge_payload(self, artist, top=False):
102         """Build payload
103         """
104         payload = {'api_key': ECH.get('apikey')}
105         if not isinstance(artist, Artist):
106             raise TypeError('"{0!r}" not an Artist object'.format(artist))
107         if artist.mbid:
108             payload.update(
109                     id='musicbrainz:artist:{0}'.format(artist.mbid))
110         else:
111             payload.update(name=artist.name)
112         payload.update(bucket='id:musicbrainz')
113         payload.update(results=100)
114         if top:
115             if artist.mbid:
116                 aid = payload.pop('id')
117                 payload.update(artist_id=aid)
118             else:
119                 name = payload.pop('name')
120                 payload.update(artist=name)
121             payload.update(results=100)
122             payload.update(sort='song_hotttnesss-desc')
123         return payload
124
125     def get_similar(self, artist=None):
126         """Fetch similar artists
127         """
128         payload = self._forge_payload(artist)
129         # Construct URL
130         ressource = '{0}/artist/similar'.format(SimaEch.root_url)
131         ans = self._fetch(ressource, payload)
132         for art in ans.get('response').get('artists'):
133             artist = {}
134             mbid = None
135             if 'foreign_ids' in art:
136                 for frgnid in art.get('foreign_ids'):
137                     if frgnid.get('catalog') == 'musicbrainz':
138                         mbid = frgnid.get('foreign_id'
139                                           ).lstrip('musicbrainz:artist:')
140             yield Artist(mbid=mbid, name=art.get('name'))
141
142     def get_toptrack(self, artist=None):
143         """Fetch artist top tracks
144         """
145         payload = self._forge_payload(artist, top=True)
146         # Construct URL
147         ressource = '{0}/song/search'.format(SimaEch.root_url)
148         ans = self._fetch(ressource, payload)
149         titles = list()
150         artist = {
151                 'artist': artist.name,
152                 'musicbrainz_artistid': artist.mbid,
153                 }
154         for song in ans.get('response').get('songs'):
155             title = song.get('title')
156             if title not in titles:
157                 titles.append(title)
158                 yield Track(title=title, **artist)
159
160
161 # VIM MODLINE
162 # vim: ai ts=4 sw=4 sts=4 expandtab