From ecc6e1c3d6698f800527a9040aecc2d7da1fb712 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Tue, 22 Mar 2016 14:47:09 +0100 Subject: [PATCH 1/9] fanart.tv albumart fetching, missing a project API key --- beetsplug/fetchart.py | 77 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index ad51bb995..61bff901f 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -192,6 +192,68 @@ 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/' + + def get(self, album): + if not album.mb_releasegroupid: + return + + response = self.request( + self.API_ALBUMS + album.mb_releasegroupid, + headers={ + 'api-key': self._config['fanarttv_api_key'].get(), + 'client-key': self._config['fanarttv_personal_key'].get() + }) + + try: + data = response.json() + except ValueError: + self._log.debug(u'fanart.tv: error loading response: {}', + response.text) + return + + def escapeforlog(s): + # the logger will eventually try to .format() the message, and + # interpret the dict as format spec... + r = [] + for c in s: + if c in ['{', '}']: + r.append(c) + r.append(c) + return ''.join(r) + self._log.debug(escapeforlog(str(data))) + + 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): @@ -351,7 +413,7 @@ class Wikipedia(ArtSource): class FileSystem(ArtSource): - """Art from the filesystem""" + """Art from the fileszystem""" @staticmethod def filename_priority(filename, cover_names): """Sort order for image names. @@ -396,7 +458,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 +467,7 @@ ART_SOURCES = { u'amazon': Amazon, u'wikipedia': Wikipedia, u'google': GoogleImages, + u'fanarttv': FanartTV, } # PLUGIN LOGIC ############################################################### @@ -425,8 +488,11 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', + 'fanarttv_api_key': None, + 'fanarttv_personal_key': None }) self.config['google_key'].redact = True + self.config['fanarttv_personal_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -447,6 +513,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_personal_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) From 8a8b8f832a52fc13bd0a9bc05e2f80e462b7ed5d Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:23:22 +0200 Subject: [PATCH 2/9] Update Documentation for fanart.tv --- docs/plugins/fetchart.rst | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index e53dbc219..24a67ded6 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_personal_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 necesarry right now, you might think about +`registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` +configurati 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 ------------------- From d46b45861b8f69e76061049fd30e603b00ca6bcb Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:40:52 +0200 Subject: [PATCH 3/9] typo, rename project key config option --- beetsplug/fetchart.py | 4 ++-- docs/plugins/fetchart.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 61bff901f..860379107 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -204,7 +204,7 @@ class FanartTV(ArtSource): response = self.request( self.API_ALBUMS + album.mb_releasegroupid, headers={ - 'api-key': self._config['fanarttv_api_key'].get(), + 'api-key': self._config['fanarttv_project_key'].get(), 'client-key': self._config['fanarttv_personal_key'].get() }) @@ -488,7 +488,7 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', - 'fanarttv_api_key': None, + 'fanarttv_project_key': None, 'fanarttv_personal_key': None }) self.config['google_key'].redact = True diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 24a67ded6..f9280204b 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -169,8 +169,8 @@ Fanart.tv Although not strictly necesarry right now, you might think about `registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` -configurati option to your key, then add ``fanarttv`` to the list of sources in -your configuration. +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/ From 2dfdc8b90ad260d4ac4ec367ff9bf4bc0b70fc26 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 02:47:56 +0200 Subject: [PATCH 4/9] fix doc formatting --- docs/plugins/fetchart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index f9280204b..005bea320 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -165,7 +165,7 @@ 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 necesarry right now, you might think about `registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` From 87aa5dab13e3605b69a67ab13bd4592b54400119 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 13:28:48 +0200 Subject: [PATCH 5/9] fixes according to feedback by @Kraymer --- beetsplug/fetchart.py | 25 ++++++++++--------------- docs/plugins/fetchart.rst | 6 +++--- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 860379107..fd84416d5 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -196,6 +196,7 @@ 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 = '' def get(self, album): if not album.mb_releasegroupid: @@ -204,8 +205,8 @@ class FanartTV(ArtSource): response = self.request( self.API_ALBUMS + album.mb_releasegroupid, headers={ - 'api-key': self._config['fanarttv_project_key'].get(), - 'client-key': self._config['fanarttv_personal_key'].get() + 'api-key': self.PROJECT_KEY, + 'client-key': self._config['fanarttv_key'].get() }) try: @@ -215,16 +216,11 @@ class FanartTV(ArtSource): response.text) return - def escapeforlog(s): + def escape_for_log(s): # the logger will eventually try to .format() the message, and # interpret the dict as format spec... - r = [] - for c in s: - if c in ['{', '}']: - r.append(c) - r.append(c) - return ''.join(r) - self._log.debug(escapeforlog(str(data))) + return ''.join((2 * c if c in '{}' else c for c in s)) + self._log.debug(escape_for_log(str(data))) if u'status' in data and data[u'status'] == u'error': if u'not found' in data[u'error message'].lower(): @@ -413,7 +409,7 @@ class Wikipedia(ArtSource): class FileSystem(ArtSource): - """Art from the fileszystem""" + """Art from the filesystem""" @staticmethod def filename_priority(filename, cover_names): """Sort order for image names. @@ -488,11 +484,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], 'google_key': None, 'google_engine': u'001442825323518660753:hrh5ch1gjzm', - 'fanarttv_project_key': None, - 'fanarttv_personal_key': None + 'fanarttv_key': None }) self.config['google_key'].redact = True - self.config['fanarttv_personal_key'].redact = True + self.config['fanarttv_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -513,7 +508,7 @@ 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_personal_key'].get() and \ + 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 ' diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 005bea320..f5e3ae601 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -57,7 +57,7 @@ file. The available options are: Default: None. - **google_engine**: The custom search engine to use. Default: The `beets custom search engine`_, which searches the entire web. - **fanarttv_personal_key**: The personal API key for requesting art from + **fanarttv_key**: The personal API key for requesting art from fanart.tv. See below. Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ @@ -167,8 +167,8 @@ After that, the fetchart plugin will fall back on other declared data sources. Fanart.tv ''''''''' -Although not strictly necesarry right now, you might think about -`registering a personal fanart.tv API key`_. Set the ``fanarttv_personal_key`` +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. From 7bec3b9de5d9cb7ab5b857a4bc1bd022a39887ca Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 14:56:14 +0200 Subject: [PATCH 6/9] fanart.tv tests, mostly copied and pasted from the google tests --- test/test_art.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) 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): From 3a2eb03e0e377c80b9e1191b60c3978e1dd9fdea Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 19:32:17 +0200 Subject: [PATCH 7/9] Add fanart.tv API key --- beetsplug/fetchart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index fd84416d5..a31e25cca 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -196,7 +196,7 @@ 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 = '' + PROJECT_KEY = '61a7d0ab4e67162b7a0c7c35915cd48e' def get(self, album): if not album.mb_releasegroupid: From 4763fec35b9efe088785dc87928ef8630755d31f Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 19:42:06 +0200 Subject: [PATCH 8/9] fanart.tv: remove overly verbose logging left over from debugging --- beetsplug/fetchart.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a31e25cca..6f38f6bd2 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -216,12 +216,6 @@ class FanartTV(ArtSource): response.text) return - def escape_for_log(s): - # the logger will eventually try to .format() the message, and - # interpret the dict as format spec... - return ''.join((2 * c if c in '{}' else c for c in s)) - self._log.debug(escape_for_log(str(data))) - 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') From 9106f2d1c27be45c1644079ff153645aa579c838 Mon Sep 17 00:00:00 2001 From: wordofglass Date: Fri, 15 Apr 2016 20:14:45 +0200 Subject: [PATCH 9/9] update changelog --- docs/changelog.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 67960d56f..9f18dda0d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,10 @@ New features: art while copying it. * :doc:`/plugins/importadded`: A new `preserve_write_mtimes` option lets you preserve mtime of files after each write. +* :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: