]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
4bc08b06e14a5c85ec25d02536dbb9ac86b4e13b
[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.1'
25 __author__ = 'Jack Kaliko'
26
27
28 import logging
29
30 from datetime import datetime, timedelta
31 from time import sleep
32
33 from requests import get, Timeout, ConnectionError
34
35 from sima import ECH
36 from sima.lib.meta import Artist
37 from sima.utils.utils import getws
38 if len(ECH.get('apikey')) == 23:
39     getws(ECH)
40
41 # Some definitions
42 WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
43 SOCKET_TIMEOUT = 4
44
45
46 class EchoError(Exception):
47     pass
48
49 class EchoNotFound(EchoError):
50     pass
51
52 class EchoTimeout(EchoError):
53     pass
54
55 class EchoHTTPError(EchoError):
56     pass
57
58 class Throttle():
59     def __init__(self, wait):
60         self.wait = wait
61         self.last_called = datetime.now()
62
63     def __call__(self, func):
64         def wrapper(*args, **kwargs):
65             while self.last_called + self.wait > datetime.now():
66                 sleep(0.1)
67             result = func(*args, **kwargs)
68             self.last_called = datetime.now()
69             return result
70         return wrapper
71
72
73 class Cache():
74     def __init__(self, elem, last=None):
75         self.elem = elem
76         self.requestdate = last
77         if not last:
78             self.requestdate = datetime.utcnow()
79
80     def created(self):
81         return self.requestdate
82
83     def get(self):
84         return self.elem
85
86
87 def purge_cache(self, age=4):
88     now = datetime.utcnow()
89     if now.hour == SimaEch.timestamp.hour:
90         return
91     SimaEch.timestamp = datetime.utcnow()
92     cache = SimaEch.cache
93     delta = timedelta(hours=age)
94     for url in list(cache.keys()):
95         timestamp = cache.get(url).created()
96         if now - timestamp > delta:
97             cache.pop(url)
98
99
100 class SimaEch():
101     """
102     """
103     root_url = 'http://{host}/api/{version}'.format(**ECH)
104     cache = {}
105     timestamp = datetime.utcnow()
106
107     def __init__(self, cache=True):
108         self.artist = None
109         self._ressource = None
110         self.current_element = None
111         self.caching = cache
112         self.purge_cache()
113
114     def _fetch(self, payload):
115         """Use cached elements or proceed http request"""
116         url = Request('GET', self._ressource, params=payload,).prepare().url
117         if url in SimaEch.cache:
118             self.current_element = SimaEch.cache.get(url).elem
119             return
120         try:
121             self._fetch_lfm(payload)
122         except Timeout:
123             raise EchoTimeout('Failed to reach server within {0}s'.format(
124                                SOCKET_TIMEOUT))
125         except ConnectionError as err:
126             raise EchoError(err)
127
128     @Throttle(WAIT_BETWEEN_REQUESTS)
129     def _fetch_lfm(self, payload):
130         """fetch from web service"""
131         req = get(self._ressource, params=payload,
132                             timeout=SOCKET_TIMEOUT)
133         if 'x-ratelimit-remaining' in req.headers:
134             logging.debug('x-ratelimit-remaining {x-ratelimit-remaining}'.format(**req.headers))
135         if req.status_code is not 200:
136             raise EchoHTTPError(req.status_code)
137         self.current_element = req.json()
138         self._controls_answer()
139         if self.caching:
140             SimaEch.cache.update({req.url:
141                                  Cache(self.current_element)})
142
143     def _controls_answer(self):
144         """Controls last.fm answer.
145         """
146         status = self.current_element.get('response').get('status')
147         code = status.get('code')
148         if code is 0:
149             return True
150         if code is 5:
151             raise EchoNotFound('Artist not found: "{0}"'.format(self.artist))
152         raise EchoError(status.get('message'))
153
154     def _forge_payload(self, artist):
155         """
156         """
157         payload = {'api_key': ECH.get('apikey')}
158         if not isinstance(artist, Artist):
159             raise TypeError('"{0!r}" not an Artist object'.format(artist))
160         self.artist = artist
161         if artist.mbid:
162             payload.update(
163                     id='musicbrainz:artist:{0}'.format(artist.mbid))
164         else:
165            payload.update(name=artist.name)
166         payload.update(bucket='id:musicbrainz')
167         payload.update(results=30)
168         return payload
169
170     def get_similar(self, artist=None):
171         """
172         """
173         payload = self._forge_payload(artist)
174         # Construct URL
175         self._ressource = '{0}/artist/similar'.format(SimaEch.root_url)
176         self._fetch(payload)
177         for art in self.current_element.get('response').get('artists'):
178             artist = {}
179             mbid = None
180             if 'foreign_ids' in art:
181                 for frgnid in art.get('foreign_ids'):
182                     if frgnid.get('catalog') == 'musicbrainz':
183                         mbid = frgnid.get('foreign_id'
184                                           ).lstrip('musicbrainz:artist:')
185             yield Artist(mbid=mbid, name=art.get('name'))
186
187
188 # VIM MODLINE
189 # vim: ai ts=4 sw=4 sts=4 expandtab