]> kaliko git repositories - mpd-sima.git/blob - sima/lib/simafm.py
18fe084a3b8aacfcd3e211c40a19e62313663972
[mpd-sima.git] / sima / lib / simafm.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009-2014 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     name = 'Last.fm'
45     cache = False
46     """HTTP cache to use, in memory or persitent.
47
48     :param BaseCache cache: Set a cache, defaults to `False`.
49     """
50     stats = {'etag':0,
51              'ccontrol':0,
52              'total':0}
53
54     def __init__(self):
55         self.http = HttpClient(cache=self.cache, stats=self.stats)
56         self.artist = None
57
58     def _controls_answer(self, ans):
59         """Controls answer.
60         """
61         if 'error' in ans:
62             code = ans.get('error')
63             mess = ans.get('message')
64             if code == 6:
65                 raise WSNotFound('{0}: "{1}"'.format(mess, self.artist))
66             raise WSError(mess)
67         return True
68
69     def _forge_payload(self, artist, method='similar', track=None):
70         """Build payload
71         """
72         payloads = dict({'similar': {'method':'artist.getsimilar',},
73                          'top': {'method':'artist.gettoptracks',},
74                          'track': {'method':'track.getsimilar',},
75                          'info': {'method':'artist.getinfo',},
76                         })
77         payload = payloads.get(method)
78         payload.update(api_key=LFM.get('apikey'), format='json')
79         if not isinstance(artist, Artist):
80             raise TypeError('"{0!r}" not an Artist object'.format(artist))
81         self.artist = artist
82         if artist.mbid:
83             payload.update(mbid='{0}'.format(artist.mbid))
84         else:
85             payload.update(artist=artist.name,
86                            autocorrect=1)
87         payload.update(results=100)
88         if method == 'track':
89             payload.update(track=track)
90         # > hashing the URL into a cache key
91         # return a sorted list of 2-tuple to have consistent cache
92         return sorted(payload.items(), key=lambda param: param[0])
93
94     def get_similar(self, artist):
95         """Fetch similar artists
96
97         :param sima.lib.meta.Artist artist: `Artist` to fetch similar artists from
98         :returns: generator of :class:`sima.lib.meta.Artist`
99         """
100         payload = self._forge_payload(artist)
101         # Construct URL
102         ans = self.http(self.root_url, payload)
103         try:
104             ans.json()
105         except ValueError as err:
106             # Corrupted/malformed cache? cf. gitlab issue #35
107             raise WSError('Malformed json, try purging the cache: %s')
108         self._controls_answer(ans.json()) # pylint: disable=no-member
109         # Artist might be found but return no 'artist' list…
110         # cf. "Mulatu Astatqe" vs. "Mulatu Astatqé" with autocorrect=0
111         # json format is broken IMHO, xml is more consistent IIRC
112         # Here what we got:
113         # >>> {"similarartists":{"#text":"\n","artist":"Mulatu Astatqe"}}
114         # autocorrect=1 should fix it, checking anyway.
115         simarts = ans.json().get('similarartists').get('artist') # pylint: disable=no-member
116         if not isinstance(simarts, list):
117             raise WSError('Artist found but no similarities returned')
118         for art in ans.json().get('similarartists').get('artist'): # pylint: disable=no-member
119             yield Artist(name=art.get('name'), mbid=art.get('mbid', None))
120
121     def get_toptrack(self, artist):
122         """Fetch artist top tracks
123
124         :param sima.lib.meta.Artist artist: `Artist` to fetch top tracks from
125         :returns: generator of :class:`sima.lib.track.Track`
126         """
127         payload = self._forge_payload(artist, method='top')
128         ans = self.http(self.root_url, payload)
129         self._controls_answer(ans.json()) # pylint: disable=no-member
130         tops = ans.json().get('toptracks').get('track') # pylint: disable=no-member
131         art = {'artist': artist.name,
132                'musicbrainz_artistid': artist.mbid,}
133         for song in tops:
134             for key in ['artist', 'streamable', 'listeners',
135                         'url', 'image', '@attr']:
136                 if key in song:
137                     song.pop(key)
138             song.update(art)
139             song.update(title=song.pop('name'))
140             song.update(time=song.pop('duration', 0))
141             yield Track(**song)
142
143 # VIM MODLINE
144 # vim: ai ts=4 sw=4 sts=4 expandtab