diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 8355aad79..d87f9f41c 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -21,6 +21,7 @@ import os from beets.plugins import BeetsPlugin from beets import importer +from beets import ui IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg'] COVER_NAMES = ['cover', 'front', 'art', 'album', 'folder'] @@ -133,10 +134,10 @@ def art_in_path(path): # Try each source in turn. def art_for_album(album, path, local_only=False): - """Given an AlbumInfo object, returns a path to downloaded art for - the album (or None if no art is found). If `local_only`, then only - local image files from the filesystem are returned; no network - requests are made. + """Given an Album object, returns a path to downloaded art for the + album (or None if no art is found). If `local_only`, then only local + image files from the filesystem are returned; no network requests + are made. """ # Local art. if isinstance(path, basestring): @@ -148,9 +149,9 @@ def art_for_album(album, path, local_only=False): return # CoverArtArchive.org. - if album.album_id: - log.debug('Fetching album art for MBID {0}.'.format(album.album_id)) - out = caa_art(album.album_id) + if album.mb_albumid: + log.debug('Fetching album art for MBID {0}.'.format(album.mb_albumid)) + out = caa_art(album.mb_albumid) if out: return out @@ -169,6 +170,23 @@ def art_for_album(album, path, local_only=False): # PLUGIN LOGIC ############################################################### +def batch_fetch_art(lib, albums, force): + """Fetch album art for each of the albums. This implements the manual + fetchart CLI command. + """ + for album in albums: + if album.artpath and not force: + message = 'has album art' + else: + path = art_for_album(album, None) + if path: + album.set_art(path, False) + message = 'found album art' + else: + message = 'no art found' + log.info(u'{0} - {1}: {2}'.format(album.albumartist, album.album, + message)) + class FetchArtPlugin(BeetsPlugin): def __init__(self): super(FetchArtPlugin, self).__init__() @@ -193,7 +211,8 @@ class FetchArtPlugin(BeetsPlugin): # For any other choices (e.g., TRACKS), do nothing. return - path = art_for_album(task.info, task.path, local_only=local) + album = config.lib.get_album(task.album_id) + path = art_for_album(album, task.path, local_only=local) if path: self.art_paths[task] = path @@ -208,3 +227,14 @@ class FetchArtPlugin(BeetsPlugin): if config.delete or config.move: task.prune(path) + + # Manual album art fetching. + def commands(self): + cmd = ui.Subcommand('fetchart', help='download album art') + cmd.parser.add_option('-f', '--force', dest='force', + action='store_true', default=False, + help='re-download art when already present') + def func(lib, config, opts, args): + batch_fetch_art(lib, lib.albums(ui.decargs(args)), opts.force) + cmd.func = func + return [cmd] diff --git a/docs/changelog.rst b/docs/changelog.rst index 63a8c9a2c..a6a392eb2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,15 +15,20 @@ art for your music, enable this plugin after upgrading to beets 1.0b15. via the :ref:`list_format_item` and :ref:`list_format_album` config options. Thanks to Fabrice Laporte. * Album cover art fetching is now encapsulated in the :doc:`/plugins/fetchart`. - Be sure to enable this plugin if you're using this functionality. -* :doc:`/plugins/fetchart`: Cover art can now be fetched from the `Cover Art - Archive`_, a new image repository from MusicBrainz and the Internet Archive. - While its coverage is currently spotty, CAA is growing and its images are - generally higher-quality than those from Amazon. You can help out by - `submitting new images to the archive`_. -* :doc:`/plugins/fetchart`: "As-is" and non-autotagged imports can now have - album art imported from the local filesystem (although Web repositories are - still not searched in these cases). + Be sure to enable this plugin if you're using this functionality. As a result + of this new organization, the new plugin has gained a few new features: + + * Cover art can now be fetched from the `Cover Art Archive`_, a new image + repository from MusicBrainz and the Internet Archive. While its coverage + is currently spotty, CAA is growing and its images are generally + higher-quality than those from Amazon. You can help out by `submitting new + images to the archive`_. + * "As-is" and non-autotagged imports can now have album art imported from + the local filesystem (although Web repositories are still not searched in + these cases). + * A new command, ``beet fetchart``, allows you to download album art + post-import. + * Errors when communicating with MusicBrainz now log an error message instead of halting the importer. * Similarly, filesystem manipulation errors now print helpful error messages diff --git a/docs/plugins/fetchart.rst b/docs/plugins/fetchart.rst index 4998fc274..6473dee90 100644 --- a/docs/plugins/fetchart.rst +++ b/docs/plugins/fetchart.rst @@ -15,6 +15,19 @@ By default, beets stores album art image files alongside the music files for an album in a file called ``cover.jpg``. To customize the name of this file, use the :ref:`art-filename` config option. +Manually Fetching Album Art +--------------------------- + +Use the ``fetchart`` command to download album art after albums have already +been imported:: + + $ beet fetchart [-f] [query] + +By default, the command will only look for album art when the album doesn't +already have it; the ``-f`` or ``--force`` switch makes it search for art +regardless. If you specify a query, only matching albums will be processed; +otherwise, the command processes every album in your library. + Album Art Sources ----------------- diff --git a/test/_common.py b/test/_common.py index 97e5090f0..b3c7f0c21 100644 --- a/test/_common.py +++ b/test/_common.py @@ -207,3 +207,14 @@ class ExtraAsserts(object): def touch(path): open(path, 'a').close() + +class Bag(object): + """An object that exposes a set of fields given as keyword + arguments. Any field not found in the dictionary appears to be None. + Used for mocking Album objects and the like. + """ + def __init__(self, **fields): + self.fields = fields + + def __getattr__(self, key): + return self.fields.get(key) diff --git a/test/test_art.py b/test/test_art.py index e431a80ce..37ffc176f 100644 --- a/test/test_art.py +++ b/test/test_art.py @@ -92,12 +92,12 @@ class CombinedTest(unittest.TestCase): def test_main_interface_returns_amazon_art(self): fetchart.urllib.urlretrieve = \ MockUrlRetrieve('anotherpath', 'image/jpeg') - album = AlbumInfo(None, None, None, None, None, asin='xxxx') + album = _common.Bag(asin='xxxx') artpath = fetchart.art_for_album(album, None) self.assertEqual(artpath, 'anotherpath') def test_main_interface_returns_none_for_missing_asin_and_path(self): - album = AlbumInfo(None, None, None, None, None, asin=None) + album = _common.Bag() artpath = fetchart.art_for_album(album, None) self.assertEqual(artpath, None) @@ -105,35 +105,35 @@ class CombinedTest(unittest.TestCase): _common.touch(os.path.join(self.dpath, 'a.jpg')) fetchart.urllib.urlretrieve = \ MockUrlRetrieve('anotherpath', 'image/jpeg') - album = AlbumInfo(None, None, None, None, None, asin='xxxx') + album = _common.Bag(asin='xxxx') artpath = fetchart.art_for_album(album, self.dpath) self.assertEqual(artpath, os.path.join(self.dpath, 'a.jpg')) def test_main_interface_falls_back_to_amazon(self): fetchart.urllib.urlretrieve = \ MockUrlRetrieve('anotherpath', 'image/jpeg') - album = AlbumInfo(None, None, None, None, None, asin='xxxx') + album = _common.Bag(asin='xxxx') artpath = fetchart.art_for_album(album, self.dpath) self.assertEqual(artpath, 'anotherpath') def test_main_interface_tries_amazon_before_aao(self): fetchart.urllib.urlretrieve = \ MockUrlRetrieve('anotherpath', 'image/jpeg') - album = AlbumInfo(None, None, None, None, None, asin='xxxx') + album = _common.Bag(asin='xxxx') fetchart.art_for_album(album, self.dpath) self.assertFalse(self.urlopen_called) def test_main_interface_falls_back_to_aao(self): fetchart.urllib.urlretrieve = \ MockUrlRetrieve('anotherpath', 'text/html') - album = AlbumInfo(None, None, None, None, None, asin='xxxx') + album = _common.Bag(asin='xxxx') fetchart.art_for_album(album, self.dpath) self.assertTrue(self.urlopen_called) def test_main_interface_uses_caa_when_mbid_available(self): mock_retrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') fetchart.urllib.urlretrieve = mock_retrieve - album = AlbumInfo(None, 'releaseid', None, None, None, asin='xxxx') + album = _common.Bag(mb_albumid='releaseid', asin='xxxx') artpath = fetchart.art_for_album(album, None) self.assertEqual(artpath, 'anotherpath') self.assertTrue('coverartarchive.org' in mock_retrieve.fetched) @@ -141,7 +141,7 @@ class CombinedTest(unittest.TestCase): def test_local_only_does_not_access_network(self): mock_retrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') fetchart.urllib.urlretrieve = mock_retrieve - album = AlbumInfo(None, 'albumid', None, None, None, asin='xxxx') + album = _common.Bag(mb_albumid='releaseid', asin='xxxx') artpath = fetchart.art_for_album(album, self.dpath, local_only=True) self.assertEqual(artpath, None) self.assertFalse(self.urlopen_called) @@ -151,7 +151,7 @@ class CombinedTest(unittest.TestCase): _common.touch(os.path.join(self.dpath, 'a.jpg')) mock_retrieve = MockUrlRetrieve('anotherpath', 'image/jpeg') fetchart.urllib.urlretrieve = mock_retrieve - album = AlbumInfo(None, 'albumid', None, None, None, asin='xxxx') + album = _common.Bag(mb_albumid='releaseid', asin='xxxx') artpath = fetchart.art_for_album(album, self.dpath, local_only=True) self.assertEqual(artpath, os.path.join(self.dpath, 'a.jpg')) self.assertFalse(self.urlopen_called)