]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simafm.py
http client/cache controller refactoring
[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 sima import LFM
30 from sima.lib.meta import Artist
31 from sima.lib.track import Track
32
33 from sima.lib.http import HttpClient
34 from sima.utils.utils import WSError, WSNotFound
35 from sima.utils.utils import getws
36 if len(LFM.get('apikey')) == 43:  # simple hack allowing imp.reload
37     getws(LFM)
38
39
40 class SimaFM:
41     """Last.fm http client
42     """
43     root_url = 'http://{host}/{version}/'.format(**LFM)
44     ratelimit = None
45     name = 'Last.fm'
46     cache = False
47     stats = {'etag':0,
48             'ccontrol':0,
49             'total':0}
50
51     def __init__(self):
52         self.http = HttpClient(cache=self.cache, stats=self.stats)
53         self.artist = None
54
55     def _controls_answer(self, ans):
56         """Controls answer.
57         """
58         if 'error' in ans:
59             code = ans.get('error')
60             mess = ans.get('message')
61             if code == 6:
62                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
63             raise WSError(mess)
64         return True
65
66     def _forge_payload(self, artist, method='similar', track=None):
67         """Build payload
68         """
69         payloads = dict({'similar': {'method':'artist.getsimilar',},
70                         'top': {'method':'artist.gettoptracks',},
71                         'track': {'method':'track.getsimilar',},
72                         'info': {'method':'artist.getinfo',},
73                         })
74         payload = payloads.get(method)
75         payload.update(api_key=LFM.get('apikey'), format='json')
76         if not isinstance(artist, Artist):
77             raise TypeError('"{0!r}" not an Artist object'.format(artist))
78         self.artist = artist
79         if artist.mbid:
80             payload.update(mbid='{0}'.format(artist.mbid))
81         else:
82             payload.update(artist=artist.name,
83                            autocorrect=1)
84         payload.update(results=100)
85         if method == 'track':
86             payload.update(track=track)
87         # > hashing the URL into a cache key
88         # return a sorted list of 2-tuple to have consistent cache
89         return sorted(payload.items(), key=lambda param: param[0])
90
91     def get_similar(self, artist=None):
92         """Fetch similar artists
93         """
94         payload = self._forge_payload(artist)
95         # Construct URL
96         ans = self.http(self.root_url, payload)
97         self._controls_answer(ans.json())
98         # Artist might be found be return no 'artist' list…
99         # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
100         # json format is broken IMHO, xml is more consistent IIRC
101         # Here what we got:
102         # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
103         # autocorrect=1 should fix it, checking anyway.
104         simarts = ans.json().get('similarartists').get('artist')
105         if not isinstance(simarts, list):
106             raise WSError('Artist found but no similarities returned')
107         for art in ans.json().get('similarartists').get('artist'):
108             yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
109
110     def get_toptrack(self, artist=None):
111         """Fetch artist top tracks
112         """
113         payload = self._forge_payload(artist, method='top')
114         ans = self.http(self.root_url, payload)
115         self._controls_answer(ans.json())
116         tops = ans.json().get('toptracks').get('track')
117         art = {
118                 'artist': artist.name,
119                 'musicbrainz_artistid': artist.mbid,
120                 }
121         for song in tops:
122             for key in ['artist', 'streamable', 'listeners',
123                         'url', 'image', '@attr']:
124                 if key in song:
125                     song.pop(key)
126             song.update(art)
127             song.update(title=song.pop('name'))
128             song.update(time=song.pop('duration'))
129             yield Track(**song)
130
131 # VIM MODLINE
132 # vim: ai ts=4 sw=4 sts=4 expandtab