diff --git a/beets/mediafile.py b/beets/mediafile.py index dddd8a57a..9b50aaa57 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -868,7 +868,7 @@ class VorbisImageStorageStyle(ListStorageStyle): base64-encoded. Values are `Image` objects. """ formats = ['OggOpus', 'OggTheora', 'OggSpeex', 'OggVorbis', - 'OggFlac', 'APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio'] + 'OggFlac'] def __init__(self): super(VorbisImageStorageStyle, self).__init__( @@ -950,6 +950,74 @@ class FlacImageStorageStyle(ListStorageStyle): mutagen_file.clear_pictures() +class APEv2ImageStorageStyle(ListStorageStyle): + """Store images in APEv2 tags. Values are `Image` objects. + """ + formats = ['APEv2File', 'WavPack', 'Musepack', 'MonkeysAudio', 'OptimFROG'] + + TAG_NAMES = { + ImageType.other: 'Cover Art (other)', + ImageType.icon: 'Cover Art (icon)', + ImageType.other_icon: 'Cover Art (other icon)', + ImageType.front: 'Cover Art (front)', + ImageType.back: 'Cover Art (back)', + ImageType.leaflet: 'Cover Art (leaflet)', + ImageType.media: 'Cover Art (media)', + ImageType.lead_artist: 'Cover Art (lead)', + ImageType.artist: 'Cover Art (artist)', + ImageType.conductor: 'Cover Art (conductor)', + ImageType.group: 'Cover Art (band)', + ImageType.composer: 'Cover Art (composer)', + ImageType.lyricist: 'Cover Art (lyricist)', + ImageType.recording_location: 'Cover Art (studio)', + ImageType.recording_session: 'Cover Art (recording)', + ImageType.performance: 'Cover Art (performance)', + ImageType.screen_capture: 'Cover Art (movie scene)', + ImageType.fish: 'Cover Art (colored fish)', + ImageType.illustration: 'Cover Art (illustration)', + ImageType.artist_logo: 'Cover Art (band logo)', + ImageType.publisher_logo: 'Cover Art (publisher logo)', + } + + def __init__(self): + super(APEv2ImageStorageStyle, self).__init__(key='') + + def fetch(self, mutagen_file): + images = [] + for cover_type, cover_tag in self.TAG_NAMES.items(): + try: + frame = mutagen_file[cover_tag] + text_delimiter_index = frame.value.find('\x00') + comment = frame.value[0:text_delimiter_index] \ + if text_delimiter_index > 0 else None + image_data = frame.value[text_delimiter_index + 1:] + images.append(Image(data=image_data, type=cover_type, + desc=comment)) + except KeyError: + pass + + return images + + def set_list(self, mutagen_file, values): + self.delete(mutagen_file) + + for image in values: + image_type = image.type or ImageType.other + comment = image.desc or '' + image_data = comment + "\x00" + image.data + cover_tag = self.TAG_NAMES[image_type] + mutagen_file[cover_tag] = image_data + + def delete(self, mutagen_file): + """Remove all images from the file. + """ + for cover_tag in self.TAG_NAMES.values(): + try: + del mutagen_file[cover_tag] + except KeyError: + pass + + # MediaField is a descriptor that represents a single logical field. It # aggregates several StorageStyles describing how to access the data for # each file type. @@ -1088,11 +1156,11 @@ class DateField(MediaField): year, month, and day number. Each number is either an integer or None. """ - # Get the underlying data and split on hyphens. + # Get the underlying data and split on hyphens and slashes. datestring = super(DateField, self).__get__(mediafile, None) if isinstance(datestring, basestring): datestring = re.sub(r'[Tt ].*$', '', unicode(datestring)) - items = unicode(datestring).split('-') + items = re.split('[-/]', unicode(datestring)) else: items = [] @@ -1206,6 +1274,7 @@ class ImageListField(MediaField): ASFImageStorageStyle(), VorbisImageStorageStyle(), FlacImageStorageStyle(), + APEv2ImageStorageStyle(), ) def __get__(self, mediafile, _): @@ -1485,6 +1554,7 @@ class MediaFile(object): StorageStyle('DESCRIPTION'), StorageStyle('COMMENT'), ASFStorageStyle('WM/Comments'), + ASFStorageStyle('Description') ) bpm = MediaField( MP3StorageStyle('TBPM'), diff --git a/beetsplug/embedart.py b/beetsplug/embedart.py index 00a6ad969..d4d0fe4af 100644 --- a/beetsplug/embedart.py +++ b/beetsplug/embedart.py @@ -110,7 +110,7 @@ def album_imported(lib, album): def embed_item(item, imagepath, maxwidth=None, itempath=None, - compare_threshold=0, ifempty=False, asalbum=False): + compare_threshold=0, ifempty=False, as_album=False): """Embed an image into the item's media file. """ if compare_threshold: @@ -126,7 +126,7 @@ def embed_item(item, imagepath, maxwidth=None, itempath=None, displayable_path(imagepath) )) return - if maxwidth and not asalbum: + if maxwidth and not as_album: imagepath = resize_image(imagepath, maxwidth) try: @@ -165,7 +165,7 @@ def embed_album(album, maxwidth=None, quiet=False): for item in album.items(): embed_item(item, imagepath, maxwidth, None, config['embedart']['compare_threshold'].get(int), - config['embedart']['ifempty'].get(bool), asalbum=True) + config['embedart']['ifempty'].get(bool), as_album=True) def resize_image(imagepath, maxwidth): diff --git a/beetsplug/web/static/beets.js b/beetsplug/web/static/beets.js index 5df50764a..eedec8a5f 100644 --- a/beetsplug/web/static/beets.js +++ b/beetsplug/web/static/beets.js @@ -146,7 +146,8 @@ var BeetsRouter = Backbone.Router.extend({ "item/query/:query": "itemQuery", }, itemQuery: function(query) { - $.getJSON('/item/query/' + query, function(data) { + var queryURL = query.split(/\s+/).map(encodeURIComponent).join('/'); + $.getJSON('/item/query/' + queryURL, function(data) { var models = _.map( data['results'], function(d) { return new Item(d); } diff --git a/docs/changelog.rst b/docs/changelog.rst index 71bd27bab..1f1221543 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -22,6 +22,8 @@ Features: embed album art when no album art is present. Thanks to kerobaros. * :doc:`/plugins/ftintitle`: The plugin now runs automatically on import. To disable this, unset the ``auto`` config flag. +* Standard cover art in APEv2 metadata is now supported. Thanks to Matthias + Kiefer. :bug:`1042` Fixes: @@ -55,6 +57,15 @@ Fixes: :user:`multikatt`. :bug:`1027`, :bug:`1040` * :doc:`/plugins/play`: Fix a potential crash when the command outputs special characters. :bug:`1041` +* :doc:`/plugins/web`: Typed queries are now treated as separate query + components. :bug:`1045` +* Date tags that use slashes instead of dashes as separators are now + interpreted correctly. And WMA (ASF) files now map the ``comments`` field to + the "Description" tag (in addition to "WM/Comments"). Thanks to Matthias + Kiefer. :bug:`1043` +* :doc:`/plugins/embedart`: Avoid resizing the image multiple times when + embedding into an album. Thanks to :user:`kerobaros`. :bug:`1028`, + :bug:`1036` 1.3.8 (September 17, 2014) diff --git a/test/rsrc/date_with_slashes.ogg b/test/rsrc/date_with_slashes.ogg new file mode 100644 index 000000000..be676115b Binary files /dev/null and b/test/rsrc/date_with_slashes.ogg differ diff --git a/test/rsrc/image.ape b/test/rsrc/image.ape new file mode 100644 index 000000000..f8a559289 Binary files /dev/null and b/test/rsrc/image.ape differ diff --git a/test/rsrc/pure.wma b/test/rsrc/pure.wma new file mode 100644 index 000000000..4dee3f7bf Binary files /dev/null and b/test/rsrc/pure.wma differ diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 56a7bf572..71d0d0971 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -770,6 +770,12 @@ class WMATest(ReadWriteTestBase, ExtendedImageStructureTestMixin, mediafile = MediaFile(mediafile.path) self.assertIn(mediafile.genre, [u'one', u'two']) + def test_read_pure_tags(self): + mediafile = self._mediafile_fixture('pure') + self.assertEqual(mediafile.comments, 'the comments') + self.assertEqual(mediafile.title, 'the title') + self.assertEqual(mediafile.artist, 'the artist') + class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, unittest.TestCase): @@ -807,6 +813,12 @@ class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, mediafile = MediaFile(mediafile.path) self.assertFalse('coverart' in mediafile.mgfile) + def test_date_tag_with_slashes(self): + mediafile = self._mediafile_fixture('date_with_slashes') + self.assertEqual(mediafile.year, 2005) + self.assertEqual(mediafile.month, 6) + self.assertEqual(mediafile.day, 5) + class FlacTest(ReadWriteTestBase, PartialTestMixin, ExtendedImageStructureTestMixin, @@ -822,7 +834,8 @@ class FlacTest(ReadWriteTestBase, PartialTestMixin, } -class ApeTest(ReadWriteTestBase, unittest.TestCase): +class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, + unittest.TestCase): extension = 'ape' audio_properties = { 'length': 1.0,