Merge pull request #1778 from lcharlick/fetchart-google

Fetchart plugin: new google custom search engine backend
This commit is contained in:
Adrian Sampson 2015-12-29 21:40:52 -08:00
commit 1033e1d77d
3 changed files with 112 additions and 9 deletions

View file

@ -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):

View file

@ -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
-------------------

View file

@ -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()