Merge pull request #802 from geigerzaehler/embed

Merge pull request #802
This commit is contained in:
Adrian Sampson 2014-06-02 21:30:28 -07:00
commit 0c5fbdcefd
5 changed files with 137 additions and 80 deletions

View file

@ -24,7 +24,7 @@ import pipes
from beets import ui, util, plugins, config
from beets.plugins import BeetsPlugin
from beetsplug.embedart import _embed
from beetsplug.embedart import embed_item
log = logging.getLogger('beets')
_fs_lock = threading.Lock()
@ -187,15 +187,8 @@ def convert_item(dest_dir, keep_new, path_formats):
if config['convert']['embed']:
album = item.get_album()
if album:
artpath = album.artpath
if artpath:
try:
_embed(artpath, [converted])
except IOError as exc:
log.warn(u'could not embed cover art in {0}: {1}'
.format(util.displayable_path(item.path),
exc))
if album and album.artpath:
embed_item(item, album.artpath, itempath=converted)
plugins.send('after_convert', item=item, dest=dest, keepnew=keep_new)

View file

@ -1,5 +1,5 @@
# This file is part of beets.
# Copyright 2013, Adrian Sampson.
# Copyright 2014, Adrian Sampson.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@ -13,6 +13,7 @@
# included in all copies or substantial portions of the Software.
"""Allows beets to embed album art into file metadata."""
import os.path
import logging
import imghdr
@ -27,36 +28,6 @@ from beets import config
log = logging.getLogger('beets')
def _embed(image_path, audio_paths, maxwidth=0):
"""Embed an image file into each file audio file.
"""
if maxwidth:
image_path = ArtResizer.shared.resize(maxwidth, syspath(image_path))
try:
with open(syspath(image_path), 'rb') as f:
data = f.read()
except IOError as exc:
log.error(u'embedart: could not read image file: {0}'.format(exc))
return
image = mediafile.Image(data, type=mediafile.ImageType.front)
# Add art to each file.
log.debug('Embedding album art.')
for path in audio_paths:
try:
f = mediafile.MediaFile(syspath(path))
f.images = [image]
f.save(config['id3v23'].get(bool))
except (OSError, IOError, mediafile.UnreadableFileError,
mediafile.MutagenError) as exc:
log.error('Could not embed art in {0}: {1}'.format(
displayable_path(path), exc
))
continue
class EmbedCoverArtPlugin(BeetsPlugin):
"""Allows albumart to be embedded into the actual files.
"""
@ -80,13 +51,17 @@ class EmbedCoverArtPlugin(BeetsPlugin):
embed_cmd.parser.add_option(
'-f', '--file', metavar='PATH', help='the image file to embed'
)
maxwidth = config['embedart']['maxwidth'].get(int)
def embed_func(lib, opts, args):
if opts.file:
imagepath = normpath(opts.file)
embed(lib, imagepath, decargs(args))
for item in lib.items(decargs(args)):
embed_item(item, imagepath, maxwidth)
else:
embed_current(lib, decargs(args))
for album in lib.albums(decargs(args)):
embed_album(album, maxwidth)
embed_cmd.func = embed_func
# Extract command.
@ -111,38 +86,58 @@ class EmbedCoverArtPlugin(BeetsPlugin):
return [embed_cmd, extract_cmd, clear_cmd]
# 'embedart' command with --file argument.
@EmbedCoverArtPlugin.listen('album_imported')
def album_imported(lib, album):
"""Automatically embed art into imported albums.
"""
if album.artpath and config['embedart']['auto']:
embed_album(album, config['embedart']['maxwidth'].get(int))
def embed(lib, imagepath, query):
albums = lib.albums(query)
for i_album in albums:
album = i_album
break
else:
log.error('No album matches query.')
def embed_item(item, imagepath, maxwidth=None, itempath=None):
"""Embed an image into the item's media file.
"""
try:
item['images'] = [_mediafile_image(imagepath, maxwidth)]
item.try_write(itempath)
except IOError as exc:
log.error(u'embedart: could not read image file: {0}'.format(exc))
finally:
# We don't want to store the image in the database
del item['images']
def embed_album(album, maxwidth=None):
"""Embed album art into all of the album's items.
"""
imagepath = album.artpath
if not imagepath:
log.info(u'No album art present: {0} - {1}'.
format(album.albumartist, album.album))
return
if not os.path.isfile(imagepath):
log.error(u'Album art not found at {0}'
.format(imagepath))
return
log.info(u'Embedding album art into {0.albumartist} - {0.album}.'.format(
album
))
_embed(imagepath, [i.path for i in album.items()],
config['embedart']['maxwidth'].get(int))
log.info(u'Embedding album art into {0.albumartist} - {0.album}.'
.format(album))
for item in album.items():
embed_item(item, imagepath, maxwidth)
# 'embedart' command without explicit file.
def _mediafile_image(image_path, maxwidth=None):
"""Return a `mediafile.Image` object for the path.
def embed_current(lib, query):
albums = lib.albums(query)
for album in albums:
if not album.artpath:
log.info(u'No album art present: {0} - {1}'.
format(album.albumartist, album.album))
continue
If maxwidth is set the image is resized if necessary.
"""
if maxwidth:
image_path = ArtResizer.shared.resize(maxwidth, syspath(image_path))
log.info(u'Embedding album art into {0} - {1}'.
format(album.albumartist, album.album))
_embed(album.artpath, [i.path for i in album.items()],
config['embedart']['maxwidth'].get(int))
with open(syspath(image_path), 'rb') as f:
data = f.read()
return mediafile.Image(data, type=mediafile.ImageType.front)
# 'extractart' command.
@ -196,12 +191,3 @@ def clear(lib, query):
continue
mf.art = None
mf.save(config['id3v23'].get(bool))
@EmbedCoverArtPlugin.listen('album_imported')
def album_imported(lib, album):
"""Automatically embed art into imported albums.
"""
if album.artpath and config['embedart']['auto']:
_embed(album.artpath, [i.path for i in album.items()],
config['embedart']['maxwidth'].get(int))

