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