]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simaecho.py
03892c0a3a7efd3ce1d41167e1a70ccbcccf3c80
[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 class SimaEch():
88     """
89     """
90     root_url = 'http://{host}/api/{version}'.format(**ECH)
91     cache = {}
92     timestamp = datetime.utcnow()
93
94     def __init__(self, cache=True):
95         self.artist = None
96         self._ressource = None
97         self.current_element = None
98         self.caching = cache
99         self.purge_cache()
100
101     def _fetch(self, payload):
102         """Use cached elements or proceed http request"""
103         url = Request('GET', self._ressource, params=payload,).prepare().url
104         if url in SimaEch.cache:
105             self.current_element = SimaEch.cache.get(url).elem
106             return
107         try:
108             self._fetch_lfm(payload)
109         except Timeout:
110             raise EchoTimeout('Failed to reach server within {0}s'.format(
111                                SOCKET_TIMEOUT))
112         except ConnectionError as err:
113             raise EchoError(err)
114
115     @Throttle(WAIT_BETWEEN_REQUESTS)
116     def _fetch_lfm(self, payload):
117         """fetch from web service"""
118         req = get(self._ressource, params=payload,
119                             timeout=SOCKET_TIMEOUT)
120         if 'x-ratelimit-remaining' in req.headers:
121             logging.debug('x-ratelimit-remaining {x-ratelimit-remaining}'.format(**req.headers))
122         if req.status_code is not 200:
123             raise EchoHTTPError(req.status_code)
124         self.current_element = req.json()
125         self._controls_answer()
126         if self.caching:
127             SimaEch.cache.update({req.url:
128                                  Cache(self.current_element)})
129
130     def _controls_answer(self):
131         """Controls last.fm answer.
132         """
133         status = self.current_element.get('response').get('status')
134         code = status.get('code')
135         if code is 0:
136             return True
137         if code is 5:
138             raise EchoNotFound('Artist not found: "{0}"'.format(self.artist))
139         raise EchoError(status.get('message'))
140
141     def _forge_payload(self, artist):
142         """
143         """
144         payload = {'api_key': ECH.get('apikey')}
145         if not isinstance(artist, Artist):
146             raise TypeError('"{0!r}" not an Artist object'.format(artist))
147         self.artist = artist
148         if artist.mbid:
149             payload.update(
150                     id='musicbrainz:artist:{0}'.format(artist.mbid))
151         else:
152            payload.update(name=artist.name)
153         payload.update(bucket='id:musicbrainz')
154         payload.update(results=30)
155         return payload
156
157     def purge_cache(self, age=4):
158         now = datetime.utcnow()
159         if now.hour == SimaEch.timestamp.hour:
160             return
161         SimaEch.timestamp = datetime.utcnow()
162         cache = SimaEch.cache
163         delta = timedelta(hours=age)
164         for url in list(cache.keys()):
165             timestamp = cache.get(url).created()
166             if now - timestamp > delta:
167                 cache.pop(url)
168
169     def get_similar(self, artist=None):
170         """
171         """
172         payload = self._forge_payload(artist)
173         # Construct URL
174         self._ressource = '{0}/artist/similar'.format(SimaEch.root_url)
175         self._fetch(payload)
176         for art in self.current_element.get('response').get('artists'):
177             artist = {}
178             mbid = None
179             if 'foreign_ids' in art:
180                for frgnid in art.get('foreign_ids'):
181                    if frgnid.get('catalog') == 'musicbrainz':
182                        mbid = frgnid.get('foreign_id').lstrip('musicbrainz:artist:')
183             yield Artist(mbid=mbid, name=art.get('name'))
184
185
186 # VIM MODLINE
187 # vim: ai ts=4 sw=4 sts=4 expandtab