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