From 80eded77b10a5fcf855114fb9120d21fcb94e089 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 8 Mar 2014 16:12:24 +0100 Subject: [PATCH 01/18] Add API tests for images in tags --- test/test_mediafile.py | 108 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 107 insertions(+), 1 deletion(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 85b235f7e..6380da856 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -23,7 +23,7 @@ import time import _common from _common import unittest -from beets.mediafile import MediaFile +from beets.mediafile import MediaFile, TagImage class ArtTestMixin(object): @@ -63,6 +63,112 @@ class ArtTestMixin(object): self.assertEqual(mediafile.art, self.jpg_data) +class ImageStructureTestMixin(object): + """Test reading and writing multiple image tags. + + The tests use the `image` media file fixture. The tags of these files + include two images, on in the PNG format, the other in JPEG format. If + the tag format supports it they also include additional metadata. + """ + + def test_read_image_structures(self): + mediafile = self._mediafile_fixture('image') + + self.assertEqual(len(mediafile.images), 2) + + image = mediafile.images[0] + self.assertEqual(image.data, self.png_data) + self.assertEqual(image.mime_type, 'image/png') + self.assertExtendedImageAttributes(image, desc='album cover', + type='front') + + image = mediafile.images[1] + self.assertEqual(image.data, self.jpg_data) + self.assertEqual(image.mime_type, 'image/jpg') + self.assertExtendedImageAttributes(image, desc='the artist', + type='performer') + + def test_set_image_structure(self): + mediafile = self._mediafile_fixture('empty') + image = TagImage(data=self.png_data, desc='album cover', type=TagImage.FRONT) + mediafile.images = [image] + mediafile.save() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(len(mediafile.images), 1) + + image = mediafile.images[0] + self.assertEqual(image.data, self.png_data) + self.assertEqual(image.mime_type, 'image/png') + self.assertExtendedImageAttributes(image, desc='album cover', + type='front') + + def test_add_image_structure(self): + mediafile = self._mediafile_fixture('image') + self.assertEqual(len(mediafile.images), 2) + + image = TagImage(data=self.png_data, desc='the composer', + type=TagImage.COMPOSER) + mediafile.images += image + mediafile.write() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(len(mediafile.images), 3) + self.assertExtendedImageAttributes(mediafile.images[2], + desc='another cover', type='composer') + + def test_mutate_image_structure(self): + mediafile = self._mediafile_fixture('image') + self.assertEqual(len(mediafile.images), 2) + + image = mediafile.images[0] + self.assertEqual(image.data, self.png_data) + self.assertEqual(image.mime_type, 'image/png') + self.assertExtendedImageAttributes(image, desc='album cover', + type='front') + + image.data = self.jpg_data + image.desc = 'new description' + image.type = TagImage.COMPOSER + mediafile.write() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(len(mediafile.images), 2) + + image = mediafile.images[0] + self.assertEqual(image.data, self.jpg_data) + self.assertEqual(image.mime_type, 'image/jpeg') + self.assertExtendedImageAttributes(image, desc='new description', + type='composer') + + def test_delete_image_structure(self): + mediafile = self._mediafile_fixture('image') + self.assertEqual(len(mediafile.images), 2) + + del mediafile.images[0] + mediafile.write() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(len(mediafile.images), 1) + self.assertEqual(image.data, self.png_data) + self.assertEqual(image.mime_type, 'image/jpg') + self.assertExtendedImageAttributes(image, desc='the artist', + type='performer') + + def assertExtendedImageAttributes(image, **kwargs): + """Ignore extended image attributes in the base tests. + """ + pass + + +class ExtendedImageStructureTestMixin(ImageStructureTestMixin): + """Checks for additional attributes in the image structure.""" + + def assertExtendedImageAttributes(image, desc, type): + self.assertEqual(image.desc, desc) + self.assertEqual(image.type, type) + + # TODO include this in ReadWriteTestBase if implemented class LazySaveTestMixin(object): """Mediafile should only write changes when tags have changed From c9fc36b02e8aa7282b8b1fe133566f092f8260d3 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 8 Mar 2014 17:03:16 +0100 Subject: [PATCH 02/18] Fix mediafile image tests --- test/test_mediafile.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 6380da856..e77bfa40b 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -80,17 +80,18 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type='front') + type=TagImage.TYPES.front) image = mediafile.images[1] self.assertEqual(image.data, self.jpg_data) - self.assertEqual(image.mime_type, 'image/jpg') + self.assertEqual(image.mime_type, 'image/jpeg') self.assertExtendedImageAttributes(image, desc='the artist', - type='performer') + type=TagImage.TYPES.artist) def test_set_image_structure(self): mediafile = self._mediafile_fixture('empty') - image = TagImage(data=self.png_data, desc='album cover', type=TagImage.FRONT) + image = TagImage(data=self.png_data, desc='album cover', + type=TagImage.TYPES.front) mediafile.images = [image] mediafile.save() @@ -101,22 +102,23 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type='front') + type=TagImage.TYPES.front) def test_add_image_structure(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) image = TagImage(data=self.png_data, desc='the composer', - type=TagImage.COMPOSER) - mediafile.images += image - mediafile.write() + type=TagImage.TYPES.composer) + mediafile.images += [image] + mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) self.assertExtendedImageAttributes(mediafile.images[2], desc='another cover', type='composer') + @unittest.skip('editing list by reference is not implemented yet') def test_mutate_image_structure(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) @@ -125,12 +127,12 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type='front') + type=TagImage.TYPES.front) image.data = self.jpg_data image.desc = 'new description' image.type = TagImage.COMPOSER - mediafile.write() + mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 2) @@ -139,14 +141,15 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.jpg_data) self.assertEqual(image.mime_type, 'image/jpeg') self.assertExtendedImageAttributes(image, desc='new description', - type='composer') + type=TagImage.TYPES.composer) + @unittest.skip('editing list by reference is not implemented yet') def test_delete_image_structure(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) del mediafile.images[0] - mediafile.write() + mediafile.save() mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 1) @@ -155,7 +158,7 @@ class ImageStructureTestMixin(object): self.assertExtendedImageAttributes(image, desc='the artist', type='performer') - def assertExtendedImageAttributes(image, **kwargs): + def assertExtendedImageAttributes(self, image, **kwargs): """Ignore extended image attributes in the base tests. """ pass @@ -164,7 +167,7 @@ class ImageStructureTestMixin(object): class ExtendedImageStructureTestMixin(ImageStructureTestMixin): """Checks for additional attributes in the image structure.""" - def assertExtendedImageAttributes(image, desc, type): + def assertExtendedImageAttributes(self, image, desc=None, type=None): self.assertEqual(image.desc, desc) self.assertEqual(image.type, type) @@ -545,7 +548,8 @@ class GenreListTestMixin(object): class MP3Test(ReadWriteTestBase, PartialTestMixin, - GenreListTestMixin, unittest.TestCase): + GenreListTestMixin, ExtendedImageStructureTestMixin, + unittest.TestCase): extension = 'mp3' audio_properties = { 'length': 1.0, From a9257ae57b5765c561b6c1dfaae33b26b3d3f542 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 8 Mar 2014 17:03:43 +0100 Subject: [PATCH 03/18] Add image mediafile fixture --- test/rsrc/image.mp3 | Bin 0 -> 10452 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/rsrc/image.mp3 diff --git a/test/rsrc/image.mp3 b/test/rsrc/image.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..c15153729c4dda93ce1c1a6327ddf7b8c6c5577c GIT binary patch literal 10452 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 e;m-a5MGV|{JdFHTQ3Xqf_qy literal 0 HcmV?d00001 From c5c87ac46cc4afb38346bd9aba5f974802d69e3c Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 8 Mar 2014 17:27:01 +0100 Subject: [PATCH 04/18] Implement extended image lists for mediafiles. Makes the test of 80eded77b10a5fcf855114fb9120d21fcb94e089 work. --- beets/mediafile.py | 145 ++++++++++++++++++++++++++++++++++------- test/test_mediafile.py | 2 +- 2 files changed, 124 insertions(+), 23 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index d60d50550..6e78d0583 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -250,6 +250,16 @@ def _sc_encode(gain, peak): return (u' %08X' * 10) % values +def _image_mime_type(data): + """Return the MIME type (either image/png or image/jpeg) of the + image data (a bytestring). + """ + kind = imghdr.what(None, h=data) + if kind == 'png': + return 'image/png' + else: + # Currently just fall back to JPEG. + return 'image/jpeg' # StorageStyle classes describe strategies for accessing values in # Mutagen file objects. @@ -637,23 +647,28 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): self.as_type = str def fetch(self, mutagen_file): - try: - frames = mutagen_file.tags.getall(self.key) - return [frame.data for frame in frames] - except IndexError: - return None + frames = mutagen_file.tags.getall(self.key) + images = [] + for frame in mutagen_file.tags.getall(self.key): + images.append(TagImage(data=frame.data, desc=frame.desc, + type=TagImage.TYPES[frame.type])) + return images - def store(self, mutagen_file, images): - image = images[0] - frame = mutagen.id3.APIC( - encoding=3, - type=3, # FrontCover - mime=ImageField._mime(image), - desc=u'', - data=image - ) - mutagen_file.tags.setall(self.key, [frame]) + def store(self, mutagen_file, frames): + mutagen_file.tags.setall(self.key, frames) + def serialize(self, image): + assert isinstance(image, TagImage) + frame = mutagen.id3.Frames[self.key]() + frame.data = image.data + frame.mime = image.mime_type + frame.desc = (image.desc or u'').encode('utf8') + frame.encoding = 3 # UTF-8 encoding of desc + if image.type: + frame.type = list(TagImage.TYPES).index(image.type) + else: + frame.type = 0 + return frame class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP3DescStorageStyle): @@ -688,7 +703,7 @@ class ASFImageStorageStyle(ListStorageStyle): for image in images: pic = mutagen.asf.ASFByteArrayAttribute() - pic.value = _pack_asf_image(ImageField._mime(image), image) + pic.value = _pack_asf_image(_image_mime_type(image), image) mutagen_file['WM/Picture'] = [pic] @@ -730,7 +745,7 @@ class VorbisImageStorageStyle(ListStorageStyle): if image_data is not None: pic = mutagen.flac.Picture() pic.data = image_data - pic.mime = ImageField._mime(image_data) + pic.mime = _image_mime_type(image_data) mutagen_file['metadata_block_picture'] = [ base64.b64encode(pic.write()) ] @@ -758,7 +773,7 @@ class FlacImageStorageStyle(ListStorageStyle): pic = mutagen.flac.Picture() pic.data = image pic.type = 3 # front cover - pic.mime = ImageField._mime(image) + pic.mime = _image_mime_type(image) mutagen_file.add_picture(pic) @@ -916,15 +931,17 @@ class DateItemField(MediaField): self.date_field._set_date_tuple(mediafile, *items) -class ImageField(MediaField): +class CoverArtField(MediaField): """A descriptor providing access to a file's embedded album art. Holds a bytestring reflecting the image data. The image should either be a JPEG or a PNG for cross-format compatibility. It's probably a bad idea to use anything but these two formats. """ + # TODO make this into shim when ImageField is implemented for all + # formats. def __init__(self): - super(ImageField, self).__init__( + super(CoverArtField, self).__init__( MP3ImageStorageStyle(), MP4ImageStorageStyle(), ASFImageStorageStyle(), @@ -946,10 +963,21 @@ class ImageField(MediaField): return 'image/jpeg' def __get__(self, mediafile, _): + if mediafile.type == 'mp3': + try: + return mediafile.images[0].data + except IndexError: + return None for style in self.styles(mediafile): return style.get(mediafile.mgfile) def __set__(self, mediafile, data): + if mediafile.type == 'mp3': + if data: + mediafile.images = [TagImage(data=data)] + else: + mediafile.images = [] + return if data is not None: if not isinstance(data, str): raise ValueError('value must be a byte string or None') @@ -957,6 +985,76 @@ class ImageField(MediaField): style.set(mediafile.mgfile, data) +class ImageListField(MediaField): + + def __init__(self): + super(ImageListField, self).__init__( + MP3ImageStorageStyle(), + MP4ImageStorageStyle(), + ASFImageStorageStyle(), + VorbisImageStorageStyle(), + FlacImageStorageStyle(), + out_type=str, + ) + + def __get__(self, mediafile, _): + images = [] + for style in self.styles(mediafile): + images.extend(style.get_list(mediafile.mgfile)) + return images + + def __set__(self, mediafile, images): + for style in self.styles(mediafile): + style.set_list(mediafile.mgfile, images) + + +class TagImage(object): + """Strucuture representing image data and metadata that can be + stored and retrieved from tags. + + The structure has four properties. + * ``data`` The binary data of the image + * ``desc`` An optional descritpion of the image + * ``type`` A string denoting the type in relation to the music. + Must be one of the ``TYPES`` enum. + * ``mime_type`` Read-only property that contains the mime type of + the binary data + """ + + TYPES = enum([ + 'other', + 'icon', + 'other icon', + 'front', + 'back', + 'leaflet', + 'media', + 'lead artist', + 'artist', + 'conductor', + 'group', + 'composer', + 'lyricist', + 'recording location', + 'recording session', + 'performance', + 'screen capture', + 'fish', + 'illustration', + 'artist logo', + 'publisher logo', + ], name='TageImage.TYPES') + + def __init__(self, data, desc=None, type=None): + self.data = data + self.desc = desc + self.type = type + + @property + def mime_type(self): + if self.data: + return _image_mime_type(self.data) + # MediaFile is a collection of fields. @@ -1302,8 +1400,11 @@ class MediaFile(object): ASFStorageStyle('beets/Album Artist Credit'), ) - # Album art. - art = ImageField() + # Legacy album art field + art = CoverArtField() + + # Image list + images = ImageListField() # MusicBrainz IDs. mb_trackid = MediaField( diff --git a/test/test_mediafile.py b/test/test_mediafile.py index e77bfa40b..1905ae04c 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -116,7 +116,7 @@ class ImageStructureTestMixin(object): mediafile = MediaFile(mediafile.path) self.assertEqual(len(mediafile.images), 3) self.assertExtendedImageAttributes(mediafile.images[2], - desc='another cover', type='composer') + desc='the composer', type=TagImage.TYPES.composer) @unittest.skip('editing list by reference is not implemented yet') def test_mutate_image_structure(self): From f16c997d9cfb565a97d809e2cfb0e4c034a86a09 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sat, 8 Mar 2014 23:44:54 +0100 Subject: [PATCH 05/18] Add documentation and clean up code --- beets/mediafile.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 6e78d0583..977012b5f 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -641,12 +641,20 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): + """Converts between APIC frames and ``TagImage`` instances. + + The `get_list` method inherited from ``ListStorageStyle`` returns a + list of ``TagImage``s. Similarily the `set_list` method accepts a + list of ``TagImage``s as its ``values`` arguemnt. + """ def __init__(self): super(MP3ImageStorageStyle, self).__init__(key='APIC') self.as_type = str def fetch(self, mutagen_file): + """Return a list of TagImages obtained from all APIC frames. + """ frames = mutagen_file.tags.getall(self.key) images = [] for frame in mutagen_file.tags.getall(self.key): @@ -658,6 +666,8 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): mutagen_file.tags.setall(self.key, frames) def serialize(self, image): + """Return an APIC frame populated with data from ``image``. + """ assert isinstance(image, TagImage) frame = mutagen.id3.Frames[self.key]() frame.data = image.data @@ -670,6 +680,7 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): frame.type = 0 return frame + class MP3SoundCheckStorageStyle(SoundCheckStorageStyleMixin, MP3DescStorageStyle): def __init__(self, index=0, **kwargs): @@ -950,18 +961,6 @@ class CoverArtField(MediaField): out_type=str, ) - @classmethod - def _mime(cls, data): - """Return the MIME type (either image/png or image/jpeg) of the - image data (a bytestring). - """ - kind = imghdr.what(None, h=data) - if kind == 'png': - return 'image/png' - else: - # Currently just fall back to JPEG. - return 'image/jpeg' - def __get__(self, mediafile, _): if mediafile.type == 'mp3': try: @@ -986,15 +985,23 @@ class CoverArtField(MediaField): class ImageListField(MediaField): + """Descriptor to access the list of images embedded in tags. + + The getter returns a list of ``TagImage`` instances obtained from + the tags. The setter accepts a list of ``TagImage`` instances to be + written to the tags. + """ def __init__(self): + # The storage styles used here must implement the + # `ListStorageStyle` interface and get and set lists of + # `TagImage`s. super(ImageListField, self).__init__( MP3ImageStorageStyle(), MP4ImageStorageStyle(), ASFImageStorageStyle(), VorbisImageStorageStyle(), FlacImageStorageStyle(), - out_type=str, ) def __get__(self, mediafile, _): From ae18ea52cfbcbcadc092183fc5f157b8c821bd4e Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 00:07:29 +0100 Subject: [PATCH 06/18] Add flac fixture with images --- test/rsrc/image.flac | Bin 0 -> 21890 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/rsrc/image.flac diff --git a/test/rsrc/image.flac b/test/rsrc/image.flac new file mode 100644 index 0000000000000000000000000000000000000000..0834828488b4c974a00a8547adeebe6740693872 GIT binary patch literal 21890 zcmeHueNY^CdL{y~hGB&G7~@5m(rRYsqnkw=(oJ%-4q5CB-q1~JX!S_DLgl*4IP-y= zY{JeciE(M|;7re;ICUkuTZATUq}C0C<@H`ENV?m-OVw=#XIIm14J`KN>#9;qjv3_q zkx~$MS-E6!Pb;}xRsOoV{BwT@8A*tyyMOQR{XOsVJn!oy9}Cyj)p^_M+Un}|ch&9x zd&dVu59;dPANKt12S4+hzy9^a?;QHG?q9w6_tT%Nt6RgB4ft$6|BYBG@w1b^mckb; zv0r`VH@&e{e~N{Ne$}AqW0`fE^hY@#oWPn(W_yVTS!v z{Lwr)GM>S$I_=MWbql)v)6ZRCPrmfrU)4KWoo(E{E)DwkN5UYG&D4H;`6QgI_75@8h#|b z8N>y3>`Rg1{}B46w?0=_|1Q34!l&aOmh|dm!d{c(!bIXoEPdg8<^sO@XG^NTu=nSC zKdEcuzRG^JZr{FrbtCvyxA))b!gYK9(huBZ-@o)8d*82XZ^Ay+H|%S#t8d@e(7tc) zgSs!&)$MD<+uASuk01N$8}>JT?zyIBM++{v(^gl%uc4uSe?w#A{{6W6Tljw6{`STW z&(D43bDgJR&w0P;qNZn!rZ0y6@QHqm(_*VnW zvGDLqU;V`qmW!S~^R=&^9Y6QW@kBB;asJiJh2Qwi-+FE8_20=#x%|wf-<1{h%G~eI zYYW%^ePOA%{O*nSR&L$C^B@1ycfa?aORFFJ$q#;bw`^{F^zo1X^w0kMFaC1#umAg2 zwYGi#lfU`9ork;s2y2%xqHU99Ao4-hl51xM|{Ub-0uYc43clOeruI&GAVQ>9E zt?Zu`_J7v(mvzpDeb~H)_PP^w@^7Em`-|y1_v-)NxMzPpi@>u8Jd41y2t13xvj{wk zz_SQEi@>u8Jd41y2t13xvj{wkz_SQEi@>u8Jd41y2t13xvj{wkz_SQEi@>u8Jd42p z2NBr&X}PKH%1~Y1ogX%NEF+kk5NZ=*#xJ$_jE-2_mSw4{DidbNk~V0BHPy0K75~7v zdX<#?j+joQ<$OOA=9#d+RUDVC!9}S#{*l9_lA5Mh;>DBB?ZPr81*RUQ*2~q_lo{J( z`rq+8_*;xloSiHbjE?TM)sRGMiGzMeDDQC14{OHpW@G4X+?Yl8l%o=R@Nlr{S4dQI{9fwP5%0*Jg z#=?ua4AveVq^P6Qn{iVLC`8oLh^6 z$@Y>&)WNG{Ffdt6=XAr`?1&|rRZ1YK>5*Z7L#UKfTXS53nhIrSgxUIho_J$ws9?19 zlL|jr(kP`kN=y-5E|pT6^7Ln|!ZwK~s^p*<3(t#}jgAzaNY|o$OU+wX)$&F=6(71; z_0?RO5tiypxlC_mI;?2d&b}VfNll)P4Q*U5PFtqiWVimvz#IGfYGNX23gwV2SgUN4 z8VrbnN=-!)RD1JmGsE&FAElAr=df`GyWmovoD~xRp*T6?CiudWTv}=sYLai~%{f;u zB@2i*C8r#mtK3g7bQ@tF$zwfK>7aAH>I-Mv$|qZ~Sv1|}ryY|9YcDG|&4$u=v%N>W zj0Iyu%VZFtT0ZWS=U=->hMC~~gs&L(&Q}UA(1~gza@w&JL|_h0>R=4NFZ~qixfWFBb~CgTau{OQeXdSS;eQ zu99YVn4$X$%Bp2q*xIyE?JM$yT_&umOq*_$Jle>xBgwTpw;ir>=tx@OXMOim$DRH6 zoGP<7Xp}Y0Jmea*G~TZ^$sEr-E<9wku*Pp2y1|A##4w|6Ej#$aM$}JSpWoBfUp|^+bIn8c*y&_n+tA-bvZB1)_N9RD?SPKbdlYht{Dk?2U zX)4FXXLtjrqG0fwZHDD_lUA?B-gn`>`!?Ikd~r-AQjSDGyg;iht-}n%8Z6&W5>t61 zbz*3_nb%mHXq~KJ8{|CFI-L_X?Nxv1$ncarM*4}d!5W7T_an=`wJ+@9nNZejE1qn1 zZHJ^hk?^SoRgwm#RAsw*b#JznFKC*-ZyR~vG|g^Cx_FYVI#^9k=d~l#CEe^scB|@U z+)@`@%sW9AU#JYrwvqIS&+2s|B~fB7$?xqVI}o@Qhf5376c^JqzSrcs=hMlN?A?5m zGKl1o0uxERuQZVMFeaB4W;mBxzMo9S4YtY{q07tb$=7V4+b0jz)!+HygoiBYC8eLF z08$34X(b)0R!fdd8*Lg-tMt{diqlaIWSFqMZwgLI_fMjEYS`Yig6aOu`M|^`zTSPT zBbRVm+V-AN^4)WUr0KSGFaH4*&9YfmuLdrfd^I2#mcC6S;|no6RvLcLbgKl%Bg3LW zO8Cf;^2F!3q)<#I`KoW(k>m^5;6kC89HlKIBot$;rIJ)`BGpe!4C4UmyTNNwza#xJ zogKQlh6ik!o!;(6majS{mj{qyCU3g+N-`P4W&))n!eR{Y0|<#KWZz1REy+^Q!5wOE z)^?AV!kScR*0acN@|+TS`cW{#=G;5xZ#Z>+tuv(8$3Gm&U z3&;ssBW0;IDd(iXE1gYBKas)DJOnheq0;4wV91`FCTslzXNPIc>tZBoLQLjy5Qb(Z zm#JZm-O(uRSVYb{CLhnli$bBy&bi7u$y6rxkF*VKc8n3j2LLkFfCyMLSiOwMlqBHg zCcD7JKfs=7B}<)cR;^xHBQm=*yK!MbSm;Jh(Fh;XB&->s>{7F#lSJGIWjvj}n}uuR z3*BX20T_B40Sr}|rlNi#MGs>Kmg;rQ zj!r*Gv*ED%j^oh&aCqJ1mmN5axi8>^0#0(L>*-xAmF41P( zjEL!+7)d4qQRG6rV8*YB$wXh(dDToNkx({M*&a06H6|?Qae%jm)~(7Y@w{`JI-Bwb zqK^23nT(z1CYKo)_tSmLzCW6!E|A192o0;^pa>f&GB4vH{8p(f;W*Wj8Plct!p*UT zMy>Z#m$lh8APhtU$bqNyN2OKLFSk<(|H0N_M*d>t3ejUrT}BfKL@F8QsfQX$Oz$;l7pv=ZF_aq}J&DxS@ozVa1Q)QtSpKTstfD zmQ0sk8Ib~Se$pgKOq&)C?r6a^LmB#+@7V}f{|7e0{owl^e=z=ueazjPk)+6@By^L0 zDr$!v@U65l3vnYp8$hP9vWL+vi)KC{B`SSBPfWyTx(7tT3k+>>G)nazS#nle+gpv};NI=FOSNaTCq!+!Ac|@D&cbyc&;kh{!d@0aT($-#A(bqie6xi#xEJ3q7Qs_a0-3+bT9L)Y8iHsQ=1Ro?(H!~Zq^;&+=!1>pdd2uSs`e{m>5F^8_F6xcp z{^NbcNQ~vpkk88l5*L*afC|Kl-VHopqd0r|5}AA#GtYF#mo-9#LN zearR17P^dbU@bf14`-Vfg<7ib5+&dm-@FyNY{)`cT}ZunJ{U^0CUC%63As=}Nr=xn z_00%h>tcCAOgU>&q-E3Pnf3t3&+KEvC{#1tMAu%0wpoj#IBXO?U6( zFL#p1J#AHIF(RhamOxslu*p>4M?suusa~OLfRN^pht|k>VRLAOa}5aPYZrG{31Ksm z6DnK?g>tj466;7{Kjk;#p;$+Xk}noBLznsF+NMj`r8|WJJp}oC3KM*`jNX%!>KIp?kX-Sqhvd*SwuX5=0nc6NFNdgOA14h-p={g_=06vUuKQVm*zz=m(K3CIJ)&+0E(uM;w z!5UcP}Mx3n==k_R!lgMU@af zteiKL`Ii@wZ1x#RMi1CBs9@bs3Zw?eFKmVLhM5%OAt=-GP6;BQA184pzDy1xO|$GE zjaW^E6n(v5x)hLWVH*-=+S)N2H$!Djy^2iyEFc9gywkUHp$xWfSjBXHUW__oTgSTB z3=|Pdr^9d(d|?U7U2V$F>~DiCbj0rkdB7*7=~Z8m4PCA!)5BzIGMVzXcsMu4EwqsK4m`@Rz9a$*!+F95SN*~lJ z5bCU|@mc;VsU7QHZ0yAmV!>+YaFCQUK4@;p4+7fk?65DadqXM+jIM)7IGM#a#*OfN zRBCaq)0dZh-_uFOb~uDes!DO)3KUb;fUl77*Xxv)ll%=DjyqI^g)K}ulE}bxl$vha zjOo=@tdE|pFJ*1H51bCG*{}|kqrhCStlE_6Dvzyiwrz�pwF0p$mf5TeYaC?WQ+= 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 Ix4&8ZUlO~irvLx| literal 0 HcmV?d00001 From 4e6643a41b57dfd747fb39ccae939efe17892fec Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 00:08:30 +0100 Subject: [PATCH 07/18] Remove picture from full flac fixture --- test/rsrc/full.flac | Bin 21890 -> 21890 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/rsrc/full.flac b/test/rsrc/full.flac index f4c803ed785544bb74fd659dd40662002294db8f..abc18ac300bdc4d525ef3e983f0033022cd0dbed 100644 GIT binary patch delta 28 kcmZo#&DgY>al=F=mPQ8I+nWzEt(TpAfk|g`fr5}J0H22ndH?_b delta 33 pcmZo#&DgY>al=F=7B&V3g~ Date: Sun, 9 Mar 2014 00:11:46 +0100 Subject: [PATCH 08/18] Implement ImageListField for FLAC --- beets/mediafile.py | 54 ++++++++++++++++++++++++++---------------- test/test_mediafile.py | 3 ++- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 977012b5f..e23bd64e9 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -659,7 +659,7 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): images = [] for frame in mutagen_file.tags.getall(self.key): images.append(TagImage(data=frame.data, desc=frame.desc, - type=TagImage.TYPES[frame.type])) + type=frame.type)) return images def store(self, mutagen_file, frames): @@ -674,10 +674,7 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): frame.mime = image.mime_type frame.desc = (image.desc or u'').encode('utf8') frame.encoding = 3 # UTF-8 encoding of desc - if image.type: - frame.type = list(TagImage.TYPES).index(image.type) - else: - frame.type = 0 + frame.type = image.type_index or 3 # front cover return frame @@ -763,30 +760,40 @@ class VorbisImageStorageStyle(ListStorageStyle): class FlacImageStorageStyle(ListStorageStyle): + """Converts between ``mutagen.flac.Picture`` and ``TagImage`` instances. + """ formats = ['flac'] def __init__(self): super(FlacImageStorageStyle, self).__init__(key='') - self.as_type = str def fetch(self, mutagen_file): - pictures = mutagen_file.pictures - if pictures: - return [picture.data or None for picture in pictures] - else: - return [] + """Return a list of TagImages stored in the tags. + """ + images = [] + for picture in mutagen_file.pictures: + images.append(TagImage(data=picture.data, desc=picture.desc, + type=picture.type)) + return images - def store(self, mutagen_file, images): + def store(self, mutagen_file, pictures): + """``pictures`` is a list of mutagen.flac.Picture instances. + """ mutagen_file.clear_pictures() - - for image in images: - pic = mutagen.flac.Picture() - pic.data = image - pic.type = 3 # front cover - pic.mime = _image_mime_type(image) + for pic in pictures: mutagen_file.add_picture(pic) + def serialize(self, image): + """Turn a TagImage into a mutagen.flac.Picture. + """ + pic = mutagen.flac.Picture() + pic.data = image.data + pic.type = image.type_index or 3 # Front cover + pic.mime = image.mime_type + pic.desc = image.desc or u'' + return pic + # MediaField is a descriptor that represents a single logical field. It @@ -962,7 +969,7 @@ class CoverArtField(MediaField): ) def __get__(self, mediafile, _): - if mediafile.type == 'mp3': + if mediafile.type in ['mp3', 'flac']: try: return mediafile.images[0].data except IndexError: @@ -971,7 +978,7 @@ class CoverArtField(MediaField): return style.get(mediafile.mgfile) def __set__(self, mediafile, data): - if mediafile.type == 'mp3': + if mediafile.type in ['mp3', 'flac']: if data: mediafile.images = [TagImage(data=data)] else: @@ -1055,6 +1062,8 @@ class TagImage(object): def __init__(self, data, desc=None, type=None): self.data = data self.desc = desc + if isinstance(type, int): + type = self.TYPES[type] self.type = type @property @@ -1062,6 +1071,11 @@ class TagImage(object): if self.data: return _image_mime_type(self.data) + @property + def type_index(self): + if self.type is None: + return None + return list(self.TYPES).index(self.type) # MediaFile is a collection of fields. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 1905ae04c..6cea66745 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -624,7 +624,8 @@ class OggTest(ReadWriteTestBase, GenreListTestMixin, unittest.TestCase): self.assertEqual(mediafile.mgfile['YEAR'], [u'2000']) class FlacTest(ReadWriteTestBase, PartialTestMixin, - GenreListTestMixin, unittest.TestCase): + GenreListTestMixin, ExtendedImageStructureTestMixin, + unittest.TestCase): extension = 'flac' audio_properties = { 'length': 1.0, From 806d3cc6e9a596f0d2de83f14a4b451067e2b41b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 00:56:44 +0100 Subject: [PATCH 09/18] Implement ImageListField for Vorbis comments --- beets/mediafile.py | 56 +++++++++++++++++++++-------------------- test/rsrc/image.ogg | Bin 0 -> 9786 bytes test/test_mediafile.py | 3 ++- 3 files changed, 31 insertions(+), 28 deletions(-) create mode 100644 test/rsrc/image.ogg diff --git a/beets/mediafile.py b/beets/mediafile.py index e23bd64e9..106e0afc6 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -720,43 +720,45 @@ class VorbisImageStorageStyle(ListStorageStyle): formats = ['opus', 'ogg', 'ape', 'wv', 'mpc'] def __init__(self): - super(VorbisImageStorageStyle, self).__init__(key='') + super(VorbisImageStorageStyle, self).__init__( + key='metadata_block_picture') self.as_type = str def fetch(self, mutagen_file): + images = [] if 'metadata_block_picture' not in mutagen_file: # Try legacy COVERART tags. - if 'coverart' in mutagen_file and mutagen_file['coverart']: - return base64.b64decode(mutagen_file['coverart'][0]) - return [] - - pics = [] + if 'coverart' in mutagen_file: + for data in mutagen_file['coverart']: + images.append(TagImage(base64.b64decode(data))) + return images for data in mutagen_file["metadata_block_picture"]: try: - pics.append(mutagen.flac.Picture(base64.b64decode(data)).data) + pic = mutagen.flac.Picture(base64.b64decode(data)) except (TypeError, AttributeError): - pass - return pics + continue + images.append(TagImage(data=pic.data, desc=pic.desc, + type=pic.type)) + return images def store(self, mutagen_file, image_data): # Strip all art, including legacy COVERART. - if 'metadata_block_picture' in mutagen_file: - if 'metadata_block_picture' in mutagen_file: - del mutagen_file['metadata_block_picture'] - if 'coverart' in mutagen_file: - del mutagen_file['coverart'] - if 'coverartmime' in mutagen_file: - del mutagen_file['coverartmime'] + if 'coverart' in mutagen_file: + del mutagen_file['coverart'] + if 'coverartmime' in mutagen_file: + del mutagen_file['coverartmime'] + super(VorbisImageStorageStyle, self).store(mutagen_file, image_data) - image_data = image_data[0] - # Add new art if provided. - if image_data is not None: - pic = mutagen.flac.Picture() - pic.data = image_data - pic.mime = _image_mime_type(image_data) - mutagen_file['metadata_block_picture'] = [ - base64.b64encode(pic.write()) - ] + + def serialize(self, image): + """Turn a TagImage into a base64 encoded FLAC picture block. + """ + pic = mutagen.flac.Picture() + pic.data = image.data + pic.type = image.type_index or 3 # Front cover + pic.mime = image.mime_type + pic.desc = image.desc or u'' + return base64.b64encode(pic.write()) class FlacImageStorageStyle(ListStorageStyle): @@ -969,7 +971,7 @@ class CoverArtField(MediaField): ) def __get__(self, mediafile, _): - if mediafile.type in ['mp3', 'flac']: + if mediafile.type in ['mp3', 'flac'] + VorbisImageStorageStyle.formats: try: return mediafile.images[0].data except IndexError: @@ -978,7 +980,7 @@ class CoverArtField(MediaField): return style.get(mediafile.mgfile) def __set__(self, mediafile, data): - if mediafile.type in ['mp3', 'flac']: + if mediafile.type in ['mp3', 'flac'] + VorbisImageStorageStyle.formats: if data: mediafile.images = [TagImage(data=data)] else: diff --git a/test/rsrc/image.ogg b/test/rsrc/image.ogg new file mode 100644 index 0000000000000000000000000000000000000000..4852448e2767ac1895f52cbb0d5a00ef6bce757d GIT binary patch literal 9786 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 Date: Sun, 9 Mar 2014 01:48:56 +0100 Subject: [PATCH 10/18] Implement ImageListField for WMA --- beets/mediafile.py | 24 +++++++++++------------- test/rsrc/image.wma | Bin 0 -> 24561 bytes test/test_mediafile.py | 9 +++++++-- 3 files changed, 18 insertions(+), 15 deletions(-) create mode 100644 test/rsrc/image.wma diff --git a/beets/mediafile.py b/beets/mediafile.py index 106e0afc6..ae97e46cb 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -691,7 +691,6 @@ class ASFImageStorageStyle(ListStorageStyle): def __init__(self): super(ASFImageStorageStyle, self).__init__(key='WM/Picture') - self.as_type = str def fetch(self, mutagen_file): if 'WM/Picture' not in mutagen_file: @@ -700,19 +699,18 @@ class ASFImageStorageStyle(ListStorageStyle): pictures = [] for picture in mutagen_file['WM/Picture']: try: - pictures.append(_unpack_asf_image(picture.value)[1]) + mime, data, type, desc = _unpack_asf_image(picture.value) except: - pass + continue + pictures.append(TagImage(data, desc=desc, type=type)) return pictures - def store(self, mutagen_file, images): - if 'WM/Picture' in mutagen_file: - del mutagen_file['WM/Picture'] - - for image in images: - pic = mutagen.asf.ASFByteArrayAttribute() - pic.value = _pack_asf_image(_image_mime_type(image), image) - mutagen_file['WM/Picture'] = [pic] + def serialize(self, image): + pic = mutagen.asf.ASFByteArrayAttribute() + pic.value = _pack_asf_image(image.mime_type, image.data, + type=image.type_index or 3, + description=image.desc or u'') + return pic class VorbisImageStorageStyle(ListStorageStyle): @@ -971,7 +969,7 @@ class CoverArtField(MediaField): ) def __get__(self, mediafile, _): - if mediafile.type in ['mp3', 'flac'] + VorbisImageStorageStyle.formats: + if mediafile.type in ['mp3', 'flac', 'asf'] + VorbisImageStorageStyle.formats: try: return mediafile.images[0].data except IndexError: @@ -980,7 +978,7 @@ class CoverArtField(MediaField): return style.get(mediafile.mgfile) def __set__(self, mediafile, data): - if mediafile.type in ['mp3', 'flac'] + VorbisImageStorageStyle.formats: + if mediafile.type in ['mp3', 'flac', 'asf'] + VorbisImageStorageStyle.formats: if data: mediafile.images = [TagImage(data=data)] else: diff --git a/test/rsrc/image.wma b/test/rsrc/image.wma new file mode 100644 index 0000000000000000000000000000000000000000..2158978f3e0e5543727650324f4ecc4ac165bae5 GIT binary patch literal 24561 zcmeFY3p`s{-!Hxs35iQZgrYSGO*BDDmA2D%lqMb6flXP(RZKIi=Z?|aVYJfHJ<*8c3Z*Iu{1 z*4p3o+rQhkHoJD@%>FK&f=56Ju=no4pW(pgpbEwisN_lMsU5{#I%l2(4VpNQz@Xm? z|3lpD{zn$izVB(}CCGB?y z@6TaFey^v~(_31%oImz^zA9h}mb|_*cl#&q_YA>-l2eZJE*(a{R|aG8>NSq%#P^sI znz8Oqr$e5IKl=Ioq7OiTU%^^m1FnD-zy^YW03ZrD0vrYc!CaN)AqE3sfIn~m%&-Cu z0pVZ@0Phb1d%PG5dQj5uOw(~ zeYo#~qk?cdn6B)v<5#Q%E9o&EX`YA&0SihcMMXtL7d-h;;LBAiD)$oO)`ArPrqfQx zt8_p9hJSdgmdk};fc|;>e!m=3^SgW0MtgL!J^;Dx-xje4KEE;W?F0J%WTha06pC1W zv46W-A>euieqOIAAP7w1z;!zeE^jdK9T)?E@}Y7VK(V$nw*wFe1Ym#%pd0`k0Ofx- zfSo}8yL~7d038IN0zg$DIsjA$qM`#)HUn6272x1#mk0bufIwAXa8)$~Qe6YAP>lkh z5ET_DOa%^y!NA(b!Eykm14o;zr>W|?`>UBoVr=45C5Scj%0@lTy$|>edk^!G>iR1U z42=k8Yl$RtTf2>$Hh;Or!O>~^4hECu;knD}Ywz7Y0s8{?9|#H#`7Y{6^!G=P{g5C? zJdu=~B1}scXJlrbJD;6*C0|-lSX5kc?dPiM8d>f2x~5yrw_94<+BJC% zU+480&{TndFHc1Wpa5w+itMS$nK$I*h_c}srQ zzf0%jHIN8ajo?040;AR7i2%>v-@ZSH0>?6tXxRh6ODGCh-uQnhgS>>kgo4$moO>sX z*LUk&$p<%+3ghFx&vgKR_7C~XJwkz%;06l@smwl*s(cUb6C6mG0>QgraPQy%8c1`N z(-C0W3)uX*Cn(?#`F(dC1ncYtYaayj4FE^L3Ge_n69-^|gv}F--GH5+TNr`6@AnqV zv?~le0>LLnflmVf4A_qOxt%9i-xGYI%jY&Q!0vOe4q&b;*dG(jr~O^8Fkn4cg9w)H z0^2jeC)2@a)4n`6p0M1~Jk^yV*&i`}D7O(}c zHejAVShjqUL0Y@_a|?g)S$4n%Fy0S*366ewzMkM*qQH6w!PH^!8UTcTo)G}Rz?uMn z{)2Sw_&nR+3H#OuHje+kCx!I{{CrT9O3qJu5{!Is~FYq$KZaQ)oJ5in`j$2i^JwuYfg-uNq6oUwVSS@D@n{)<*7H|7bm- zz_ysD;)f(N@eKfzjky$n!`#nh78%5I1iP5)tqphCsc_O%m?^c)5RM-V`3T>9j#|p{ zq{BUXnSgY?r4z(fj4J?C#iE|Ho*opBxg}4qS7kp$y50;1K{x;xAQdWrpH}?O?s^=m z9)r5xD?8aQJ6YodPlcfgNe8y{vZ=5l=#n(v`B&z*5o<*^sOHpjD}hZa4%eufD>Is0 zaH_dSQrd$ z97=wkNJb^b)QG&&e8omY?QW4MgWsn*6p@SXW#*e;>b;M>5($GOn8Z4r-N_8rgX|86 z7yNH(V-!;#Y7_yt6(+%x*uZa0kSvGXiAZj@{<|kf)%o*DU1u*GZ@#>a(mSyuTN2L8 zxG9A#R$`|3I+|h+}Cv4h-l`j>MYtN17mT$mT43B*DZ{nIv>- zLCKUY75gj{`AqrdW&4(G&KzGTT9ik>YW!yPkyEFnVPAx2{50^QZWvo)Tq|w%#bhSolwt7-D9IvZPlS{8SZd<`Du|{aPi|*XvAdJjK-2B zjMe+T8xdQ*hjQVqB=jtVd+C>eNbclCcY|<~LhPjOg$Ojp#wrOOWVY6LdHP-Ft*5U!GRWt) z2Cz=pI?%`T+e8tXOdO`Y6mIE9MtfMIbaxYo0DywFNJ&K`y4Q+!TY`tUBnruH&1WN6r;hUe_*Ld^a8ITj;knYUSveoS_Q~6~+PF z9JlOIZWN4+lF_6{83nN*kMXpZe43~C^u9ivA(e>BCnhGy^2LIuiz^->WZcYFedwb7 zR8*{dElt)fARO#Dfr52xOyavROzdZ)eH;uPhvZ@wr^-r)Ul zq3|nZ)KaZ{Pk4N5EC1^!!6OC27PYU6iMua(E9zRg+@e#eO46dDj$-j+uE|Ge)Gt~cZQKv)pg%CgMrCOV8n+)lRAKizKZp_BoH+W{DrU{DtIxb}C zgYLpA-(xp^Oy2bI+1R_YvWY-ahwri65zPl$br0VbWDk#E>sn&|Dd7JofPl8CKj?ko z-{2ks=p=tYK?s&!mfpK;ZeB{rLul?hp;}d4;kaVj$7ru#g`)uvC`BXFe0rMGydkP3 zhdMb{wai4MnUNnP0D0mX%Db#Vl>uK5Q%S_D0H;XntA1rMEhQO%jj5}19u9SmW|K$v zOGBBPm=&{hbZCx!yf+BEFj$l+$k#!qAf3#v8m1D19$Kh6<_n}I`HlGG6hElnmP|wt zfI^sUZ+@MhTzjwv(1Fz;;bl*zVWRC~|A)zmPJ&GR>wJf3u66!1>iN^>&RIiMS6K_q z%F>faPW#?E(J!GyfUAQ$OmhPpS8vNvcS6v0h#2)smh-fn&SulCUm>Un0aU(2a4iYC|*T=8J4pk@x!jX%P`CcfoWe<;D^sMATu^mm+4H2_rUXmw+3pV4*v;xycFP8aBFzL@Pkc+zN?}ev+w1J}r9ttr{Oz*L@&`I`Zd3PTI}dHuIL7el&`|y= z!9<$$wA5}Mzj9_s(YwPKadm}NyGES!r}prq_sUSl6r;hn`ljmQ%%M7;#KNXQah;mJ zM{`cs)ab&HG;9`qv?;j5cWcz<T(6LyZ0kSp z<1D4bhS-RyM)ThJ*0O^N@{{P+`bbsL{|faxf46`wL;qhV@IT8R*TDgu0_6|*^1wdx zVSRtMFLl$exA>NaWqT=e_4l68E!VG5F#|T{X)-oJ)=(j42ndHs#Jix+o6X-yqY`1g zS4`o!AhQGRN!9^KQ@?T8TdnjH5F7VnEIPr`F`H-Rmnm#3@DL!OHosEg)@i|7Aq?>? za~3W^_1t=djs{wX$PdmJ<8onWLwsH_tV6Sjd03Q%tx3pGXR z-soc3&-C0<@)|0t(WHnK;e|tgvqJ|)?GAw4EG2v1+1Wl`qKZ*%{8mkTbN#iAxfVbSy!y*te_1wg&EjFW$eoH7`^Reo1fxvyI%E)n6NLJ&3Tn56Z zC#fry8huoVC13fdYil1Hc6)&A(X0p~=(5OfcKJpwEs$)@us29PtG%X^!mDJASefDs zrN>M7n6h@1==NIzub6s#{wlWZ#(}P$qavANpnST_OB570KcaE$-TVlqQ-8@$-i|;> z{CVTDrJav2>Y0SiTNvdG5~rM3cBL^o9Nzrk)kdf6W{fx}Z9koz{v@n*yC4nQd-hHf zHshC`_wR=u>c5GMkS19Ex8rD1cHF0q zU{FGGZ~au*$*onId|Hrys}oi8W9(dJW>R+7K{(&}$f3Y(5# zYDKyGPPR<3x}fAKl%UAVxm&9t<^3-==I;(dpK%YJ=pD?tex>Q4vLANmN2t1$HTp&UVrG6G$APkJ4 z%-u-?y{}Q|PAf>{Igw)&w!q?i5Rz`3Gz~vFPmH6}sEV={;UU-7=?}1^kR_*;sP# zyi|iZVwLnRVlpPC?>dHiZ-;L3dWCB(N67KI^%{lkjfh*5epww=pLa?xHuKiZ3HJCtN({vcO)=LAUokLwbtTQGEHGao?}^)j z9k}z`I{d|tg{n``2d*8xbRpV9%vpLW+hOSAy74#41AVQN2hMi(df%Q2sFx^Th$kjq ztd5#X9m~utHQr2ExOP!3f{u+0#LDkJQ9>SQA!Bkndlale`N`9^WcS;9EOy;;cpkf} zp<>}4@8^N3hNyuz#&!)kw#twCgJj02w_iF$aZ@t2w55f+DF1LqC3Xqq@+KGi?SGVn zc7CnCIdk%4z!Qf+Y_)vQ&}sEiWCW>kz4(SzZ7Ft!R~)6xAq41-?BMZgA@ZqcrQMi3 zqS*O$E3xC-{~7opYJbvuo4><7#&~a5hQE{JLtR3Ac`%&rLbCz(;q313F9Qste@oHS zg^9#{IOl!mH4#+g76H!?VP>9|W|D8R_rB?VYO3lwLUCSMN|C6+2L(m4IO_hEX$}VL z6DANGl&}E~+lAveG6@gd0sgs?CK#SUqo!L!&J(=>FFkgnmUVnV8LXhp5`D^)XU?xk zp;~jCi%`OA!V?J8eHYjXfhQwn7aD2)w>AuZtV(X;yTO1Jrni@^4HyO zYx0c$&ZJ=6v895CtntZRuja>BK_Wz&ET})e%%}qLNFx+NxPP%Sb`b@fw?o8W^QB?7 zTSM+^!^Gqw9WY!XXu_kqI1%!>A^^xB-l^J;wa|zi`|T#VprVFWTDb6u5?8W&*x)a_Dj(+xQ6+2aqM~p5~+@8O(@FeHwsLRZreo79c+{_!AA5FS# zxI?dYf-_e?QaH7w?xuHWZWqQQqJum`wAd4L+-=n;FGpT>`0$xcwC(H~rK!Cj@SA$> z$U^F~6_y2O+cDDM@Qa0{k$kUB#u#o+Z0)!chC5e0vf~T)h!_Xy+@&*MNKTdyDvBymrb2Ix-m^T=_8Yd8M?jiEGL9Et;@+7PKAV~O!%;39*kMU zfAol0EU&*ZJ9g1Aa^*+a7>Mxl)>-ijr2efO#Ib9#PaQVd*x`Y(;;U-PH0+fo3$OX% zj%^IqfU;{Q{7UCQ>8hz{uMc5y&vs^U|7S4&4`q;LFRI`99RI(=y-qbq?@cxKgq%4; zI)7>V*c~5ltq(qaO_wUYweCDwXI9J)k?lncGBPiz@=Q0L%ahO<40S3C>1dCU#tF8;ghqLHRW=h9TaExE zd1Ote0m&$oBY+Mi#NT4Xo@3FyAp!y(2IbMb4N%#KUgwI@*2yNM_@D8H`+`Joj4@E8 zsV5ADAngmvd<2|R{c=M=#@c;*oPX%09(3^BmrmZG8hvQYFqcXY;8mQc{9jG+nSFqZ z<2BwV_2+bsSfUrqLb1%dNxAntPpBJTBN0?}Xf!(AI2%oL)$oTSQ8kyh1mgWUBs$xd zVp=*0@k!&oy~kwWCf1(nV|k@5P6*@@6O6~|&``P%D`!%XaBLZTKKhZNMIbgm@51-1 zrnyBUt9`3W_ANc{_E32-(^7!Jl5pA9kQe zGvUgrG)iYX6r#arw!wL@{eF58a66DD7GL#f#j@q`4Jha^wTV)UH$25nXY1;)21;ce7-E z87ne7EQ~IyX6uT%ndMzW^<_s2!^V`NCp&YBu3{heL7KO|eAfHnINv>Dq4P#3@E_p) zPcxQL|1TDPmp{G(O{B~A59IRbKJ#H`|7Kq{gASs)`jgja>>p=p5)K4@r$QBJUi0Zg zKn?6_P>n!4OS9!+GvBC1(6+WpkdcGaXV_`#pB9*z>+h65=kwE``JR4zku3ESbPmE5 zf=fdQ8eooA3}%TrK`jchF87?Nu$E>Mtm3)a)RD0Yi6}YG6Qre{;JPCS@CnTnjuU$|1DmOi*BHS)bY@z-4k`S(B^1wclwb<7+PyJ6gdUPg*L4AYj<-!} zBVf`=W@%!P^E?YwHMJlZQUrWjViLCEQA+d^f7^RZt5hWX(}y`}xFoN{t6Qkwc)XS2 zFe9xZOXfG{B<8b|I;At(Y4iLvS&N|i)G2qT746OyI+O|p5-WQW8dou&jGI`orHWi$ zc$VaC@53GGh;u&XmRRoI>ip@4ftL-O4C&eD)lQJ^%{i1H*|ORCaf~6Yh0ErtCw9keq-uN zlaelAD?7Kw66BlqsC`vid<8P+`)>AK=p^?-99H+uch7jmxof^E?tngis!|r$)ujLF zh;6+raGfdpFla%sEiZ>|c(i!v1vbt2!uR?rBUVa_9~aa>(*^nEFQkm09xllD#2h6o zWcJ_eefh$wGy!(Dvp%b}bE*HNR8%bv{L-QDX*s2$I(+KLE%88wF{LWD_KnL6tJ;eW ze(N@Mi55er#}C~98v69d*mA{==h@F1_EydfTJ14p{rc{g#Y5m1t$f1k9M{S1PUizP zlVPNRrl%ASG;x z2l~=HVWp)F1Sp&a;TenYCHp`dqDiHTDlIKV_^Tt1$%1wJBbcm){PPEsr6dB5&T#2N zA`p4*1Z%Srf^{lW2ZlpuqZr<09HDy>4#Tv~<3Js|9RiU|I67F!{yEi5D1dp`iSPo^ zxlACNe@zHwi}~j2fhM3w@YZ6~BZN3RKFLwV)0NE(GG*_F^djIk>X6hE-d@MjYp6i3 zsPl_4ym3RSuAl99@z!5wdPBMRXR0R{5GZQz0mpP$!Y^q2u7eE%b3AWFN+~C;#t}hx zrw7q&#T2ts#W+DhnZMjhl#N0|-DwcVs(f*_sX+3nVo5-T9o@s{n0LR0C;ZIx=DbVJ z=g?xZebo;qHETplLWWp7^jJYj_vHn`VxnxH{|-Y&+elb*AD!N3m0zZxP&49sea`2& z*Av-odeDeUXDri)R7f1n8*(+!luWuJxmU4WrwtJ0^+-@iz}8_w7G!X<_D7=66=7C0 zSLwO(&CUc~`lm(tl(+g|xcLzJKzqo3y|K^KYdYskZFH;Mv zg1if7c8m$?HJA(hAlU;%*Y`_5SZ%uf=|f#>Zux*bdN`+jHZhVr#C^kms?cjtLbNw2 z1d;W`kt-juimFqv1#az!FSO*j)}BGK*d3h&rJ+v;c_|oMrlv4UQWicvXwPkkP;OIQ z+`LJCr!z0><7=;*4|u$X5XvHEZpVzm^rD(iV9y+0x_EuDwtv&RnM0BGky_#dm80M% zhf?-&@oTl0;+GxswKiGGs_vgR%Rn*ZtD)gdGiM`Yy|-`k%3nDJD9Q>auda$4tGsfy zv+j892fav@d5xNdL}={_t(w=Zdbh@Myv|2zc@JGyxvDwq(-0Jwy7u{8ZAwDOj9+iBfGOK11W@BQP5mX+$d<*jt&kti49 z{s6~DKbQ!yF84IRCy3e~GmCifWU&OAi)P^C1q>kVAv4Xx3=c`<3^OgylVAltI-IhN zdCfvUI_kbyz$ekdNGOKd_9CLlLD%dY)Rloe5>FKW+5v^cd*b;~41|#uiAE#>y6$!! zYZuz%Pc+fg+yN;QK#Mm-p72B9)!ZF({qIkeV8evjw)vMr)7qd+Db8PQOsa}Xx(&g3q`nfDF-zX|u|fDHABco3%wGpYEQmwmk9)H-<5a1SlcEE4oUj0 zRd;C}ehZv5vIE6s5Pf81%ofhd`@OC7#+hFZa=R7bL$pHSAgkwS=4%4cVkI?3NwSDb z`zAZc$N8-)0d7?*52VyVd^Dn!ZS3ms(GD+*xa_7U<1tc9*37A(LWmJ|BC(Fgt$pRK z9xPRxNhR;fk1b!qPsDo4W2--5f9eaMGz3dOdWhLeLk`bC)z_lprm9WOaQW8Ne)fy? zACk}<@?Fz`;;2>Mz3+}wm_4acl6<%u#PSAj!eV6k3#(#RaQA%F?^U=7yk33jWDD|O ziu?nz3s+aGDLC2BN)(yf&Tl`uy&iJCd6YK$b}h+iRP{KE?F~P<(dT0 zYtGonL~j&{+5zAjZ%4k6e+7mqn-rz#?siy#$}dAD#%ll`NImvZA)L)tcjYjk1)h(S zGxAlFkrE6lh}E3Wep{a_IY_3^vCypP8>A-HM*xnd&Lu!(L&+Sd|o>17BTL2tJP5X7gmqI$+{DBm?S}N=Rg(-e&avrpWkhSzHGae zl$T9@dD>KXRL&fc%w4#YlkvD z+)%Qmt`8n#*Aj1jT%;@peo4DB853vHI`+E!IWxR$-;M9zIKWa0rxz!mva6{q{hSxt z%5alG%W5&XesrP#fbT?)Z!S*Ey|-s|Kxd?%Ir-zTjWFzNBWLpm=drWdpfz5(R`HHE z)W0FQc*?CPwkpO{2C6~Dk!AV^d}G~GhduMXHX@1ir$1^OlTivD4zsYQ^0^=bB^CLu z>>46_ZF>C#6l9clU^$W&P&NsTw{TNh-5qRRoRZ#z!5dZ#-sTNjDIRG35`1vj65cW% zYl)uQe)8zTn^nX@639W)uE466GBa-u1_Z9l@@X{v=K{$x>i@;U@A3y3^gLOXKfsUB zAN!K|*L?{;=L^ao+xFhs^wBe8-u?lzqQ%xW@5f}nM;w|`P*ge8M$3sZ5tX9ND!mn?J7 zlzooBvgknw0?a@8NIPIhf)njbOkX1pnii+A69n8UoP^Nt3Ngv=)YM}Vi#O9Um_c?S zDEiKLoB#1rR;m>>Taxng$j&Vc!z|OmM)|mZ`DfsveH7TT2&)a+k zqA1#yZjQD-?Z^U1Iz*PgX}N5EQr0H8z8W9bA;kl@l_bq+z zvGiX_Rx@J+9F-BpfQQ5Lo_HE5MnTFG*TG?&cS^c%4+|v==YlFGqBK0Ke`$+Xy4XHu zzIfw1k9n8!8zd*kUWK|>=A5YV!;K%bjcqA0)jM8@d&9A%-_3Rva}9UstFW*>O`|HT3$7ZMl8S$s-seEd)_kzJ^p5(K-9lX>~;A2d^3% zVVt@CLZ59Na&Zf%n=^-x%^65rs?}G18+tNjRwr_0XT4AKg0nLyv($4X>*9xFKnCg_ zA~t0N)H(^=y@!4pdKrENQp+D%T-tS399eD&mv>6S=AU}(^ia5qzt$plP|RB;5nm0} zsTtYiApBCphjyy?jB>jUyxGHG4P_q}d)Z*SYkWaBv+{D&vl@9-x9cxNYn{!MTO97w zR&K9+cMB8{3ZI58n%LdqO(rQEDigx4H*cXt)TX!cu4PRx?M&daS{Ijwv^$%3{nV+Q znW@xywa^58kVqch8qlQMLK?vq&Ya%y9`rN#`Ekj>d|0FHQG_~RY!N-*i_LmDrd(&P zE3T)=5ic-aUl}^FhGRaRJoE5<``+x~#n__JsEWV&`1~hH2nhB+aR2>h+<%roK>sTp zySd#U8^+35`tjgT^1M!Sm_fqYCGUDoTh&3J+9sK&4oN6rY9V?32lX!z`NXXMz1=_ueq#G2c8@7yU<>+AcK(+g@sg|> zGln@PbFU_@D40%FWzZQKm?j3*7Ah_q#$t9wQxPEuxHXlUOTfTWf&`$3mMNWlOq4z> zWg_wutAzMFv(L199fd`}4x^@$K^TQ=mQlZH-wj(r_*0>IkB&G`q_v{!onPX~C3D~W z?wZX#i@`yW<&xm=!Fi2?K2oK@2heZNzWBCNT^><;e6#@c%{touYJ{u*qQrxRoxop7 z2Id8kWP3e?tk>=RY{c;~(DBK6R(amZO4t+j#!YE&>*1zNY#&>%;B;^<)` zgP&wKBJ3_dr%bG!Q7lEs-6n;H^e7n{Cl3A)YQRYy>w3XU-OX*<A5~S!K>d zp#BT_9az2or_Ptv>oRE_&`{&q3sFBWm@Yk4P9Lqnj?Z2IxyD(m)Z2>Tb(9A$c*;G@ z4)SkV+`K2svk;@<4`8I4hvC8OH{&>XQL0Ywg3t+@MRpy9h#AOFy=PQHaRc0#oO z#C^eE>HRY9_kw!hHGK~!zdIl~(rLP0^Vh6)Wbzs&~U7QXmni!&3t4xu_ zNlvcf4hGQX9N32@5pDZYbUhiSVhjwm&&$y;*%_Hnr($**(F*n(?&1L=^uByPXJ;C8 zA3Ia3T1xk86XZ81Gm(U}U(NQUIcu9fuGpmMi7q;L`L{%G=kv9Uxd0BqT~`80ya2SX z$C;Sz^>ImnQFk}wr`z-Z43@bG@_aVCQoAufKQA5W-NSaV*JGhfbxb81Mi7Ch(ze?P zlh4T}A_evsK;O2{wtz2E9GvJ*t0-<3bLQ29a}mzd6r^@4lHNO0XLHVZf|DJ@9ntP) zdA%fJ>a}ZgV(+rqg4jE+laEJ8AB{MQ<-{w(CvQ2~caMrHW8MaOr(f0@LOmjOpZ^;u&X-h+N_Z{zoLqHBjlvqdV3Ox?0ufBA(b8n51Jd8~m zFZ%&qRa3P%5qEpX^KH1Wu5Q`wh^YLjdlT-@bmmE+Oh+=qA3RFn{_vHkz7dh zS;!PA&kUxLSKz>f*!zcl6Ko^1kgt-4FSrF8pxWE5*xJSJQac>?|Gz9zlCzHHDKD~s9 zjJJ4vkeN`U>S$+rx50)6_fqpbcaLGT-g+Mm>fqSud`K8{tAvX;+IbHLJmxVGdgk}( zx=d4avjI+8#5dkvT!Mz><{CNvYLJ`D#Sta`IE+M<&9E8=`1m%s4VxxPVG;ovpK5{= zs1_p1xbA*{3%>$MEz*^-QI5L1kmyT|Oo-+^aUxR-4;msJd|cXWQqiEU7{}y6Z1!@B z^?hu^>+jAVIdS3yz2uSKg^Yu%Nv7P=E45A3G&6|FUKbcsOnRkc~CV^Xl+I*Hy7E) zR$zZin#MteYQ|RmIe9q)vY7NeRvnKAUaR-czVS9EvL5S*Crsw!oFh)FrD)IxTi%V^ zO6U71(%ObSv`L*kl%Lvid&JyS8t9W%{=(2rpJS1z;VD6k6wZpfD;HxuxpLpag(yXx z(~jx#)oS!GWq$qfa9Qk8Ev$$ea~bbtS0~iZyt(G~<*vaRB)NaK|F!!bq+UanY^;3T z$0u*d8T20M7y=cU^0j?u-j)ZFxC`A&Ieph3ei0!Y+%XmdiY0q614SLil+`)rw)U`!nnV)adDAH)wzguPha~i;va$K0tOI`qa5p;WE6o zXFgGmh>Z%g8WavU*3{bY{sH^TsQ(uWzsn!Xp6|=@htBfoKI?&1z)#a({ZMrDqFO;c zu*KJXn`X7n%xyK13aGEn;hFHVUh|f_j}e)?rg*{LXNZHzs_XU{q8!Bl+ceo3@W<;5 zB_`1PHtSWY-pMFIn~3Kv5Fwd*DiBk3jyuWBu~!d=gMrcw3#EbPd*RYiew=%VCZZFX z>4FxCdc2Mz)_dMZ(Ol3FB+blVRW7fB9+>>_(OyRk`)5HJ)f1CnfNyxTZ*>*j1Z|q< zWxd7D5q+fAl!bR-q*3=vM5-6?F4O(YFejHCexv6tgDm%dyva1VdV?nb_9I|?0SX;r z!xw4Nk+;%BY-$M_6?Muy!w2+-@==(TvE#i+tPm(W=~)2%v3{DS*xoFu4VhPRKWiUZ zqR8*g5URX37LrC|y)Y94y)A%LF)IClq_Zi9lcwzutcRnq0uT3t@Z=a#Ms7H=l-RdJviqKB4FPWf=nDG?$T4aD(t$HW-t8kB--0geg{GcZJB6YO{8!_HGYeZzktGeu zf0c6=nk-hG#$SQNhWcJ2Ek*_+^Bx|aKN&S_H6XpM{lWNp1U%y8jJ#_yYB5}|(Yx=~ z<)xqp@I#;v^iojNjfE?)i{FOlS+C5dTNu^qU!u%yzbH{5f*#P>(sh}7XP+4jZ(dP* z^p$Hs`7s%)-7TB?4;%3xZXxJDaliiWaj&xcJyK>petP@f&y(x!2lSoRxOE^5*YUF* zVgnH`R=u{$hGufjy0KKxV3kSM#>}=84NgoAque{lRd5eI9o6qW@s^JHo;+s&bzhio ztyRM#@x_uV6xHM!=ndscO)2UXXf_>6I|9jt8NvZ!8qzeKU`ItFg6>5QFcS zyhi{=lca`V(7Kav9l6FqHBq+^L`Ew45F% zZ1x}`zX>}~RFE{3MuR{3?6tsn8-}vV#)+QsF#{TG(lFLS=PmRBSLdQ++a?H?IG@9Q3TR%6wyNwJ>65s0QhyaaEfDXu_q0yFk!^HZ0UWF@0|t%@1gxGU5SP2imcX-jpdtGrNf-nCJd9s2LKL zydFXF!xwu+o53BdT|!sT5>8N6k*gl1dO_2ZGyJZnXiRT1FELrK&g(AQaEQrg7U7z3 z&F9OiH@NewNyDrf7~XBiDOEkBSSm9nO=!OIiE~gHAHC1Ru^`*{m(^j3*K}E0lzg+F zM@^Q$e;Go8v0A+3n{#30c=%$Vq1&6&hIAlq zhw-5U)pF8;a#O#uz2}K%tT$pQY6)~ykBvxCj!!gr#zwc2 z0|^nTPhiTAi8oVxK?}*a;pI6q55)s`@MX(_9K>pvh2b2?30nGI)FVtuO$VNnd%e4q z$KH#z?=(?TBXv9dw!uFwyfOGYod4qu1oJ2E&-@klnmRQaIp9A}Sr6L>gDunXcYj*r zx{74R2wJ(pCzN@Qyu}7EqXi5007uRT(JS?sg7tW#Q$?CsHroSMK;Yq~C z`Di+Ve~)Zpf@^~%rvad4`aGU)z!N502O~{##i=?Zz6mi?l_xR;1blWHfyXllEQJbd z5At;c5Nl_IHOGkt%f#uJx2C6NG6Az6vkNs_RzhfHP+Uo6F7F!n{H zlkc5C>i`)V&%v?JqMGD;7*B9ZzpDu@mhZzZPtu%)jG&NFVrrb6qt!vn7iUNTKl?VPPs6R_5w(M4nteF&r%kM& zm}=L0!9ZoSWiLz?O@jQ)hZpmOhqH%w{FYcxxVj$n4N3hJll3&@ zlv+a2@d%%hf~+oi^qXVH$3&oF=k>+h12VKKJEJMY3UgyYmBw$$e+e z4m}6iv-+gziL4i41{%Z$?~Si=>$x_Dm zF;quXmDvReLhI79Ul8LopdPt+XHi@du|b(d9Y4N<{g~ zx&bMuL_VDTEK0e@^vTho`h`JD9qP#OgGcH?H?pPi?e2V+LP+*At`FlM;r&nVmr?&O z7C`w!=NrCiF8C*+6~Kq+Ef4Rr0CMwhHs;mHSSVm-ux;Y-q&I#%W@k;5rp?%IlLmS^ zKVRFe1vEC1k;uCJ4wzD)#weNTM5W;%W-e%aylF8m*UT?OEaaE4VEXDr5(_ZPH*f$I zQ0GJh-LH{05Hve&pB7!EY!}R5olP`>5KK#$jwk{EGr3QLTf;rvZCo%66LPEylxY}} zpWq1b*fsl|ch zi3x}IJt*{o9MdZX?dZ)_i?+QjPPQ~3=jj)eEYLkyMz9DT**HDWkGU^wGLfE7o7pQS#@1DfT`sZ6Z;$nQ$2Be@~wIt|_IaV$U)V?{Sw{dhVb74(u z%hJ=VC&ye3(OWlfBzq{9oTEG(rX};f138;dG?K9wr0EfHW94_n)eQ@)3u-A|Gxhcx z++Om|URbQ#ZRloPmvvtrlXgm1yVOYNTNaw2c)1XF8F^T6l(sFRJ3BP7XCXZEqBLwj zXnQ|-JYeZ&zq?|B(y%kNs4L3v@nLb~0omA6W$m@>FlBo38Pf?bwTXM-BgYlo-f&6J z`-P!wyZ^wRLG=H`{hPnyUK<#ewBBi;!V2EhYutGH(Jsa_fEudefOA2fIKOv4;$WL5 zVLJ*zhUWJAlkQvrJtS*u2fHjo{OU0$N&@9CpEJYV>O-KRe4(5(%q!^&9`vC9< z9JnWxTC`oy%QR3INJEOmI(g^O#9}1A=e2Cv<7zV?=&5lUrO)qI-O zpR5vcvS=2;vhsVE+ogKg$eiwNM@XYPbQ+xX(k*u>{ZGjF)#Z%odh~92F}!GV^@BB&TTsS z%FsF47W{D-vB(vL8mmk#Mn-bn`PCA|72=e)mPuFyNfMGY9#chrUp}XP*%EYcvhN@@ zcrDBxPZZ-`5MwiB)3E_3zk1h}cM+EA{c5c+yXAlEZ&$qCnyky({u_ip&U?ve!QD0C zzsB;jsrskHK22E=2%IL}8kN@m?RUA*?RPpXzcp)rFYf;O%T?^_deaz_#a%bv>2rOq zbbs#T|2%(tUF7?D|JPq*I%RV&7}%1V+_9&$H{#oW;HJe+J*8R4 Date: Sun, 9 Mar 2014 12:44:47 +0100 Subject: [PATCH 11/18] Implement ImageListField for MP4 --- beets/mediafile.py | 67 +++++++++-------------------------------- test/rsrc/image.m4a | Bin 0 -> 5862 bytes test/test_mediafile.py | 10 ++++-- 3 files changed, 22 insertions(+), 55 deletions(-) create mode 100644 test/rsrc/image.m4a diff --git a/beets/mediafile.py b/beets/mediafile.py index ae97e46cb..209ad407d 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -495,28 +495,16 @@ class MP4ImageStorageStyle(MP4ListStorageStyle): def __init__(self, **kwargs): super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) - self.as_type = str - def store(self, mutagen_file, images): - covers = [self._mp4_cover(image) for image in images] - mutagen_file['covr'] = covers + def get_list(self, mutagen_file): + return [TagImage(data) for data in self.fetch(mutagen_file)] - @classmethod - def _mp4_cover(cls, data): - """Make ``MP4Cover`` tag from image data. - - Returns instance of ``mutagen.mp4.MP4Cover`` with correct cover - format. - """ - kind = imghdr.what(None, h=data) - if kind == 'png': + def serialize(self, image): + if image.mime_type == 'image/png': kind = mutagen.mp4.MP4Cover.FORMAT_PNG - elif kind == 'jpeg': + elif image.mime_type == 'image/jpeg': kind = mutagen.mp4.MP4Cover.FORMAT_JPEG - else: - raise ValueError('MP4 only supports PNG and JPEG images') - - return mutagen.mp4.MP4Cover(data, kind) + return mutagen.mp4.MP4Cover(image.data, kind) class MP3StorageStyle(StorageStyle): @@ -950,45 +938,20 @@ class DateItemField(MediaField): class CoverArtField(MediaField): - """A descriptor providing access to a file's embedded album art. - Holds a bytestring reflecting the image data. The image should - either be a JPEG or a PNG for cross-format compatibility. It's - probably a bad idea to use anything but these two formats. - """ - # TODO make this into shim when ImageField is implemented for all - # formats. - def __init__(self): - super(CoverArtField, self).__init__( - MP3ImageStorageStyle(), - MP4ImageStorageStyle(), - ASFImageStorageStyle(), - VorbisImageStorageStyle(), - FlacImageStorageStyle(), - out_type=str, - ) + pass def __get__(self, mediafile, _): - if mediafile.type in ['mp3', 'flac', 'asf'] + VorbisImageStorageStyle.formats: - try: - return mediafile.images[0].data - except IndexError: - return None - for style in self.styles(mediafile): - return style.get(mediafile.mgfile) + try: + return mediafile.images[0].data + except IndexError: + return None def __set__(self, mediafile, data): - if mediafile.type in ['mp3', 'flac', 'asf'] + VorbisImageStorageStyle.formats: - if data: - mediafile.images = [TagImage(data=data)] - else: - mediafile.images = [] - return - if data is not None: - if not isinstance(data, str): - raise ValueError('value must be a byte string or None') - for style in self.styles(mediafile): - style.set(mediafile.mgfile, data) + if data: + mediafile.images = [TagImage(data=data)] + else: + mediafile.images = [] class ImageListField(MediaField): diff --git a/test/rsrc/image.m4a b/test/rsrc/image.m4a new file mode 100644 index 0000000000000000000000000000000000000000..89704d557bb3ffac704b5309f2c946e3942a3490 GIT binary patch literal 5862 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 TZ{U Date: Sun, 9 Mar 2014 12:47:38 +0100 Subject: [PATCH 12/18] Rename TagImage to Image --- beets/mediafile.py | 40 ++++++++++++++++++++-------------------- test/test_mediafile.py | 24 ++++++++++++------------ 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 209ad407d..48fd0942f 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -497,7 +497,7 @@ class MP4ImageStorageStyle(MP4ListStorageStyle): super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) def get_list(self, mutagen_file): - return [TagImage(data) for data in self.fetch(mutagen_file)] + return [Image(data) for data in self.fetch(mutagen_file)] def serialize(self, image): if image.mime_type == 'image/png': @@ -629,11 +629,11 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): - """Converts between APIC frames and ``TagImage`` instances. + """Converts between APIC frames and ``Image`` instances. The `get_list` method inherited from ``ListStorageStyle`` returns a - list of ``TagImage``s. Similarily the `set_list` method accepts a - list of ``TagImage``s as its ``values`` arguemnt. + list of ``Image``s. Similarily the `set_list` method accepts a + list of ``Image``s as its ``values`` arguemnt. """ def __init__(self): @@ -641,12 +641,12 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): self.as_type = str def fetch(self, mutagen_file): - """Return a list of TagImages obtained from all APIC frames. + """Return a list of Images obtained from all APIC frames. """ frames = mutagen_file.tags.getall(self.key) images = [] for frame in mutagen_file.tags.getall(self.key): - images.append(TagImage(data=frame.data, desc=frame.desc, + images.append(Image(data=frame.data, desc=frame.desc, type=frame.type)) return images @@ -656,7 +656,7 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): def serialize(self, image): """Return an APIC frame populated with data from ``image``. """ - assert isinstance(image, TagImage) + assert isinstance(image, Image) frame = mutagen.id3.Frames[self.key]() frame.data = image.data frame.mime = image.mime_type @@ -690,7 +690,7 @@ class ASFImageStorageStyle(ListStorageStyle): mime, data, type, desc = _unpack_asf_image(picture.value) except: continue - pictures.append(TagImage(data, desc=desc, type=type)) + pictures.append(Image(data, desc=desc, type=type)) return pictures def serialize(self, image): @@ -716,14 +716,14 @@ class VorbisImageStorageStyle(ListStorageStyle): # Try legacy COVERART tags. if 'coverart' in mutagen_file: for data in mutagen_file['coverart']: - images.append(TagImage(base64.b64decode(data))) + images.append(Image(base64.b64decode(data))) return images for data in mutagen_file["metadata_block_picture"]: try: pic = mutagen.flac.Picture(base64.b64decode(data)) except (TypeError, AttributeError): continue - images.append(TagImage(data=pic.data, desc=pic.desc, + images.append(Image(data=pic.data, desc=pic.desc, type=pic.type)) return images @@ -737,7 +737,7 @@ class VorbisImageStorageStyle(ListStorageStyle): def serialize(self, image): - """Turn a TagImage into a base64 encoded FLAC picture block. + """Turn a Image into a base64 encoded FLAC picture block. """ pic = mutagen.flac.Picture() pic.data = image.data @@ -748,7 +748,7 @@ class VorbisImageStorageStyle(ListStorageStyle): class FlacImageStorageStyle(ListStorageStyle): - """Converts between ``mutagen.flac.Picture`` and ``TagImage`` instances. + """Converts between ``mutagen.flac.Picture`` and ``Image`` instances. """ formats = ['flac'] @@ -757,11 +757,11 @@ class FlacImageStorageStyle(ListStorageStyle): super(FlacImageStorageStyle, self).__init__(key='') def fetch(self, mutagen_file): - """Return a list of TagImages stored in the tags. + """Return a list of Images stored in the tags. """ images = [] for picture in mutagen_file.pictures: - images.append(TagImage(data=picture.data, desc=picture.desc, + images.append(Image(data=picture.data, desc=picture.desc, type=picture.type)) return images @@ -773,7 +773,7 @@ class FlacImageStorageStyle(ListStorageStyle): mutagen_file.add_picture(pic) def serialize(self, image): - """Turn a TagImage into a mutagen.flac.Picture. + """Turn a Image into a mutagen.flac.Picture. """ pic = mutagen.flac.Picture() pic.data = image.data @@ -949,7 +949,7 @@ class CoverArtField(MediaField): def __set__(self, mediafile, data): if data: - mediafile.images = [TagImage(data=data)] + mediafile.images = [Image(data=data)] else: mediafile.images = [] @@ -957,15 +957,15 @@ class CoverArtField(MediaField): class ImageListField(MediaField): """Descriptor to access the list of images embedded in tags. - The getter returns a list of ``TagImage`` instances obtained from - the tags. The setter accepts a list of ``TagImage`` instances to be + The getter returns a list of ``Image`` instances obtained from + the tags. The setter accepts a list of ``Image`` instances to be written to the tags. """ def __init__(self): # The storage styles used here must implement the # `ListStorageStyle` interface and get and set lists of - # `TagImage`s. + # `Image`s. super(ImageListField, self).__init__( MP3ImageStorageStyle(), MP4ImageStorageStyle(), @@ -985,7 +985,7 @@ class ImageListField(MediaField): style.set_list(mediafile.mgfile, images) -class TagImage(object): +class Image(object): """Strucuture representing image data and metadata that can be stored and retrieved from tags. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 5ffbd0b1a..afe0e3ac3 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -23,7 +23,7 @@ import time import _common from _common import unittest -from beets.mediafile import MediaFile, TagImage +from beets.mediafile import MediaFile, Image class ArtTestMixin(object): @@ -80,18 +80,18 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type=TagImage.TYPES.front) + type=Image.TYPES.front) image = mediafile.images[1] self.assertEqual(image.data, self.jpg_data) self.assertEqual(image.mime_type, 'image/jpeg') self.assertExtendedImageAttributes(image, desc='the artist', - type=TagImage.TYPES.artist) + type=Image.TYPES.artist) def test_set_image_structure(self): mediafile = self._mediafile_fixture('empty') - image = TagImage(data=self.png_data, desc='album cover', - type=TagImage.TYPES.front) + image = Image(data=self.png_data, desc='album cover', + type=Image.TYPES.front) mediafile.images = [image] mediafile.save() @@ -102,14 +102,14 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type=TagImage.TYPES.front) + type=Image.TYPES.front) def test_add_image_structure(self): mediafile = self._mediafile_fixture('image') self.assertEqual(len(mediafile.images), 2) - image = TagImage(data=self.png_data, desc='the composer', - type=TagImage.TYPES.composer) + image = Image(data=self.png_data, desc='the composer', + type=Image.TYPES.composer) mediafile.images += [image] mediafile.save() @@ -123,7 +123,7 @@ class ImageStructureTestMixin(object): except IndexError: image = None self.assertExtendedImageAttributes(image, - desc='the composer', type=TagImage.TYPES.composer) + desc='the composer', type=Image.TYPES.composer) @unittest.skip('editing list by reference is not implemented yet') def test_mutate_image_structure(self): @@ -134,11 +134,11 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.png_data) self.assertEqual(image.mime_type, 'image/png') self.assertExtendedImageAttributes(image, desc='album cover', - type=TagImage.TYPES.front) + type=Image.TYPES.front) image.data = self.jpg_data image.desc = 'new description' - image.type = TagImage.COMPOSER + image.type = Image.COMPOSER mediafile.save() mediafile = MediaFile(mediafile.path) @@ -148,7 +148,7 @@ class ImageStructureTestMixin(object): self.assertEqual(image.data, self.jpg_data) self.assertEqual(image.mime_type, 'image/jpeg') self.assertExtendedImageAttributes(image, desc='new description', - type=TagImage.TYPES.composer) + type=Image.TYPES.composer) @unittest.skip('editing list by reference is not implemented yet') def test_delete_image_structure(self): From d2b627cc1c769d9861bbea791988316ce41df70b Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 12:50:24 +0100 Subject: [PATCH 13/18] Make sure the png_data method is available --- test/test_mediafile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index afe0e3ac3..8b4386f5b 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -63,7 +63,7 @@ class ArtTestMixin(object): self.assertEqual(mediafile.art, self.jpg_data) -class ImageStructureTestMixin(object): +class ImageStructureTestMixin(ArtTestMixin): """Test reading and writing multiple image tags. The tests use the `image` media file fixture. The tags of these files From 28bab0a9a3bcc2d8bdb5cb78c1783ea43a3ba3c1 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 13:01:14 +0100 Subject: [PATCH 14/18] Test invalid image format for MP4 --- beets/mediafile.py | 11 ++++------- test/test_mediafile.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 48fd0942f..b25184727 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -251,15 +251,10 @@ def _sc_encode(gain, peak): def _image_mime_type(data): - """Return the MIME type (either image/png or image/jpeg) of the - image data (a bytestring). + """Return the MIME type of the image data (a bytestring). """ kind = imghdr.what(None, h=data) - if kind == 'png': - return 'image/png' - else: - # Currently just fall back to JPEG. - return 'image/jpeg' + return 'image/{0}'.format(kind) # StorageStyle classes describe strategies for accessing values in # Mutagen file objects. @@ -504,6 +499,8 @@ class MP4ImageStorageStyle(MP4ListStorageStyle): kind = mutagen.mp4.MP4Cover.FORMAT_PNG elif image.mime_type == 'image/jpeg': kind = mutagen.mp4.MP4Cover.FORMAT_JPEG + else: + raise ValueError('The MP4 format only supports PNG and JPEG images') return mutagen.mp4.MP4Cover(image.data, kind) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 8b4386f5b..c8e878086 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -46,6 +46,14 @@ class ArtTestMixin(object): return self._jpg_data _jpg_data = None + @property + def tiff_data(self): + if not self._jpg_data: + with open(os.path.join(_common.RSRC, 'image-2x3.tiff'), 'rb') as f: + self._jpg_data = f.read() + return self._jpg_data + _jpg_data = None + def test_set_png_art(self): mediafile = self._mediafile_fixture('empty') mediafile.art = self.png_data @@ -578,6 +586,13 @@ class MP4Test(ReadWriteTestBase, PartialTestMixin, 'bitdepth': 16, 'channels': 2, } + + def test_add_tiff_image_fails(self): + mediafile = self._mediafile_fixture('empty') + with self.assertRaises(ValueError): + mediafile.images = [Image(data=self.tiff_data)] + + class AlacTest(ReadWriteTestBase, GenreListTestMixin, unittest.TestCase): extension = 'alac.m4a' audio_properties = { From 9fe212feead4eb7c03caddd4dd3de585d4930dde Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 13:09:26 +0100 Subject: [PATCH 15/18] Add support for different coverart mime types --- beets/mediafile.py | 14 +++++++++++++- test/test_mediafile.py | 18 ++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index b25184727..7cf4ef6e3 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -254,7 +254,19 @@ def _image_mime_type(data): """Return the MIME type of the image data (a bytestring). """ kind = imghdr.what(None, h=data) - return 'image/{0}'.format(kind) + if kind in ['gif', 'jpeg', 'png', 'tiff', 'bmp']: + return 'image/{0}'.format(kind) + elif kind == 'pgm': + return 'image/x-portable-graymap' + elif kind == 'pbm': + return 'image/x-portable-bitmap' + elif kind == 'ppm': + return 'image/x-portable-pixmap' + elif kind == 'xbm': + return 'image/x-xbitmap' + else: + return 'image/x-{0}'.format(kind) + # StorageStyle classes describe strategies for accessing values in # Mutagen file objects. diff --git a/test/test_mediafile.py b/test/test_mediafile.py index c8e878086..e1a926530 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -186,6 +186,24 @@ class ExtendedImageStructureTestMixin(ImageStructureTestMixin): self.assertEqual(image.desc, desc) self.assertEqual(image.type, type) + def test_add_tiff_image(self): + mediafile = self._mediafile_fixture('image') + self.assertEqual(len(mediafile.images), 2) + + image = Image(data=self.tiff_data, desc='the composer', + type=Image.TYPES.composer) + mediafile.images += [image] + mediafile.save() + + mediafile = MediaFile(mediafile.path) + self.assertEqual(len(mediafile.images), 3) + + # WMA does not preserve the order, so we have to work around this + image = filter(lambda i: i.mime_type == 'image/tiff', + mediafile.images)[0] + self.assertExtendedImageAttributes(image, + desc='the composer', type=Image.TYPES.composer) + # TODO include this in ReadWriteTestBase if implemented class LazySaveTestMixin(object): From 0a3e47c965ce3bafcd5dc2b1734ba39cf67e19ba Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 13:45:29 +0100 Subject: [PATCH 16/18] Add deserialize method to storage styles --- beets/mediafile.py | 117 ++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 59 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index 7cf4ef6e3..aebffe6f4 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -274,6 +274,15 @@ def _image_mime_type(data): class StorageStyle(object): """Parameterizes the storage behavior of a single field for a certain tag format. + + The ``get()`` method retrieves data from tags of a mutagen file. It + uses the fetch method to obtain the raw mutagen value. It then uses + the ``deserialize()`` method to convert it into a python value. + + ``set()`` uses ``serialize()`` to convert the passed value into a + suitable mutagen type. The ``store()`` method is then used to write + that value to the tags. + - key: The Mutagen key used to access the field's data. - as_type: Which type the value is stored as (unicode, int, bool, or str). @@ -286,20 +295,26 @@ class StorageStyle(object): # TODO Use mutagen file types instead of MediaFile formats formats = ['flac', 'opus', 'ogg', 'ape', 'wv', 'mpc'] + """List of file format the StorageStyle can handle. - def __init__(self, key, as_type=unicode, suffix=None, - float_places=2, formats=None): + Format names correspond to those returned by ``mediafile.type``. + """ + + def __init__(self, key, as_type=unicode, suffix=None, float_places=2): self.key = key self.as_type = as_type self.suffix = suffix self.float_places = float_places - if formats: - self.formats = formats # Convert suffix to correct string type. if self.suffix and self.as_type == unicode: self.suffix = self.as_type(self.suffix) + def get(self, mutagen_file): + """Fetches raw data from tags and deserializes it into a python value. + """ + return self.deserialize(self.fetch(mutagen_file)) + def fetch(self, mutagen_file): """Retrieve the first raw value of this tag from the mediafile.""" try: @@ -307,21 +322,20 @@ class StorageStyle(object): except KeyError: return None - def get(self, mutagen_file): - data = self.fetch(mutagen_file) - data = self._strip_possible_suffix(data) - return data + def deserialize(self, mutagen_value): + if self.suffix and isinstance(mutagen_value, unicode) \ + and mutagen_value.endswith(self.suffix): + return mutagen_value[:-len(self.suffix)] + else: + return mutagen_value + + def set(self, mutagen_file, value): + self.store(mutagen_file, self.serialize(value)) def store(self, mutagen_file, value): """Stores a serialized value in the mediafile.""" mutagen_file[self.key] = [value] - def set(self, mutagen_file, value): - if value is None: - value = self._none_value() - value = self.serialize(value) - self.store(mutagen_file, value) - def serialize(self, value): """Convert value to a type that is suitable for storing in a tag.""" if value is None: @@ -379,8 +393,7 @@ class ListStorageStyle(StorageStyle): return None def get_list(self, mutagen_file): - data = self.fetch(mutagen_file) - return [self._strip_possible_suffix(item) for item in data] + return [self.deserialize(item) for item in self.fetch(mutagen_file)] def fetch(self, mutagen_file): try: @@ -449,25 +462,24 @@ class MP4TupleStorageStyle(MP4StorageStyle): """Store values as part of a numeric pair. """ - def __init__(self, key, pack_pos=0, **kwargs): + def __init__(self, key, index=0, **kwargs): super(MP4TupleStorageStyle, self).__init__(key, **kwargs) - self.pack_pos = pack_pos + self.index = index - def _fetch_unpacked(self, mutagen_file): - items = super(MP4TupleStorageStyle, self).fetch(mutagen_file) or [] + def deserialize(self, mutagen_value): + items = mutagen_value or [] packing_length = 2 return list(items) + [0] * (packing_length - len(items)) def get(self, mutagen_file): - data = self._fetch_unpacked(mutagen_file)[self.pack_pos] - return self._strip_possible_suffix(data) + return super(MP4TupleStorageStyle, self).get(mutagen_file)[self.index] def set(self, mutagen_file, value): if value is None: value = 0 - data = self._fetch_unpacked(mutagen_file) - data[self.pack_pos] = int(value) - self.store(mutagen_file, data) + items = self.deserialize(self.fetch(mutagen_file)) + items[self.index] = int(value) + self.store(mutagen_file, items) class MP4ListStorageStyle(ListStorageStyle, MP4StorageStyle): @@ -503,8 +515,8 @@ class MP4ImageStorageStyle(MP4ListStorageStyle): def __init__(self, **kwargs): super(MP4ImageStorageStyle, self).__init__(key='covr', **kwargs) - def get_list(self, mutagen_file): - return [Image(data) for data in self.fetch(mutagen_file)] + def deserialize(self, data): + return Image(data) def serialize(self, image): if image.mime_type == 'image/png': @@ -619,7 +631,7 @@ class MP3SlashPackStorageStyle(MP3StorageStyle): self.pack_pos = pack_pos def _fetch_unpacked(self, mutagen_file): - data = super(MP3SlashPackStorageStyle, self).fetch(mutagen_file) or '' + data = self.fetch(mutagen_file) or '' items = unicode(data).split('/') packing_length = 2 return list(items) + [None] * (packing_length - len(items)) @@ -649,15 +661,13 @@ class MP3ImageStorageStyle(ListStorageStyle, MP3StorageStyle): super(MP3ImageStorageStyle, self).__init__(key='APIC') self.as_type = str + def deserialize(self, apic_frame): + """Convert APIC frame into Image.""" + return Image(data=apic_frame.data, desc=apic_frame.desc, + type=apic_frame.type) + def fetch(self, mutagen_file): - """Return a list of Images obtained from all APIC frames. - """ - frames = mutagen_file.tags.getall(self.key) - images = [] - for frame in mutagen_file.tags.getall(self.key): - images.append(Image(data=frame.data, desc=frame.desc, - type=frame.type)) - return images + return mutagen_file.tags.getall(self.key) def store(self, mutagen_file, frames): mutagen_file.tags.setall(self.key, frames) @@ -689,18 +699,9 @@ class ASFImageStorageStyle(ListStorageStyle): def __init__(self): super(ASFImageStorageStyle, self).__init__(key='WM/Picture') - def fetch(self, mutagen_file): - if 'WM/Picture' not in mutagen_file: - return [] - - pictures = [] - for picture in mutagen_file['WM/Picture']: - try: - mime, data, type, desc = _unpack_asf_image(picture.value) - except: - continue - pictures.append(Image(data, desc=desc, type=type)) - return pictures + def deserialize(self, asf_picture): + mime, data, type, desc = _unpack_asf_image(asf_picture.value) + return Image(data, desc=desc, type=type) def serialize(self, image): pic = mutagen.asf.ASFByteArrayAttribute() @@ -766,13 +767,11 @@ class FlacImageStorageStyle(ListStorageStyle): super(FlacImageStorageStyle, self).__init__(key='') def fetch(self, mutagen_file): - """Return a list of Images stored in the tags. - """ - images = [] - for picture in mutagen_file.pictures: - images.append(Image(data=picture.data, desc=picture.desc, - type=picture.type)) - return images + return mutagen_file.pictures + + def deserialize(self, flac_picture): + return Image(data=flac_picture.data, desc=flac_picture.desc, + type=flac_picture.type) def store(self, mutagen_file, pictures): """``pictures`` is a list of mutagen.flac.Picture instances. @@ -1200,7 +1199,7 @@ class MediaFile(object): ) track = MediaField( MP3SlashPackStorageStyle('TRCK', pack_pos=0), - MP4TupleStorageStyle('trkn', pack_pos=0), + MP4TupleStorageStyle('trkn', index=0), StorageStyle('TRACK'), StorageStyle('TRACKNUMBER'), ASFStorageStyle('WM/TrackNumber'), @@ -1208,7 +1207,7 @@ class MediaFile(object): ) tracktotal = MediaField( MP3SlashPackStorageStyle('TRCK', pack_pos=1), - MP4TupleStorageStyle('trkn', pack_pos=1), + MP4TupleStorageStyle('trkn', index=1), StorageStyle('TRACKTOTAL'), StorageStyle('TRACKC'), StorageStyle('TOTALTRACKS'), @@ -1217,7 +1216,7 @@ class MediaFile(object): ) disc = MediaField( MP3SlashPackStorageStyle('TPOS', pack_pos=0), - MP4TupleStorageStyle('disk', pack_pos=0), + MP4TupleStorageStyle('disk', index=0), StorageStyle('DISC'), StorageStyle('DISCNUMBER'), ASFStorageStyle('WM/PartOfSet'), @@ -1225,7 +1224,7 @@ class MediaFile(object): ) disctotal = MediaField( MP3SlashPackStorageStyle('TPOS', pack_pos=1), - MP4TupleStorageStyle('disk', pack_pos=1), + MP4TupleStorageStyle('disk', index=1), StorageStyle('DISCTOTAL'), StorageStyle('DISCC'), StorageStyle('TOTALDISCS'), From ee40050d4bed8b85f3e35b30a1bc3ba252c6bd35 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 14:08:43 +0100 Subject: [PATCH 17/18] Remove unused methods --- beets/mediafile.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/beets/mediafile.py b/beets/mediafile.py index aebffe6f4..ae6385649 100644 --- a/beets/mediafile.py +++ b/beets/mediafile.py @@ -370,13 +370,6 @@ class StorageStyle(object): elif self.out_type == unicode: return u'' - def _strip_possible_suffix(self, data): - if self.suffix and isinstance(data, unicode) \ - and data.endswith(self.suffix): - return data[:-len(self.suffix)] - else: - return data - class ListStorageStyle(StorageStyle): """Abstract class that provides access to lists. @@ -445,12 +438,6 @@ class MP4StorageStyle(StorageStyle): formats = ['aac', 'alac'] - def fetch(self, mutagen_file): - try: - return mutagen_file[self.key][0] - except KeyError: - return None - def serialize(self, value): value = super(MP4StorageStyle, self).serialize(value) if self.key.startswith('----:') and isinstance(value, unicode): From 0335059293ee02b1d8687bff6191a14d5c5caf2d Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Sun, 9 Mar 2014 14:09:31 +0100 Subject: [PATCH 18/18] Add tiff image fixture --- test/rsrc/image-2x3.tiff | Bin 0 -> 206 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 test/rsrc/image-2x3.tiff diff --git a/test/rsrc/image-2x3.tiff b/test/rsrc/image-2x3.tiff new file mode 100644 index 0000000000000000000000000000000000000000..289fe91bfe08a61ac17fbb1a586250cc9f639175 GIT binary patch literal 206 zcmebD)M8LzU|{%<1Mn~~FfubR0#z^pF(Z`C4B`XXEI|4g5Hmx?LF(9$)C)4Qfc0_! x#YLgwAU)zxaS0$>ijftpW*d+#4Q1~GvSpCW)Ic&5D8=9b#2~w1GzS9&0svdu9P