]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simafm.py
Add ETag support for simafm
[mpd-sima.git] / sima / lib / simafm.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009, 2010, 2011, 2012, 2013, 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 Last.fm web service
22 """
23
24 __version__ = '0.5.1'
25 __author__ = 'Jack Kaliko'
26
27
28
29 from requests import Session, Request, Timeout, ConnectionError
30
31 from sima import LFM, SOCKET_TIMEOUT, WAIT_BETWEEN_REQUESTS
32 from sima.lib.meta import Artist
33
34 from sima.lib.http import CacheController
35 from sima.utils.utils import WSError, WSNotFound, WSTimeout, WSHTTPError
36 from sima.utils.utils import getws, Throttle
37 if len(LFM.get('apikey')) == 43:  # simple hack allowing imp.reload
38     getws(LFM)
39
40
41 class SimaFM:
42     """Last.fm http client
43     """
44     root_url = 'http://{host}/{version}/'.format(**LFM)
45     ratelimit = None
46     name = 'Last.fm'
47     cache = False
48     stats = {'etag':0,
49             'ccontrol':0,
50             'total':0}
51
52     def __init__(self):
53         self.controller = CacheController(self.cache)
54         self.artist = None
55
56     def _fetch(self, payload):
57         """
58         Prepare http request
59         Use cached elements or proceed http request
60         """
61         req = Request('GET', SimaFM.root_url, params=payload,
62                       ).prepare()
63         SimaFM.stats.update(total=SimaFM.stats.get('total')+1)
64         if self.cache:
65             cached_response = self.controller.cached_request(req.url, req.headers)
66             if cached_response:
67                 SimaFM.stats.update(ccontrol=SimaFM.stats.get('ccontrol')+1)
68                 return cached_response.json()
69         try:
70             return self._fetch_ws(req)
71         except Timeout:
72             raise WSTimeout('Failed to reach server within {0}s'.format(
73                                SOCKET_TIMEOUT))
74         except ConnectionError as err:
75             raise WSError(err)
76
77     @Throttle(WAIT_BETWEEN_REQUESTS)
78     def _fetch_ws(self, prepreq):
79         """fetch from web service"""
80         sess = Session()
81         resp = sess.send(prepreq, timeout=SOCKET_TIMEOUT)
82         if resp.status_code == 304:
83             SimaFM.stats.update(etag=SimaFM.stats.get('etag')+1)
84             resp = self.controller.update_cached_response(prepreq, resp)
85         elif resp.status_code != 200:
86             raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
87         ans = resp.json()
88         self._controls_answer(ans)
89         if self.cache:
90             self.controller.cache_response(resp.request, resp)
91         return ans
92
93     def _controls_answer(self, ans):
94         """Controls answer.
95         """
96         if 'error' in ans:
97             code = ans.get('error')
98             mess = ans.get('message')
99             if code == 6:
100                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
101             raise WSError(mess)
102         return True
103
104     def _forge_payload(self, artist, method='similar', track=None):
105         """Build payload
106         """
107         payloads = dict({'similar': {'method':'artist.getsimilar',},
108                         'top': {'method':'artist.gettoptracks',},
109                         'track': {'method':'track.getsimilar',},
110                         'info': {'method':'artist.getinfo',},
111                         })
112         payload = payloads.get(method)
113         payload.update(api_key=LFM.get('apikey'), format='json')
114         if not isinstance(artist, Artist):
115             raise TypeError('"{0!r}" not an Artist object'.format(artist))
116         self.artist = artist
117         if artist.mbid:
118             payload.update(mbid='{0}'.format(artist.mbid))
119         else:
120             payload.update(artist=artist.name,
121                            autocorrect=1)
122         payload.update(results=100)
123         if method == 'track':
124             payload.update(track=track)
125         # > hashing the URL into a cache key
126         # return a sorted list of 2-tuple to have consistent cache
127         return sorted(payload.items(), key=lambda param: param[0])
128
129     def get_similar(self, artist=None):
130         """Fetch similar artists
131         """
132         payload = self._forge_payload(artist)
133         # Construct URL
134         ans = self._fetch(payload)
135         # Artist might be found be return no 'artist' list…
136         # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
137         # json format is broken IMHO, xml is more consistent IIRC
138         # Here what we got:
139         # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
140         # autocorrect=1 should fix it, checking anyway.
141         simarts = ans.get('similarartists').get('artist')
142         if not isinstance(simarts, list):
143             raise WSError('Artist found but no similarities returned')
144         for art in ans.get('similarartists').get('artist'):
145             yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
146
147
148 # VIM MODLINE
149 # vim: ai ts=4 sw=4 sts=4 expandtab