diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 6db46f8c1..48580a844 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -15,14 +15,16 @@ """Allows beets to embed album art into file metadata.""" import os.path +import tempfile +from mimetypes import guess_extension +import requests + +from beets import art, config, ui from beets.plugins import BeetsPlugin -from beets import ui -from beets.ui import print_, decargs -from beets.util import syspath, normpath, displayable_path, bytestring_path +from beets.ui import decargs, print_ +from beets.util import bytestring_path, displayable_path, normpath, syspath from beets.util.artresizer import ArtResizer -from beets import config -from beets import art def _confirm(objs, album): @@ -75,15 +77,17 @@ class EmbedCoverArtPlugin(BeetsPlugin): def commands(self): # Embed command. - embed_cmd = ui.Subcommand( - 'embedart', help='embed image files into file metadata' - ) - embed_cmd.parser.add_option( - '-f', '--file', metavar='PATH', help='the image file to embed' - ) - embed_cmd.parser.add_option( - "-y", "--yes", action="store_true", help="skip confirmation" - ) + embed_cmd = ui.Subcommand('embedart', + help='embed image files into file metadata') + embed_cmd.parser.add_option('-f', '--file', metavar='PATH', + help='the image file to embed') + + embed_cmd.parser.add_option("-y", "--yes", action="store_true", + help="skip confirmation") + + embed_cmd.parser.add_option('-u', '--url', metavar='URL', + help='the URL of the image file to embed') + maxwidth = self.config['maxwidth'].get(int) quality = self.config['quality'].get(int) compare_threshold = self.config['compare_threshold'].get(int) @@ -107,13 +111,41 @@ class EmbedCoverArtPlugin(BeetsPlugin): art.embed_item(self._log, item, imagepath, maxwidth, None, compare_threshold, ifempty, quality=quality) + elif opts.url: + try: + response = requests.get(opts.url, timeout=5) + response.raise_for_status() + except requests.exceptions.RequestException as e: + self._log.error("{}".format(e)) + return + extension = guess_extension(response.headers + ['Content-Type']) + if extension is None: + self._log.error('Invalid image file') + return + file = f'image{extension}' + tempimg = os.path.join(tempfile.gettempdir(), file) + try: + with open(tempimg, 'wb') as f: + f.write(response.content) + except Exception as e: + self._log.error("Unable to save image: {}".format(e)) + return + items = lib.items(decargs(args)) + # Confirm with user. + if not opts.yes and not _confirm(items, not opts.url): + os.remove(tempimg) + return + for item in items: + art.embed_item(self._log, item, tempimg, maxwidth, + None, compare_threshold, ifempty, + quality=quality) + os.remove(tempimg) else: albums = lib.albums(decargs(args)) - # Confirm with user. if not opts.yes and not _confirm(albums, not opts.file): return - for album in albums: art.embed_album(self._log, album, maxwidth, False, compare_threshold, ifempty, diff --git a/docs/changelog.rst b/docs/changelog.rst index 8d03cd1d0..b7aaa7dff 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -11,10 +11,12 @@ for Python 3.6). New features: +* Added option to specify a URL in the `embedart` plugin. + :bug:`83` * :ref:`list-cmd` `singleton:true` queries have been made faster * :ref:`list-cmd` `singleton:1` and `singleton:0` can now alternatively be used in queries, same as `comp` * --from-logfile now parses log files using a UTF-8 encoding in `beets/beets/ui/commands.py`. - :bug:`4693` + :bug:`4693` * :doc:`/plugins/bareasc` lookups have been made faster * :ref:`list-cmd` lookups using the pattern operator `::` have been made faster * Added additional error handling for `spotify` plugin. diff --git a/docs/plugins/embedart.rst b/docs/plugins/embedart.rst index defd3fa4b..f87fabc71 100644 --- a/docs/plugins/embedart.rst +++ b/docs/plugins/embedart.rst @@ -85,12 +85,15 @@ Manually Embedding and Extracting Art The ``embedart`` plugin provides a couple of commands for manually managing embedded album art: -* ``beet embedart [-f IMAGE] QUERY``: embed images into the every track on the +* ``beet embedart [-f IMAGE] QUERY``: embed images in every track of the albums matching the query. If the ``-f`` (``--file``) option is given, then use a specific image file from the filesystem; otherwise, each album embeds its own currently associated album art. The command prompts for confirmation before making the change unless you specify the ``-y`` (``--yes``) option. +* ``beet embedart [-u IMAGE_URL] QUERY``: embed image specified in the URL + into every track of the albums matching the query. The ``-u`` (``--url``) option can be used to specify the URL of the image to be used. The command prompts for confirmation before making the change unless you specify the ``-y`` (``--yes``) option. + * ``beet extractart [-a] [-n FILE] QUERY``: extracts the images for all albums matching the query. The images are placed inside the album folder. You can specify the destination file name using the ``-n`` option, but leave off the diff --git a/test/test_embedart.py b/test/test_embedart.py index 6cf5bfa56..1797c790a 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -28,6 +28,7 @@ from beets import config, logging, ui from beets.util import syspath, displayable_path from beets.util.artresizer import ArtResizer from beets import art +from test.test_art import FetchImageHelper def require_artresizer_compare(test): @@ -42,7 +43,7 @@ def require_artresizer_compare(test): return wrapper -class EmbedartCliTest(_common.TestCase, TestHelper): +class EmbedartCliTest(TestHelper, FetchImageHelper): small_artpath = os.path.join(_common.RSRC, b'image-2x3.jpg') abbey_artpath = os.path.join(_common.RSRC, b'abbey.jpg') @@ -216,6 +217,40 @@ class EmbedartCliTest(_common.TestCase, TestHelper): mediafile = MediaFile(syspath(item.path)) self.assertEqual(mediafile.images[0].data, self.image_data) + def test_embed_art_from_url_with_yes_input(self): + self._setup_data() + album = self.add_album_fixture() + item = album.items()[0] + self.mock_response('http://example.com/test.jpg', 'image/jpeg') + self.io.addinput('y') + self.run_command('embedart', '-u', 'http://example.com/test.jpg') + mediafile = MediaFile(syspath(item.path)) + self.assertEqual( + mediafile.images[0].data, + self.IMAGEHEADER.get('image/jpeg').ljust(32, b'\x00') + ) + + def test_embed_art_from_url_png(self): + self._setup_data() + album = self.add_album_fixture() + item = album.items()[0] + self.mock_response('http://example.com/test.png', 'image/png') + self.run_command('embedart', '-y', '-u', 'http://example.com/test.png') + mediafile = MediaFile(syspath(item.path)) + self.assertEqual( + mediafile.images[0].data, + self.IMAGEHEADER.get('image/png').ljust(32, b'\x00') + ) + + def test_embed_art_from_url_not_image(self): + self._setup_data() + album = self.add_album_fixture() + item = album.items()[0] + self.mock_response('http://example.com/test.html', 'text/html') + self.run_command('embedart', '-y', '-u', 'http://example.com/test.txt') + mediafile = MediaFile(syspath(item.path)) + self.assertFalse(mediafile.images) + class DummyArtResizer(ArtResizer): """An `ArtResizer` which pretends that ImageMagick is available, and has