]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simafm.py
f54ab015e99a57a01ddded9e58db83a711795ac9
[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 from datetime import timedelta
29
30 from requests import Session, Request, Timeout, ConnectionError
31
32 from sima import LFM
33 from sima.lib.meta import Artist
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 # Some definitions
42 WAIT_BETWEEN_REQUESTS = timedelta(0, 2)
43 SOCKET_TIMEOUT = 6
44
45
46 class SimaFM:
47     """Last.fm http client
48     """
49     root_url = 'http://{host}/{version}/'.format(**LFM)
50     ratelimit = None
51     name = 'Last.fm'
52     cache = {}
53
54     def __init__(self, cache=True):
55         self.controller = CacheController(self.cache)
56         self.artist = None
57
58     def _fetch(self, payload):
59         """
60         Prepare http request
61         Use cached elements or proceed http request
62         """
63         req = Request('GET', SimaFM.root_url, params=payload,
64                       ).prepare()
65         if self.cache:
66             cached_response = self.controller.cached_request(req.url, req.headers)
67             if cached_response:
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         #self.__class__.ratelimit = resp.headers.get('x-ratelimit-remaining', None)
83         if resp.status_code is not 200:
84             raise WSHTTPError('{0.status_code}: {0.reason}'.format(resp))
85         ans = resp.json()
86         self._controls_answer(ans)
87         if self.cache:
88             self.controller.cache_response(resp.request, resp)
89         return ans
90
91     def _controls_answer(self, ans):
92         """Controls answer.
93         """
94         if 'error' in ans:
95             code = ans.get('error')
96             mess = ans.get('message')
97             if code == 6:
98                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
99             raise WSError(mess)
100         return True
101
102     def _forge_payload(self, artist, method='similar', track=None):
103         """Build payload
104         """
105         payloads = dict({'similar': {'method':'artist.getsimilar',},
106                         'top': {'method':'artist.gettoptracks',},
107                         'track': {'method':'track.getsimilar',},
108                         'info': {'method':'artist.getinfo',},
109                         })
110         payload = payloads.get(method)
111         payload.update(api_key=LFM.get('apikey'), format='json')
112         if not isinstance(artist, Artist):
113             raise TypeError('"{0!r}" not an Artist object'.format(artist))
114         self.artist = artist
115         if artist.mbid:
116             payload.update(mbid='{0}'.format(artist.mbid))
117         else:
118             payload.update(artist=artist.name,
119                            autocorrect=1)
120         payload.update(results=100)
121         if method == 'track':
122             payload.update(track=track)
123         return payload
124
125     def get_similar(self, artist=None):
126         """Fetch similar artists
127         """
128         payload = self._forge_payload(artist)
129         # Construct URL
130         ans = self._fetch(payload)
131         # Artist might be found be return no 'artist' list…
132         # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
133         # json format is broken IMHO, xml is more consistent IIRC
134         # Here what we got:
135         # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
136         # autocorrect=1 should fix it, checking anyway.
137         simarts = ans.get('similarartists').get('artist')
138         if not isinstance(simarts, list):
139             raise WSError('Artist found but no similarities returned')
140         for art in ans.get('similarartists').get('artist'):
141             yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
142
143
144 # VIM MODLINE
145 # vim: ai ts=4 sw=4 sts=4 expandtab