1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
4 # This program is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation, either version 3 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 Consume Last.fm web service
24 __author__ = 'Jack Kaliko'
27 from datetime import datetime, timedelta
29 from requests import get, Request, Timeout, ConnectionError
32 from sima.lib.meta import Artist
33 from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
34 from sima.utils.utils import getws, Throttle, Cache, purge_cache
35 if len(LFM.get('apikey')) == 43: # simple hack allowing imp.reload
39 WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
44 """Last.fm http client
46 root_url = 'http://{host}/{version}/'.format(**LFM)
48 timestamp = datetime.utcnow()
52 def __init__(self, cache=True):
54 self._url = self.__class__.root_url
55 self.current_element = None
57 purge_cache(self.__class__)
59 def _fetch(self, payload):
60 """Use cached elements or proceed http request"""
61 url = Request('GET', self._url, params=payload,).prepare().url
62 if url in SimaFM.cache:
63 self.current_element = SimaFM.cache.get(url).elem
66 self._fetch_ws(payload)
68 raise WSTimeout('Failed to reach server within {0}s'.format(
70 except ConnectionError as err:
73 @Throttle(WAIT_BETWEEN_REQUESTS)
74 def _fetch_ws(self, payload):
75 """fetch from web service"""
76 req = get(self._url, params=payload,
77 timeout=SOCKET_TIMEOUT)
78 #self.__class__.ratelimit = req.headers.get('x-ratelimit-remaining', None)
79 if req.status_code is not 200:
80 raise WSHTTPError(req.status_code)
81 self.current_element = req.json()
82 self._controls_answer()
84 SimaFM.cache.update({req.url:
85 Cache(self.current_element)})
87 def _controls_answer(self):
90 if 'error' in self.current_element:
91 code = self.current_element.get('error')
92 mess = self.current_element.get('message')
94 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
98 def _forge_payload(self, artist, method='similar', track=None):
101 payloads = dict({'similar': {'method':'artist.getsimilar',},
102 'top': {'method':'artist.gettoptracks',},
103 'track': {'method':'track.getsimilar',},
104 'info': {'method':'artist.getinfo',},
106 payload = payloads.get(method)
107 payload.update(api_key=LFM.get('apikey'), format='json')
108 if not isinstance(artist, Artist):
109 raise TypeError('"{0!r}" not an Artist object'.format(artist))
112 payload.update(mbid='{0}'.format(artist.mbid))
114 payload.update(artist=artist.name,
116 payload.update(results=100)
117 if method == 'track':
118 payload.update(track=track)
121 def get_similar(self, artist=None):
122 """Fetch similar artists
124 payload = self._forge_payload(artist)
127 # Artist might be found be return no 'artist' list…
128 # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
129 # json format is broken IMHO, xml is more consistent IIRC
131 # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
132 # autocorrect=1 should fix it, checking anyway.
133 simarts = self.current_element.get('similarartists').get('artist')
134 if not isinstance(simarts, list):
135 raise WSError('Artist found but no similarities returned')
136 for art in self.current_element.get('similarartists').get('artist'):
137 yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
141 # vim: ai ts=4 sw=4 sts=4 expandtab