fetchart: command to manually download art

This commit is contained in:
Adrian Sampson 2012-06-24 18:27:00 -07:00
parent 77cbb19564
commit fbb5823541
5 changed files with 85 additions and 26 deletions

View file

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

View file

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

View file

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

View file

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

View file

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