]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simafm.py
Fix data structures inconsistency in last.fm WS
[mpd-sima.git] / sima / lib / simafm.py
1 # -*- coding: utf-8 -*-
2 # Copyright (c) 2009, 2010, 2011, 2012, 2013, 2014 Jack Kaliko <kaliko@azylum.org>
3 #
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.
8 #
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.
13 #
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/>.
16 #
17 #
18
19 """
20 Consume EchoNest web service
21 """
22
23 __version__ = '0.5.0'
24 __author__ = 'Jack Kaliko'
25
26
27 from datetime import datetime, timedelta
28
29 from requests import get, Request, Timeout, ConnectionError
30
31 from sima import LFM
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
36     getws(LFM)
37
38 # Some definitions
39 WAIT_BETWEEN_REQUESTS = timedelta(0, 1)
40 SOCKET_TIMEOUT = 6
41
42
43 class SimaFM():
44     """
45     """
46     root_url = 'http://{host}/{version}/'.format(**LFM)
47     cache = {}
48     timestamp = datetime.utcnow()
49     name = 'Last.fm'
50     ratelimit = None
51
52     def __init__(self, cache=True):
53         self.artist = None
54         self._url = self.__class__.root_url
55         self.current_element = None
56         self.caching = cache
57         purge_cache(self.__class__)
58
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
64             return
65         try:
66             self._fetch_ws(payload)
67         except Timeout:
68             raise WSTimeout('Failed to reach server within {0}s'.format(
69                                SOCKET_TIMEOUT))
70         except ConnectionError as err:
71             raise WSError(err)
72
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()
83         if self.caching:
84             SimaFM.cache.update({req.url:
85                                  Cache(self.current_element)})
86
87     def _controls_answer(self):
88         """Controls answer.
89         """
90         if 'error' in self.current_element:
91             code = self.current_element.get('error')
92             mess = self.current_element.get('message')
93             if code == 6:
94                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
95             raise WSError(mess)
96         return True
97
98     def _forge_payload(self, artist, method='similar', track=None):
99         """
100         """
101         payloads = dict({'similar': {'method':'artist.getsimilar',},
102                         'top': {'method':'artist.gettoptracks',},
103                         'track': {'method':'track.getsimilar',},
104                         'info': {'method':'artist.getinfo',},
105                         })
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))
110         self.artist = artist
111         if artist.mbid:
112             payload.update(mbid='{0}'.format(artist.mbid))
113         else:
114            payload.update(artist=artist.name,
115                           autocorrect=1)
116         payload.update(results=100)
117         if method == 'track':
118             payload.update(track=track)
119         return payload
120
121     def get_similar(self, artist=None):
122         """
123         """
124         payload = self._forge_payload(artist)
125         # Construct URL
126         self._fetch(payload)
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
130         # Here what we got:
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))
138
139
140 # VIM MODLINE
141 # vim: ai ts=4 sw=4 sts=4 expandtab