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