diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index a56f9f95a..b1ce11409 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -96,8 +96,9 @@ class RequestMixin(object): # ART SOURCES ################################################################ class ArtSource(RequestMixin): - def __init__(self, log): + def __init__(self, log, config): self._log = log + self._config = config def get(self, album): raise NotImplementedError() @@ -157,6 +158,41 @@ class AlbumArtOrg(ArtSource): self._log.debug(u'no image found on page') +class GoogleImages(ArtSource): + URL = u'https://www.googleapis.com/customsearch/v1' + + def get(self, album): + """Return art URL from google custom search engine + given an album title and interpreter. + """ + if not (album.albumartist and album.album): + return + search_string = (album.albumartist + ',' + album.album).encode('utf-8') + response = self.request(self.URL, params={ + 'key': self._config['google_key'].get(), + 'cx': self._config['google_engine'].get(), + 'q': search_string, + 'searchType': 'image' + }) + + # Get results using JSON. + try: + data = response.json() + except ValueError: + self._log.debug(u'google: error loading response: {}' + .format(response.text)) + return + + if 'error' in data: + reason = data['error']['errors'][0]['reason'] + self._log.debug(u'google fetchart error: {0}', reason) + return + + if 'items' in data.keys(): + for item in data['items']: + yield item['link'] + + class ITunesStore(ArtSource): # Art from the iTunes Store. def get(self, album): @@ -361,7 +397,7 @@ class FileSystem(ArtSource): # Try each source in turn. SOURCES_ALL = [u'coverart', u'itunes', u'amazon', u'albumart', - u'wikipedia'] + u'wikipedia', u'google'] ART_SOURCES = { u'coverart': CoverArtArchive, @@ -369,6 +405,7 @@ ART_SOURCES = { u'albumart': AlbumArtOrg, u'amazon': Amazon, u'wikipedia': Wikipedia, + u'google': GoogleImages, } # PLUGIN LOGIC ############################################################### @@ -387,7 +424,10 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): 'cautious': False, 'cover_names': ['cover', 'front', 'art', 'album', 'folder'], 'sources': ['coverart', 'itunes', 'amazon', 'albumart'], + 'google_key': None, + 'google_engine': u'001442825323518660753:hrh5ch1gjzm', }) + self.config['google_key'].redact = True # Holds paths to downloaded images between fetching them and # placing them in the filesystem. @@ -405,10 +445,14 @@ class FetchArtPlugin(plugins.BeetsPlugin, RequestMixin): available_sources = list(SOURCES_ALL) if not HAVE_ITUNES and u'itunes' in available_sources: available_sources.remove(u'itunes') + if not self.config['google_key'].get() and \ + u'google' in available_sources: + available_sources.remove(u'google') sources_name = plugins.sanitize_choices( self.config['sources'].as_str_seq(), available_sources) - self.sources = [ART_SOURCES[s](self._log) for s in sources_name] - self.fs_source = FileSystem(self._log) + self.sources = [ART_SOURCES[s](self._log, self.config) + for s in sources_name] + self.fs_source = FileSystem(self._log, self.config) # Asynchronous; after music is added to the library. def fetch_art(self, session, task): diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 80149d478..e53dbc219 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -50,12 +50,18 @@ 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``. Enable those two sources for more matches at + ``wikipedia`` and ``google``. Enable those two 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. Note: ``minwidth`` and ``enforce_ratio`` options require either `ImageMagick`_ or `Pillow`_. +.. _beets custom search engine: https://cse.google.com.au:443/cse/publicurl?cx=001442825323518660753:hrh5ch1gjzm .. _Pillow: https://github.com/python-pillow/Pillow .. _ImageMagick: http://www.imagemagick.org/ @@ -137,6 +143,24 @@ Once the library is installed, the plugin will use it to search automatically. .. _python-itunes: https://github.com/ocelma/python-itunes .. _pip: http://pip.openplans.org/ +Google custom search +'''''''''''''''''''' + +To use the google image search backend you need to +`register for a Google API key`_. Set the ``google_key`` configuration +option to your key, then add ``google`` to the list of sources in your +configuration. + +.. _register for a Google API key: https://code.google.com/apis/console. + +Optionally, you can `define a custom search engine`_. Get your search engine's +token and use it for your ``google_engine`` configuration option. The +default engine searches the entire web for cover art. + +.. _define a custom search engine: http://www.google.com/cse/all + +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. Embedding Album Art ------------------- diff --git a/test/test_art.py b/test/test_art.py index cb29f3769..e2ba9b89b 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -65,13 +65,13 @@ class FetchImageTest(UseThePlugin): self.assertNotEqual(artpath, None) -class FSArtTest(_common.TestCase): +class FSArtTest(UseThePlugin): def setUp(self): super(FSArtTest, self).setUp() self.dpath = os.path.join(self.temp_dir, 'arttest') os.mkdir(self.dpath) - self.source = fetchart.FileSystem(logger) + self.source = fetchart.FileSystem(logger, self.plugin.config) def test_finds_jpg_in_directory(self): _common.touch(os.path.join(self.dpath, 'a.jpg')) @@ -190,13 +190,13 @@ class CombinedTest(UseThePlugin): self.assertEqual(len(responses.calls), 0) -class AAOTest(_common.TestCase): +class AAOTest(UseThePlugin): ASIN = 'xxxx' AAO_URL = 'http://www.albumart.org/index_detail.php?asin={0}'.format(ASIN) def setUp(self): super(AAOTest, self).setUp() - self.source = fetchart.AlbumArtOrg(logger) + self.source = fetchart.AlbumArtOrg(logger, self.plugin.config) @responses.activate def run(self, *args, **kwargs): @@ -226,6 +226,41 @@ class AAOTest(_common.TestCase): self.assertEqual(list(res), []) +class GoogleImageTest(UseThePlugin): + def setUp(self): + super(GoogleImageTest, self).setUp() + self.source = fetchart.GoogleImages(logger, self.plugin.config) + + @responses.activate + def run(self, *args, **kwargs): + super(GoogleImageTest, self).run(*args, **kwargs) + + def mock_response(self, url, json): + responses.add(responses.GET, url, body=json, + content_type='application/json') + + def test_google_art_finds_image(self): + album = _common.Bag(albumartist="some artist", album="some album") + json = b'{"items": [{"link": "url_to_the_image"}]}' + self.mock_response(fetchart.GoogleImages.URL, json) + result_url = self.source.get(album) + self.assertEqual(list(result_url)[0], 'url_to_the_image') + + def test_google_art_returns_no_result_when_error_received(self): + album = _common.Bag(albumartist="some artist", album="some album") + json = b'{"error": {"errors": [{"reason": "some reason"}]}}' + self.mock_response(fetchart.GoogleImages.URL, json) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + def test_google_art_returns_no_result_with_malformed_response(self): + album = _common.Bag(albumartist="some artist", album="some album") + json = b"""bla blup""" + self.mock_response(fetchart.GoogleImages.URL, json) + result_url = self.source.get(album) + self.assertEqual(list(result_url), []) + + class ArtImporterTest(UseThePlugin): def setUp(self): super(ArtImporterTest, self).setUp()