View file

@ -232,11 +232,11 @@ class TestHelper(object):
items.append(item)
return items
def add_album_fixture(self, track_count=1):
def add_album_fixture(self, track_count=1, ext='mp3'):
"""Add an album with files to the database.
"""
items = []
path = os.path.join(_common.RSRC, 'full.mp3')
path = os.path.join(_common.RSRC, 'full.' + ext)
for i in range(track_count):
item = Item.from_path(str(path))
item.album = u'\u00e4lbum' # Check unicode paths

View file

@ -13,9 +13,13 @@
# included in all copies or substantial portions of the Software.
import os.path
import _common
from _common import unittest
from helper import TestHelper, control_stdin
from beets.mediafile import MediaFile
class ImportConvertTest(unittest.TestCase, TestHelper):
def setUp(self):
@ -63,7 +67,8 @@ class ConvertCliTest(unittest.TestCase, TestHelper):
def setUp(self):
self.setup_beets(disk=True) # Converter is threaded
self.item, = self.add_item_fixtures(ext='ogg')
self.album = self.add_album_fixture(ext='ogg')
self.item = self.album.items()[0]
self.load_plugins('convert')
self.convert_dest = os.path.join(self.temp_dir, 'convert_dest')
@ -90,6 +95,20 @@ class ConvertCliTest(unittest.TestCase, TestHelper):
self.item.load()
self.assertEqual(os.path.splitext(self.item.path)[1], '.mp3')
def test_embed_album_art(self):
self.config['convert']['embed'] = True
image_path = os.path.join(_common.RSRC, 'image-2x3.jpg')
self.album.artpath = image_path
self.album.store()
with open(os.path.join(image_path)) as f:
image_data = f.read()
with control_stdin('y'):
self.run_command('convert', self.item.path)
converted = os.path.join(self.convert_dest, 'converted.mp3')
mediafile = MediaFile(converted)
self.assertEqual(mediafile.images[0].data, image_data)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)

59
test/test_embedart.py Normal file
View file

@ -0,0 +1,59 @@
# This file is part of beets.
# Copyright 2014, Thomas Scholtes.
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
import os.path
import _common
from _common import unittest
from helper import TestHelper
from beets.mediafile import MediaFile
class EmbedartCliTest(unittest.TestCase, TestHelper):
artpath = os.path.join(_common.RSRC, 'image-2x3.jpg')
def setUp(self):
self.setup_beets() # Converter is threaded
self.load_plugins('embedart')
with open(self.artpath) as f:
self.image_data = f.read()
def tearDown(self):
self.unload_plugins()
self.teardown_beets()
def test_embed_art_from_file(self):
album = self.add_album_fixture()
item = album.items()[0]
self.run_command('embedart', '-f', self.artpath)
mediafile = MediaFile(item.path)
self.assertEqual(mediafile.images[0].data, self.image_data)
def test_embed_art_from_album(self):
album = self.add_album_fixture()
item = album.items()[0]
album.artpath = self.artpath
album.store()
self.run_command('embedart')
mediafile = MediaFile(item.path)
self.assertEqual(mediafile.images[0].data, self.image_data)
def suite():
return unittest.TestLoader().loadTestsFromName(__name__)
if __name__ == '__main__':
unittest.main(defaultTest='suite')