From 9c6491a65d7abcc5dcc49ecbb1883e8ab8a43fd8 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 2 Jun 2014 16:04:55 +0200 Subject: [PATCH 1/3] Embedart TestCase --- test/test_embedart.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/test_embedart.py diff --git a/test/test_embedart.py b/test/test_embedart.py new file mode 100644 index 000000000..418ad56d8 --- /dev/null +++ b/test/test_embedart.py @@ -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') From f19fa1567e4a86627eb987575cae6a518e7c81fc Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 2 Jun 2014 17:09:16 +0200 Subject: [PATCH 2/3] Test converter embeds album art --- test/helper.py | 4 ++-- test/test_convert.py | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/test/helper.py b/test/helper.py index 16cfd11d3..aa9c92add 100644 --- a/test/helper.py +++ b/test/helper.py @@ -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 diff --git a/test/test_convert.py b/test/test_convert.py index 4af539923..21de94696 100644 --- a/test/test_convert.py +++ b/test/test_convert.py @@ -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__) From 2813cd26c1d713387e8e0b31d76bbce220ffdca7 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Mon, 2 Jun 2014 17:10:48 +0200 Subject: [PATCH 3/3] Refactor embedart to work at item level Embedding images now triggers the `*_write` plugin events. This allows *beets-check* to update the checksum. See the [beets-check issue][1]. [1]: https://github.com/geigerzaehler/beets-check/issues/7 --- beetsplug/convert.py | 13 ++--- beetsplug/embedart.py | 120 +++++++++++++++++++----------------------- 2 files changed, 56 insertions(+), 77 deletions(-) diff --git a/beetsplug/convert.py b/beetsplug/convert.py index 578c1ae87..9892cc001 100644 --- a/beetsplug/convert.py +++ b/beetsplug/convert.py @@ -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) diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index ffdf70715..0322af6c7 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -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))