Repository: beetbox/beets

Issue:
fetchart: Add last.fm source
## Use case

When downloading album art via `fetchart`, it would be useful to be able to query [last.fm](https://last.fm). This functionality is present in [airsonic](https://github.com/airsonic/airsonic), and I tried to find how it works and came across [this commit](https://github.com/airsonic/airsonic/commit/72851f072128134f4ef8c103146aa7596735183d) which seems to implement this functionality.

## Solution

The `fetchart` plugin should have a new source called `lastfm` or `last.fm` that queries last.fm.

## Alternatives

For some releases, the sources currently available for album art don't suffice. Thus, the only suitable alternative is to grab the art manually.



Discussion:
Sounds good! Seems like it shouldn't be that complicated if someone wants to jump in.
I wouldn't mind picking this up - seems rather straighforward.

Resolving patch:
diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py
--- a/beetsplug/fetchart.py
+++ b/beetsplug/fetchart.py
@@ -21,6 +21,7 @@
 import os
 import re
 from tempfile import NamedTemporaryFile
+from collections import OrderedDict
 
 import requests
 
@@ -742,11 +743,72 @@ def get(self, album, plugin, paths):
                                       match=Candidate.MATCH_FALLBACK)
 
 
+class LastFM(RemoteArtSource):
+    NAME = u"Last.fm"
+
+    # Sizes in priority order.
+    SIZES = OrderedDict([
+        ('mega', (300, 300)),
+        ('extralarge', (300, 300)),
+        ('large', (174, 174)),
+        ('medium', (64, 64)),
+        ('small', (34, 34)),
+    ])
+
+    if util.SNI_SUPPORTED:
+        API_URL = 'https://ws.audioscrobbler.com/2.0'
+    else:
+        API_URL = 'http://ws.audioscrobbler.com/2.0'
+
+    def __init__(self, *args, **kwargs):
+        super(LastFM, self).__init__(*args, **kwargs)
+        self.key = self._config['lastfm_key'].get(),
+
+    def get(self, album, plugin, paths):
+        if not album.mb_albumid:
+            return
+
+        try:
+            response = self.request(self.API_URL, params={
+                'method': 'album.getinfo',
+                'api_key': self.key,
+                'mbid': album.mb_albumid,
+                'format': 'json',
+            })
+        except requests.RequestException:
+            self._log.debug(u'lastfm: error receiving response')
+            return
+
+        try:
+            data = response.json()
+
+            if 'error' in data:
+                if data['error'] == 6:
+                    self._log.debug('lastfm: no results for {}',
+                                    album.mb_albumid)
+                else:
+                    self._log.error(
+                        'lastfm: failed to get album info: {} ({})',
+                        data['message'], data['error'])
+            else:
+                images = {image['size']: image['#text']
+                          for image in data['album']['image']}
+
+                # Provide candidates in order of size.
+                for size in self.SIZES.keys():
+                    if size in images:
+                        yield self._candidate(url=images[size],
+                                              size=self.SIZES[size])
+        except ValueError:
+            self._log.debug(u'lastfm: error loading response: {}'
+                            .format(response.text))
+            return
+
 # Try each source in turn.
 
 SOURCES_ALL = [u'filesystem',
                u'coverart', u'itunes', u'amazon', u'albumart',
-               u'wikipedia', u'google', u'fanarttv']
+               u'wikipedia', u'google', u'fanarttv', u'lastfm']
 
 ART_SOURCES = {
     u'filesystem': FileSystem,
@@ -757,6 +819,7 @@ def get(self, album, plugin, paths):
     u'wikipedia': Wikipedia,
     u'google': GoogleImages,
     u'fanarttv': FanartTV,
+    u'lastfm': LastFM,
 }
 SOURCE_NAMES = {v: k for k, v in ART_SOURCES.items()}
 
@@ -787,11 +850,13 @@ def __init__(self):
             'google_key': None,
             'google_engine': u'001442825323518660753:hrh5ch1gjzm',
             'fanarttv_key': None,
+            'lastfm_key': None,
             'store_source': False,
             'high_resolution': False,
         })
         self.config['google_key'].redact = True
         self.config['fanarttv_key'].redact = True
+        self.config['lastfm_key'].redact = True
 
         self.minwidth = self.config['minwidth'].get(int)
         self.maxwidth = self.config['maxwidth'].get(int)
@@ -831,6 +896,9 @@ def __init__(self):
         if not self.config['google_key'].get() and \
                 u'google' in available_sources:
             available_sources.remove(u'google')
+        if not self.config['lastfm_key'].get() and \
+                u'lastfm' in available_sources:
+            available_sources.remove(u'lastfm')
         available_sources = [(s, c)
                              for s in available_sources
                              for c in ART_SOURCES[s].VALID_MATCHING_CRITERIA]


Test patch: