diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ad51bb995..6f38f6bd2 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -192,6 +192,58 @@ class GoogleImages(ArtSource): yield item['link'] +class FanartTV(ArtSource): + """Art from fanart.tv requested using their API""" + API_URL = 'http://webservice.fanart.tv/v3/' + API_ALBUMS = API_URL + 'music/albums/' + PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' + + def get(self, album): + if not album.mb_releasegroupid: + return + + response = self.request( + self.API_ALBUMS + album.mb_releasegroupid, + headers={ + 'api-key': self.PROJECT_KEY, + 'client-key': self._config['fanarttv_key'].get() + }) + + try: + data = response.json() + except ValueError: + self._log.debug(u'fanart.tv: error loading response: {}', + response.text) + return + + if u'status' in data and data[u'status'] == u'error': + if u'not found' in data[u'error message'].lower(): + self._log.debug(u'fanart.tv: no image found') + elif u'api key' in data[u'error message'].lower(): + self._log.warning(u'fanart.tv: Invalid API key given, please ' + u'enter a valid one in your config file.') + else: + self._log.debug(u'fanart.tv: error on request: {}', + data[u'error message']) + return + + matches = [] + # can there be more than one releasegroupid per responce? + for mb_releasegroupid in data.get(u'albums', dict()): + if album.mb_releasegroupid == mb_releasegroupid: + # note: there might be more art referenced, e.g. cdart + matches.extend( + data[u'albums'][mb_releasegroupid][u'albumcover']) + # can this actually occur? + else: + self._log.debug(u'fanart.tv: unexpected mb_releasegroupid in ' + u'response!') + + matches.sort(key=lambda x: x[u'likes'], reverse=True) + for item in matches: + yield item[u'url'] + + class ITunesStore(ArtSource): # Art from the iTunes Store. def get(self, album): @@ -396,7 +448,7 @@ class FileSystem(ArtSource): # Try each source in turn. SOURCES_ALL = [u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia', u'google'] + u'wikipedia', u'google', u'fanarttv'] ART_SOURCES = { u'coverart': CoverArtArchive, @@ -405,6 +457,7 @@ ART_SOURCES = { u'amazon': Amazon, u'wikipedia': Wikipedia, u'google': GoogleImages, + u'fanarttv': FanartTV, } # PLUGIN LOGIC ############################################################### @@ -425,8 +478,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', + 'fanarttv_key': None }) self.config['google_key'].redact = True + self.config['fanarttv_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -447,6 +502,13 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): if not self.config['google_key'].get() and \ u'google' in available_sources: available_sources.remove(u'google') + if not self.config['fanarttv_key'].get() and \ + u'fanarttv' in available_sources: + self._log.warn( + u'fanart.tv source enabled, but no personal API given. This ' + u'works as of now, however, fanart.tv prefers users to ' + u'register a personal key. Additionaly this makes new art ' + u'available shorter after its upload. See the documentation.') sources_name = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) self.sources = [ART_SOURCES[s](self._log, self.config) diff --git a/docs/changelog.rst b/docs/changelog.rst index 81078950e..600bc4cb7 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,6 +13,10 @@ New features: * :doc:`/plugins/lyrics`: The plugin can now translate the fetched lyrics to a configured `bing_lang_to` langage. Enabling translation require to register for a Microsoft Azure Marketplace free account. Thanks to :user:`Kraymer`. +* :doc:`/plugins/fetchart`: Album art can now be fetched from `fanart.tv`_. + Albums are matched using the ``mb_releasegroupid`` tag. + +.. _fanart.tv: https://fanart.tv/ Fixes: diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index e53dbc219..f5e3ae601 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -50,13 +50,15 @@ file. The available options are: - **sources**: List of sources to search for images. An asterisk `*` expands to all available sources. Default: ``coverart itunes amazon albumart``, i.e., everything but - ``wikipedia`` and ``google``. Enable those two sources for more matches at - the cost of some speed. + ``wikipedia``, ``google`` and ``fanarttv``. Enable those sources for more + matches at the cost of some speed. - **google_key**: Your Google API key (to enable the Google Custom Search backend). Default: None. - **google_engine**: The custom search engine to use. Default: The `beets custom search engine`_, which searches the entire web. + **fanarttv_key**: The personal API key for requesting art from + fanart.tv. See below. Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ or `Pillow`_. @@ -162,6 +164,21 @@ default engine searches the entire web for cover art. Note that the Google custom search API is limited to 100 queries per day. After that, the fetchart plugin will fall back on other declared data sources. +Fanart.tv +''''''''' + +Although not strictly necessary right now, you might think about +`registering a personal fanart.tv API key`_. Set the ``fanarttv_key`` +configuration option to your key, then add ``fanarttv`` to the list of sources +in your configuration. + +.. _registering a personal fanart.tv API key: https://fanart.tv/get-an-api-key/ + +More detailed information can be found `on their blog`_. Specifically, the +personal key will give you earlier access to new art. + +.. _on their blog: https://fanart.tv/2015/01/personal-api-keys/ + Embedding Album Art ------------------- diff --git a/test/test_art.py b/test/test_art.py index ebfcd53f4..79326b6c4 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -260,6 +260,81 @@ class GoogleImageTest(UseThePlugin): self.assertEqual(list(result_url), []) +class FanartTVTest(UseThePlugin): + RESPONSE_MULTIPLE = u"""{ + "name": "artistname", + "mbid_id": "artistid", + "albums": { + "thereleasegroupid": { + "albumcover": [ + { + "id": "24", + "url": "http://example.com/1.jpg", + "likes": "0" + }, + { + "id": "42", + "url": "http://example.com/2.jpg", + "likes": "0" + }, + { + "id": "23", + "url": "http://example.com/3.jpg", + "likes": "0" + } + ], + "cdart": [ + { + "id": "123", + "url": "http://example.com/4.jpg", + "likes": "0", + "disc": "1", + "size": "1000" + } + ] + } + } + }""" + RESPONSE_ERROR = u"""{ + "status": "error", + "error message": "the error message" + }""" + RESPONSE_MALFORMED = u"bla blup" + + def setUp(self): + super(FanartTVTest, self).setUp() + self.source = fetchart.FanartTV(logger, self.plugin.config) + + @responses.activate + def run(self, *args, **kwargs): + super(FanartTVTest, self).run(*args, **kwargs) + + def mock_response(self, url, json): + responses.add(responses.GET, url, body=json, + content_type='application/json') + + def test_fanarttv_finds_image(self): + album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + self.RESPONSE_MULTIPLE) + result_url = self.source.get(album) + self.assertEqual(list(result_url)[0], 'http://example.com/1.jpg') + + def test_fanarttv_returns_no_result_when_error_received(self): + album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + self.RESPONSE_ERROR) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + def test_fanarttv_returns_no_result_with_malformed_response(self): + album = _common.Bag(mb_releasegroupid=u'thereleasegroupid') + self.mock_response(fetchart.FanartTV.API_ALBUMS + u'thereleasegroupid', + self.RESPONSE_MALFORMED) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + @_common.slow_test() class ArtImporterTest(UseThePlugin): def setUp(self):