diff --git a/beets/art.py b/beets/art.py index 4f873fc608..979a6f7226 100644 --- a/beets/art.py +++ b/beets/art.py @@ -22,7 +22,6 @@ import subprocess import platform from tempfile import NamedTemporaryFile -import imghdr import os from beets.util import displayable_path, syspath, bytestring_path @@ -194,7 +193,7 @@ def extract(log, outpath, item): return # Add an extension to the filename. - ext = imghdr.what(None, h=art) + ext = mediafile.image_extension(art) if not ext: log.warning(u'Unknown image type in {0}.', displayable_path(item.path)) diff --git a/beets/mediafile.py b/beets/mediafile.py index 6dd7bc8160..784695c1a7 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -59,6 +59,7 @@ from beets import logging from beets.util import displayable_path, syspath, as_string +from beets.util.collections import IdentityFallbackDict import six @@ -81,6 +82,8 @@ 'aiff': 'AIFF', } +PREFERRED_IMAGE_EXTENSIONS = IdentityFallbackDict({'jpeg': 'jpg'}) + # Exceptions. @@ -308,6 +311,17 @@ def _sc_encode(gain, peak): # Cover art and other images. +def _imghdr_what_wrapper(data): + """A wrapper around imghdr.what to account for jpeg files that can only be + identified as such using their magic bytes + See #1545 + See https://github.com/file/file/blob/master/magic/Magdir/jpeg#L12 + """ + # imghdr.what returns none for jpegs with only the magic bytes, so + # _wider_test_jpeg is run in that case. It still returns None if it didn't + # match such a jpeg file. + return imghdr.what(None, h=data) or _wider_test_jpeg(data) + def _wider_test_jpeg(data): """Test for a jpeg file following the UNIX file implementation which @@ -318,14 +332,14 @@ def _wider_test_jpeg(data): return 'jpeg' -def _image_mime_type(data): +def image_mime_type(data): """Return the MIME type of the image data (a bytestring). """ # This checks for a jpeg file with only the magic bytes (unrecognized by # imghdr.what). imghdr.what returns none for that type of file, so # _wider_test_jpeg is run in that case. It still returns None if it didn't # match such a jpeg file. - kind = imghdr.what(None, h=data) or _wider_test_jpeg(data) + kind = _imghdr_what_wrapper(data) if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: return 'image/{0}'.format(kind) elif kind == 'pgm': @@ -340,6 +354,10 @@ def _image_mime_type(data): return 'image/x-{0}'.format(kind) +def image_extension(data): + return PREFERRED_IMAGE_EXTENSIONS[_imghdr_what_wrapper(data)] + + class ImageType(enum.Enum): """Indicates the kind of an `Image` stored in a file's tag. """ @@ -394,7 +412,7 @@ def __init__(self, data, desc=None, type=None): @property def mime_type(self): if self.data: - return _image_mime_type(self.data) + return image_mime_type(self.data) @property def type_index(self): diff --git a/beets/util/collections.py b/beets/util/collections.py new file mode 100644 index 0000000000..6813b95df7 --- /dev/null +++ b/beets/util/collections.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# This file is part of beets. +# Copyright 2016, Adrian Sampson. +# +# 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. +"""Custom collections classes +""" + + +class IdentityFallbackDict(dict): + """A dictionary which is "transparent" (maps keys to themselves) for all + keys not in it. + """ + def __getitem__(self, key): + try: + return dict.__getitem__(self, key) + except KeyError: + return key diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 4db22397ed..27ffa49cb7 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -29,7 +29,7 @@ from beets import ui from beets import util from beets import config -from beets.mediafile import _image_mime_type +from beets.mediafile import image_mime_type from beets.util.artresizer import ArtResizer from beets.util import confit from beets.util import syspath, bytestring_path, py3_path @@ -250,7 +250,7 @@ def fetch_image(self, candidate, extra): # server didn't return enough data, i.e. corrupt image return - real_ct = _image_mime_type(header) + real_ct = image_mime_type(header) if real_ct is None: # detection by file magic failed, fall back to the # server-supplied Content-Type diff --git a/docs/changelog.rst b/docs/changelog.rst index 08be98a5c8..7c03e230d2 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -52,6 +52,12 @@ The are a couple of small new features: And there are a few bug fixes too: +* :doc:`/plugins/embedart`: The plugin now uses ``jpg`` as an extension rather + than ``jpeg``, to ensure consistency with :doc:`plugins/fetchart`. + Thanks to :user:`tweitzel`. :bug:`2254` :bug:`2255` +* :doc:`/plugins/embedart`: The plugin now works for all jpeg files, including + those that are only recognizable by their magic bytes. + :bug:`1545` :bug:`2255` * :doc:`/plugins/web`: The JSON output is no longer pretty-printed (for a space savings). :bug:`2050` * :doc:`/plugins/permissions`: Fix a regression in the previous release where @@ -70,7 +76,7 @@ And there are a few bug fixes too: This is fixed. :bug:`2168` * :doc:`/plugins/embyupdate`: Fixes authentication header problem that caused a problem that it was not possible to get tokens from the Emby API. -* :doc:`/plugins/lyrics`: Search for lyrics using the title part preceding the +* :doc:`/plugins/lyrics`: Search for lyrics using the title part preceding the colon character. :bug:`2206` * Fix a crash when a query contains a date field that is not set for all the items. :bug:`1938` diff --git a/test/rsrc/image-jpeg.mp3 b/test/rsrc/image-jpeg.mp3 new file mode 100644 index 0000000000..3f2d22ffb1 Binary files /dev/null and b/test/rsrc/image-jpeg.mp3 differ diff --git a/test/test_embedart.py b/test/test_embedart.py index 4ba5c34242..0d64b0d9a1 100644 --- a/test/test_embedart.py +++ b/test/test_embedart.py @@ -163,6 +163,17 @@ def test_non_ascii_album_path(self): self.assertExists(os.path.join(albumpath, b'extracted.png')) + def test_extracted_extension(self): + resource_path = os.path.join(_common.RSRC, b'image-jpeg.mp3') + album = self.add_album_fixture() + trackpath = album.items()[0].path + albumpath = album.path + shutil.copy(syspath(resource_path), syspath(trackpath)) + + self.run_command('extractart', '-n', 'extracted') + + self.assertExists(os.path.join(albumpath, b'extracted.jpg')) + @patch('beets.art.subprocess') @patch('beets.art.extract') diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 4e053192a3..0be1776992 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -91,8 +91,7 @@ def test_only_magic_bytes_jpeg(self): with open(magic_bytes_file, 'rb') as f: jpg_data = f.read() self.assertEqual( - beets.mediafile._image_mime_type(jpg_data), - 'image/jpeg') + beets.mediafile._imghdr_what_wrapper(jpg_data), 'jpeg') def test_soundcheck_non_ascii(self): # Make sure we don't crash when the iTunes SoundCheck field contains