1#!/usr/bin/python
2
3# Audio Tools, a module and set of tools for manipulating audio data
4# Copyright (C) 2007-2014  Brian Langenberger
5
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation; either version 2 of the License, or
9# (at your option) any later version.
10
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15
16# You should have received a copy of the GNU General Public License
17# along with this program; if not, write to the Free Software
18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20import unittest
21import audiotools
22import tempfile
23
24from test import (parser, BLANK_PCM_Reader, EXACT_SILENCE_PCM_Reader,
25                  Combinations,
26                  TEST_COVER1, TEST_COVER2, TEST_COVER3, TEST_COVER4,
27                  HUGE_BMP)
28
29
30def do_nothing(self):
31    pass
32
33# add a bunch of decorator metafunctions like LIB_CORE
34# which can be wrapped around individual tests as needed
35for section in parser.sections():
36    for option in parser.options(section):
37        if parser.getboolean(section, option):
38            vars()["%s_%s" % (section.upper(),
39                              option.upper())] = lambda function: function
40        else:
41            vars()["%s_%s" % (section.upper(),
42                              option.upper())] = lambda function: do_nothing
43
44
45class MetaDataTest(unittest.TestCase):
46    def setUp(self):
47        self.metadata_class = audiotools.MetaData
48        self.supported_fields = ["track_name",
49                                 "track_number",
50                                 "track_total",
51                                 "album_name",
52                                 "artist_name",
53                                 "performer_name",
54                                 "composer_name",
55                                 "conductor_name",
56                                 "media",
57                                 "ISRC",
58                                 "catalog",
59                                 "copyright",
60                                 "publisher",
61                                 "year",
62                                 "date",
63                                 "album_number",
64                                 "album_total",
65                                 "comment"]
66        self.supported_formats = []
67
68    def empty_metadata(self):
69        return self.metadata_class()
70
71    @METADATA_METADATA
72    def test_roundtrip(self):
73        for audio_class in self.supported_formats:
74            with tempfile.NamedTemporaryFile(
75                    suffix="." + audio_class.SUFFIX) as temp_file:
76                track = audio_class.from_pcm(temp_file.name,
77                                             BLANK_PCM_Reader(1))
78                metadata = self.empty_metadata()
79                setattr(metadata, self.supported_fields[0], u"Foo")
80                track.set_metadata(metadata)
81                metadata2 = track.get_metadata()
82                self.assertIsInstance(metadata2, self.metadata_class)
83
84                # also ensure that the new track is playable
85                audiotools.transfer_framelist_data(track.to_pcm(),
86                                                   lambda f: f)
87
88    @METADATA_METADATA
89    def test_attribs(self):
90        import sys
91        import string
92        import random
93
94        # a nice sampling of Unicode characters
95        chars = u"".join([u"".join(map(chr if (sys.version_info[0] >= 3)
96                                       else unichr, l))
97                          for l in [range(0x30, 0x39 + 1),
98                                    range(0x41, 0x5A + 1),
99                                    range(0x61, 0x7A + 1),
100                                    range(0xC0, 0x17E + 1),
101                                    range(0x18A, 0x1EB + 1),
102                                    range(0x3041, 0x3096 + 1),
103                                    range(0x30A1, 0x30FA + 1)]])
104
105        for audio_class in self.supported_formats:
106            with tempfile.NamedTemporaryFile(
107                suffix="." + audio_class.SUFFIX) as temp_file:
108                track = audio_class.from_pcm(temp_file.name,
109                                             BLANK_PCM_Reader(1))
110
111                # check that setting the fields to random values works
112                for field in self.supported_fields:
113                    metadata = self.empty_metadata()
114                    if field not in audiotools.MetaData.INTEGER_FIELDS:
115                        unicode_string = u"".join(
116                            [random.choice(chars)
117                             for i in range(random.choice(range(1, 21)))])
118                        setattr(metadata, field, unicode_string)
119                        track.set_metadata(metadata)
120                        metadata = track.get_metadata()
121                        self.assertEqual(getattr(metadata, field),
122                                         unicode_string)
123                    else:
124                        number = random.choice(range(1, 100))
125                        setattr(metadata, field, number)
126                        track.set_metadata(metadata)
127                        metadata = track.get_metadata()
128                        self.assertEqual(getattr(metadata, field), number)
129
130                # check that blanking out the fields works
131                for field in self.supported_fields:
132                    metadata = self.empty_metadata()
133                    self.assertIsNone(getattr(metadata, field))
134                    if field not in audiotools.MetaData.INTEGER_FIELDS:
135                        setattr(metadata, field, u"")
136                        track.set_metadata(metadata)
137                        metadata = track.get_metadata()
138                        self.assertEqual(getattr(metadata, field), u"")
139                    else:
140                        setattr(metadata, field, None)
141                        track.set_metadata(metadata)
142                        metadata = track.get_metadata()
143                        self.assertIsNone(getattr(metadata, field))
144
145                # re-set the fields with random values
146                for field in self.supported_fields:
147                    metadata = self.empty_metadata()
148                    if field not in audiotools.MetaData.INTEGER_FIELDS:
149                        unicode_string = u"".join(
150                            [random.choice(chars)
151                             for i in range(random.choice(range(1, 21)))])
152                        setattr(metadata, field, unicode_string)
153                        track.set_metadata(metadata)
154                        metadata = track.get_metadata()
155                        self.assertEqual(getattr(metadata, field),
156                                         unicode_string)
157                    else:
158                        number = random.choice(range(1, 100))
159                        setattr(metadata, field, number)
160                        track.set_metadata(metadata)
161                        metadata = track.get_metadata()
162                        self.assertEqual(getattr(metadata, field), number)
163
164                    # check that deleting the fields works
165                    delattr(metadata, field)
166                    track.set_metadata(metadata)
167                    metadata = track.get_metadata()
168                    self.assertEqual(
169                        getattr(metadata, field),
170                        None,
171                        "%s != %s for field %s" % (
172                            repr(getattr(metadata, field)), None, field))
173
174                # check an unsupported field
175                metadata = self.empty_metadata()
176                self.assertRaises(AttributeError,
177                                  getattr,
178                                  metadata,
179                                  "foo")
180
181                metadata.foo = u"foo"
182                self.assertEqual(metadata.foo, u"foo")
183                metadata.foo = u"bar"
184                self.assertEqual(metadata.foo, u"bar")
185
186                del(metadata.foo)
187                self.assertRaises(AttributeError,
188                                  getattr,
189                                  metadata,
190                                  "foo")
191
192    @METADATA_METADATA
193    def test_field_mapping(self):
194        # ensure that setting a class field
195        # updates its corresponding low-level implementation
196
197        # ensure that updating the low-level implementation
198        # is reflected in the class field
199
200        pass
201
202    @METADATA_METADATA
203    def test_foreign_field(self):
204        pass
205
206    @METADATA_METADATA
207    def test_converted(self):
208        # build a generic MetaData with everything
209        image1 = audiotools.Image.new(TEST_COVER1, u"Text 1", 0)
210        image2 = audiotools.Image.new(TEST_COVER2, u"Text 2", 1)
211        image3 = audiotools.Image.new(TEST_COVER3, u"Text 3", 2)
212
213        metadata_orig = audiotools.MetaData(track_name=u"a",
214                                            track_number=1,
215                                            track_total=2,
216                                            album_name=u"b",
217                                            artist_name=u"c",
218                                            performer_name=u"d",
219                                            composer_name=u"e",
220                                            conductor_name=u"f",
221                                            media=u"g",
222                                            ISRC=u"h",
223                                            catalog=u"i",
224                                            copyright=u"j",
225                                            publisher=u"k",
226                                            year=u"l",
227                                            date=u"m",
228                                            album_number=3,
229                                            album_total=4,
230                                            comment=u"n",
231                                            images=[image1, image2, image3])
232
233        # ensure converted() builds something with our class
234        metadata_new = self.metadata_class.converted(metadata_orig)
235        self.assertEqual(metadata_new.__class__, self.metadata_class)
236
237        # ensure our fields match
238        for field in audiotools.MetaData.FIELDS:
239            if field in self.supported_fields:
240                self.assertEqual(getattr(metadata_orig, field),
241                                 getattr(metadata_new, field))
242            else:
243                self.assertIsNone(getattr(metadata_new, field))
244
245        # ensure images match, if supported
246        if self.metadata_class.supports_images():
247            self.assertEqual(metadata_new.images(),
248                             [image1, image2, image3])
249
250        # subclasses should ensure non-MetaData fields are converted
251
252        # ensure that convert() builds a whole new object
253        metadata_new.track_name = u"Foo"
254        self.assertEqual(metadata_new.track_name, u"Foo")
255        metadata_new2 = self.metadata_class.converted(metadata_new)
256        self.assertEqual(metadata_new2.track_name, u"Foo")
257        metadata_new2.track_name = u"Bar"
258        self.assertEqual(metadata_new2.track_name, u"Bar")
259        self.assertEqual(metadata_new.track_name, u"Foo")
260
261    @METADATA_METADATA
262    def test_supports_images(self):
263        self.assertEqual(self.metadata_class.supports_images(), True)
264
265    @METADATA_METADATA
266    def test_images(self):
267        # perform tests only if images are actually supported
268        if self.metadata_class.supports_images():
269            for audio_class in self.supported_formats:
270                temp_file = tempfile.NamedTemporaryFile(
271                    suffix="." + audio_class.SUFFIX)
272                try:
273                    track = audio_class.from_pcm(temp_file.name,
274                                                 BLANK_PCM_Reader(1))
275
276                    metadata = self.empty_metadata()
277                    self.assertEqual(metadata.images(), [])
278
279                    image1 = audiotools.Image.new(TEST_COVER1,
280                                                  u"Text 1", 0)
281                    image2 = audiotools.Image.new(TEST_COVER2,
282                                                  u"Text 2", 1)
283                    image3 = audiotools.Image.new(TEST_COVER3,
284                                                  u"Text 3", 2)
285
286                    track.set_metadata(metadata)
287                    metadata = track.get_metadata()
288
289                    # ensure that adding one image works
290                    metadata.add_image(image1)
291                    track.set_metadata(metadata)
292                    metadata = track.get_metadata()
293                    self.assertIn(image1, metadata.images())
294                    self.assertNotIn(image2, metadata.images())
295                    self.assertNotIn(image3, metadata.images())
296
297                    # ensure that adding a second image works
298                    metadata.add_image(image2)
299                    track.set_metadata(metadata)
300                    metadata = track.get_metadata()
301                    self.assertIn(image1, metadata.images())
302                    self.assertIn(image2, metadata.images())
303                    self.assertNotIn(image3, metadata.images())
304
305                    # ensure that adding a third image works
306                    metadata.add_image(image3)
307                    track.set_metadata(metadata)
308                    metadata = track.get_metadata()
309                    self.assertIn(image1, metadata.images())
310                    self.assertIn(image2, metadata.images())
311                    self.assertIn(image3, metadata.images())
312
313                    # ensure that deleting the first image works
314                    metadata.delete_image(image1)
315                    track.set_metadata(metadata)
316                    metadata = track.get_metadata()
317                    self.assertNotIn(image1, metadata.images())
318                    self.assertIn(image2, metadata.images())
319                    self.assertIn(image3, metadata.images())
320
321                    # ensure that deleting the second image works
322                    metadata.delete_image(image2)
323                    track.set_metadata(metadata)
324                    metadata = track.get_metadata()
325                    self.assertNotIn(image1, metadata.images())
326                    self.assertNotIn(image2, metadata.images())
327                    self.assertIn(image3, metadata.images())
328
329                    # ensure that deleting the third image works
330                    metadata.delete_image(image3)
331                    track.set_metadata(metadata)
332                    metadata = track.get_metadata()
333                    self.assertNotIn(image1, metadata.images())
334                    self.assertNotIn(image2, metadata.images())
335                    self.assertNotIn(image3, metadata.images())
336
337                finally:
338                    temp_file.close()
339
340    @METADATA_METADATA
341    def test_mode(self):
342        import os
343
344        # ensure that setting, updating and deleting metadata
345        # doesn't change the file's original mode
346        for audio_class in self.supported_formats:
347            temp_file = tempfile.NamedTemporaryFile(
348                suffix="." + audio_class.SUFFIX)
349            try:
350                mode = 0o755
351                track = audio_class.from_pcm(temp_file.name,
352                                             BLANK_PCM_Reader(1))
353                original_mode = os.stat(track.filename).st_mode
354
355                os.chmod(track.filename, mode)
356                # may not round-trip as expected on some systems
357                mode = os.stat(track.filename).st_mode
358                self.assertNotEqual(mode, original_mode)
359
360                metadata = self.empty_metadata()
361                metadata.track_name = u"Test 1"
362                track.set_metadata(metadata)
363
364                self.assertEqual(os.stat(track.filename).st_mode, mode)
365
366                metadata = track.get_metadata()
367                metadata.track_name = u"Test 2"
368                track.update_metadata(metadata)
369
370                self.assertEqual(os.stat(track.filename).st_mode, mode)
371
372                track.delete_metadata()
373
374                self.assertEqual(os.stat(track.filename).st_mode, mode)
375            finally:
376                temp_file.close()
377
378    @METADATA_METADATA
379    def test_delete_metadata(self):
380        for audio_class in self.supported_formats:
381            temp_file = tempfile.NamedTemporaryFile(
382                suffix="." + audio_class.SUFFIX)
383            try:
384                track = audio_class.from_pcm(temp_file.name,
385                                             BLANK_PCM_Reader(1))
386
387                self.assertTrue((track.get_metadata() is None) or
388                                (track.get_metadata().track_name is None))
389
390                track.set_metadata(
391                    audiotools.MetaData(track_name=u"Track Name"))
392                self.assertEqual(track.get_metadata().track_name,
393                                 u"Track Name")
394
395                track.delete_metadata()
396                self.assertTrue((track.get_metadata() is None) or
397                                (track.get_metadata().track_name is None))
398
399                track.set_metadata(
400                    audiotools.MetaData(track_name=u"Track Name"))
401                self.assertEqual(track.get_metadata().track_name,
402                                 u"Track Name")
403
404                track.set_metadata(None)
405                self.assertTrue((track.get_metadata() is None) or
406                                (track.get_metadata().track_name is None))
407            finally:
408                temp_file.close()
409
410    @METADATA_METADATA
411    def test_raw_info(self):
412        import sys
413
414        if sys.version_info[0] >= 3:
415            __unicode__ = str
416        else:
417            __unicode__ = unicode
418
419        if self.metadata_class is audiotools.MetaData:
420            return
421
422        # ensure raw_info() returns a Unicode object
423        # and has at least some output
424
425        metadata = self.empty_metadata()
426        for field in self.supported_fields:
427            if field not in audiotools.MetaData.INTEGER_FIELDS:
428                setattr(metadata, field, u"A" * 5)
429            else:
430                setattr(metadata, field, 1)
431        raw_info = metadata.raw_info()
432        self.assertIsInstance(raw_info, __unicode__)
433        self.assertGreater(len(raw_info), 0)
434
435    @LIB_CUESHEET
436    @METADATA_METADATA
437    def test_cuesheet(self):
438        for audio_class in self.supported_formats:
439            if not audio_class.supports_cuesheet():
440                continue
441
442            from audiotools import Sheet, SheetTrack, SheetIndex
443            from fractions import Fraction
444
445            sheet = Sheet(sheet_tracks=[
446                          SheetTrack(
447                              number=1,
448                              track_indexes=[
449                                  SheetIndex(number=1,
450                                             offset=Fraction(0, 1))],
451                              filename=u"CDImage.wav"),
452                          SheetTrack(
453                              number=2,
454                              track_indexes=[
455                                  SheetIndex(number=0,
456                                             offset=Fraction(4507, 25)),
457                                  SheetIndex(number=1,
458                                             offset=Fraction(4557, 25))],
459                              filename=u"CDImage.wav"),
460                          SheetTrack(
461                              number=3,
462                              track_indexes=[
463                                  SheetIndex(number=0,
464                                             offset=Fraction(27013, 75)),
465                                  SheetIndex(number=1,
466                                             offset=Fraction(27161, 75))],
467                              filename=u"CDImage.wav"),
468                          SheetTrack(
469                              number=4,
470                              track_indexes=[
471                                  SheetIndex(number=0,
472                                             offset=Fraction(37757, 75)),
473                                  SheetIndex(number=1,
474                                             offset=Fraction(37907, 75))],
475                              filename=u"CDImage.wav"),
476                          SheetTrack(
477                              number=5,
478                              track_indexes=[
479                                  SheetIndex(number=0,
480                                             offset=Fraction(11213, 15)),
481                                  SheetIndex(number=1,
482                                             offset=Fraction(11243, 15))],
483                              filename=u"CDImage.wav"),
484                          SheetTrack(
485                              number=6,
486                              track_indexes=[
487                                  SheetIndex(number=0,
488                                             offset=Fraction(13081, 15)),
489                                  SheetIndex(number=1,
490                                             offset=Fraction(13111, 15))],
491                              filename=u"CDImage.wav")])
492
493            with tempfile.NamedTemporaryFile(
494                suffix="." + audio_class.SUFFIX) as temp_file:
495                # build empty audio file
496                temp_track = audio_class.from_pcm(
497                    temp_file.name,
498                    EXACT_SILENCE_PCM_Reader(43646652),
499                    total_pcm_frames=43646652)
500
501                # ensure it has no cuesheet
502                self.assertIsNone(temp_track.get_cuesheet())
503
504                # set cuesheet
505                temp_track.set_cuesheet(sheet)
506
507                # ensure its cuesheet matches the original
508                track_sheet = temp_track.get_cuesheet()
509                self.assertIsNotNone(track_sheet)
510                self.assertEqual(track_sheet, sheet)
511
512                # deleting cuesheet should delete cuesheet
513                temp_track.delete_cuesheet()
514                self.assertIsNone(temp_track.get_cuesheet())
515
516                # setting cuesheet to None should delete cuesheet
517                temp_track.set_cuesheet(sheet)
518                self.assertEqual(temp_track.get_cuesheet(), sheet)
519                temp_track.set_cuesheet(None)
520                self.assertIsNone(temp_track.get_cuesheet())
521
522    @LIB_CUESHEET
523    @METADATA_METADATA
524    def test_metadata_independence(self):
525        from audiotools import Sheet, SheetTrack, SheetIndex
526        from fractions import Fraction
527
528        metadata = audiotools.MetaData(track_name=u"Track Name",
529                                       track_number=1)
530
531        replay_gain = audiotools.ReplayGain(track_gain=2.0,
532                                            track_peak=0.25,
533                                            album_gain=1.0,
534                                            album_peak=0.5)
535
536        sheet = Sheet(sheet_tracks=[
537                      SheetTrack(
538                          number=1,
539                          track_indexes=[
540                              SheetIndex(number=1,
541                                         offset=Fraction(0, 1))],
542                          filename=u"CDImage.wav"),
543                      SheetTrack(
544                          number=2,
545                          track_indexes=[
546                              SheetIndex(number=0,
547                                         offset=Fraction(4507, 25)),
548                              SheetIndex(number=1,
549                                         offset=Fraction(4557, 25))],
550                          filename=u"CDImage.wav"),
551                      SheetTrack(
552                          number=3,
553                          track_indexes=[
554                              SheetIndex(number=0,
555                                         offset=Fraction(27013, 75)),
556                              SheetIndex(number=1,
557                                         offset=Fraction(27161, 75))],
558                          filename=u"CDImage.wav"),
559                      SheetTrack(
560                          number=4,
561                          track_indexes=[
562                              SheetIndex(number=0,
563                                         offset=Fraction(37757, 75)),
564                              SheetIndex(number=1,
565                                         offset=Fraction(37907, 75))],
566                          filename=u"CDImage.wav"),
567                      SheetTrack(
568                          number=5,
569                          track_indexes=[
570                              SheetIndex(number=0,
571                                         offset=Fraction(11213, 15)),
572                              SheetIndex(number=1,
573                                         offset=Fraction(11243, 15))],
574                          filename=u"CDImage.wav"),
575                      SheetTrack(
576                          number=6,
577                          track_indexes=[
578                              SheetIndex(number=0,
579                                         offset=Fraction(13081, 15)),
580                              SheetIndex(number=1,
581                                         offset=Fraction(13111, 15))],
582                          filename=u"CDImage.wav")])
583
584        for audio_class in self.supported_formats:
585            with tempfile.NamedTemporaryFile(
586                suffix="." + audio_class.SUFFIX) as temp_file:
587                if audio_class.supports_cuesheet():
588                    track = audio_class.from_pcm(
589                        temp_file.name,
590                        EXACT_SILENCE_PCM_Reader(43646652),
591                        total_pcm_frames=43646652)
592                else:
593                    track = audio_class.from_pcm(
594                        temp_file.name,
595                        EXACT_SILENCE_PCM_Reader(44100 * 5),
596                        total_pcm_frames=44100 * 5)
597
598                self.assertTrue(
599                    (track.get_metadata() is None) or
600                    ((track.get_metadata().track_name is None) and
601                     (track.get_metadata().track_number is None)))
602
603                # if class supports metadata
604                if audio_class.supports_metadata():
605                    # setting metadata should work
606                    track.set_metadata(metadata)
607                    self.assertEqual(track.get_metadata(), metadata)
608
609                    # and deleting metadata should work
610                    track.delete_metadata()
611
612                    # note that some classes can't delete metadata
613                    # entirely since it contains non-textual data
614                    self.assertTrue(
615                        (track.get_metadata() is None) or
616                        ((track.get_metadata().track_name is None) and
617                         (track.get_metadata().track_number is None)))
618
619                    track.set_metadata(metadata)
620                    self.assertEqual(track.get_metadata(), metadata)
621                    track.set_metadata(None)
622                    self.assertTrue(
623                        (track.get_metadata() is None) or
624                        ((track.get_metadata().track_name is None) and
625                         (track.get_metadata().track_number is None)))
626                else:
627                    # otherwise they should do nothing
628                    track.set_metadata(metadata)
629                    self.assertTrue(
630                        (track.get_metadata() is None) or
631                        ((track.get_metadata().track_name is None) and
632                         (track.get_metadata().track_number is None)))
633
634                    track.delete_metadata()
635                    self.assertTrue(
636                        (track.get_metadata() is None) or
637                        ((track.get_metadata().track_name is None) and
638                         (track.get_metadata().track_number is None)))
639
640                    track.set_metadata(None)
641                    self.assertTrue(
642                        (track.get_metadata() is None) or
643                        ((track.get_metadata().track_name is None) and
644                         (track.get_metadata().track_number is None)))
645
646                self.assertIsNone(track.get_replay_gain())
647
648                # if class supports ReplayGain
649                if audio_class.supports_replay_gain():
650                    # setting ReplayGain should work
651                    track.set_replay_gain(replay_gain)
652                    self.assertEqual(track.get_replay_gain(), replay_gain)
653
654                    # and deleting ReplayGain should work
655                    track.delete_replay_gain()
656                    self.assertIsNone(track.get_replay_gain())
657
658                    track.set_replay_gain(replay_gain)
659                    self.assertEqual(track.get_replay_gain(), replay_gain)
660                    track.set_replay_gain(None)
661                    self.assertIsNone(track.get_replay_gain())
662                else:
663                    # otherwise they should do nothing
664                    track.set_replay_gain(replay_gain)
665                    self.assertIsNone(track.get_replay_gain())
666
667                    track.delete_replay_gain()
668                    self.assertIsNone(track.get_replay_gain())
669
670                    track.set_replay_gain(None)
671                    self.assertIsNone(track.get_replay_gain())
672
673                self.assertIsNone(track.get_cuesheet())
674
675                # if class supports cuesheets
676                if audio_class.supports_cuesheet():
677                    # setting cuesheet should work
678                    track.set_cuesheet(sheet)
679                    self.assertEqual(track.get_cuesheet(), sheet)
680
681                    # and deleting cuesheet should work
682                    track.delete_cuesheet()
683                    self.assertIsNone(track.get_cuesheet())
684
685                    track.set_cuesheet(sheet)
686                    self.assertEqual(track.get_cuesheet(), sheet)
687                    track.set_cuesheet(None)
688                    self.assertIsNone(track.get_cuesheet())
689                else:
690                    # otherwise they should do nothing
691                    track.set_cuesheet(sheet)
692                    self.assertIsNone(track.get_cuesheet())
693
694                    track.delete_cuesheet()
695                    self.assertIsNone(track.get_cuesheet())
696
697                    track.set_cuesheet(None)
698                    self.assertIsNone(track.get_cuesheet())
699
700                # deleting metadata doesn't affect
701                # ReplayGain or embedded cuesheet (if supported)
702                track.set_metadata(metadata)
703                track.set_replay_gain(replay_gain)
704                track.set_cuesheet(sheet)
705                track.delete_metadata()
706                if track.supports_replay_gain():
707                    self.assertEqual(track.get_replay_gain(), replay_gain)
708                else:
709                    self.assertIsNone(track.get_replay_gain())
710                if track.supports_cuesheet():
711                    self.assertEqual(track.get_cuesheet(), sheet)
712                else:
713                    self.assertIsNone(track.get_cuesheet())
714
715                # deleting ReplayGain doesn't affect
716                # metadata or embedded cuesheet (if supported)
717                track.set_metadata(metadata)
718                track.set_replay_gain(replay_gain)
719                track.set_cuesheet(sheet)
720                track.delete_replay_gain()
721                if track.supports_metadata():
722                    self.assertEqual(track.get_metadata(), metadata)
723                else:
724                    self.assertIsNone(track.get_metadata())
725                if track.supports_cuesheet():
726                    self.assertEqual(track.get_cuesheet(), sheet)
727                else:
728                    self.assertIsNone(track.get_cuesheet())
729
730                # deleting cuesheet doesn't affect
731                # metadata or ReplayGain (if supported)
732                track.set_metadata(metadata)
733                track.set_replay_gain(replay_gain)
734                track.set_cuesheet(sheet)
735                track.delete_cuesheet()
736                if track.supports_metadata():
737                    self.assertEqual(track.get_metadata(), metadata)
738                else:
739                    self.assertIsNone(track.get_metadata())
740                if track.supports_replay_gain():
741                    self.assertEqual(track.get_replay_gain(), replay_gain)
742                else:
743                    self.assertIsNone(track.get_replay_gain())
744
745    @METADATA_METADATA
746    def test_converted_duplication(self):
747        # ensure the converting a metadata object to its own class
748        # doesn't share the same fields as the original object
749        # so that updating one doesn't update the other
750        metadata1 = self.metadata_class.converted(
751            audiotools.MetaData(track_name=u"Track Name 1",
752                                track_number=1))
753
754        if self.metadata_class.supports_images():
755            metadata1.add_image(audiotools.Image.new(TEST_COVER1,
756                                                     u"",
757                                                     audiotools.FRONT_COVER))
758
759        metadata2 = self.metadata_class.converted(metadata1)
760        self.assertIsNotNone(metadata2)
761        self.assertIsInstance(metadata2, self.metadata_class)
762
763        self.assertEqual(metadata1.track_name, metadata2.track_name)
764        self.assertEqual(metadata1.track_number, metadata2.track_number)
765        self.assertEqual(metadata1.images(), metadata2.images())
766
767        metadata2.track_name = u"Track Name 2"
768        metadata2.track_number = 2
769        self.assertNotEqual(metadata1.track_name, metadata2.track_name)
770        self.assertNotEqual(metadata1.track_number, metadata2.track_number)
771        if self.metadata_class.supports_images():
772            metadata2.delete_image(metadata2.images()[0])
773            self.assertNotEqual(metadata1.images(), metadata2.images())
774
775    @METADATA_METADATA
776    def test_converted_none(self):
777        self.assertIsNone(self.metadata_class.converted(None))
778
779
780class WavPackApeTagMetaData(MetaDataTest):
781    def setUp(self):
782        self.metadata_class = audiotools.ApeTag
783        self.supported_fields = ["track_name",
784                                 "track_number",
785                                 "track_total",
786                                 "album_name",
787                                 "artist_name",
788                                 "performer_name",
789                                 "composer_name",
790                                 "conductor_name",
791                                 "ISRC",
792                                 "catalog",
793                                 "copyright",
794                                 "publisher",
795                                 "year",
796                                 "date",
797                                 "album_number",
798                                 "album_total",
799                                 "comment"]
800        self.supported_formats = [audiotools.WavPackAudio]
801
802    def empty_metadata(self):
803        return self.metadata_class.converted(audiotools.MetaData())
804
805    @METADATA_WAVPACK
806    def test_getitem(self):
807        from audiotools.ape import ApeTag, ApeTagItem
808
809        # getitem with no matches raises KeyError
810        self.assertRaises(KeyError, ApeTag([]).__getitem__, b"Title")
811
812        # getitem with one match returns that item
813        self.assertEqual(ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])[b"Foo"],
814                         ApeTagItem(0, 0, b"Foo", b"Bar"))
815
816        # getitem with multiple matches returns the first match
817        # (this is not a valid ApeTag and should be cleaned)
818        self.assertEqual(ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar"),
819                                 ApeTagItem(0, 0, b"Foo", b"Baz")])[b"Foo"],
820                         ApeTagItem(0, 0, b"Foo", b"Bar"))
821
822        # tag items *are* case-sensitive according to the specification
823        self.assertRaises(
824            KeyError,
825            ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")]).__getitem__,
826            b"foo")
827
828    @METADATA_WAVPACK
829    def test_setitem(self):
830        from audiotools.ape import ApeTag, ApeTagItem
831
832        # setitem adds new key if necessary
833        metadata = ApeTag([])
834        metadata[b"Foo"] = ApeTagItem(0, 0, b"Foo", b"Bar")
835        self.assertEqual(metadata.tags,
836                         [ApeTagItem(0, 0, b"Foo", b"Bar")])
837
838        # setitem replaces matching key with new value
839        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
840        metadata[b"Foo"] = ApeTagItem(0, 0, b"Foo", b"Baz")
841        self.assertEqual(metadata.tags,
842                         [ApeTagItem(0, 0, b"Foo", b"Baz")])
843
844        # setitem leaves other items alone
845        # when adding or replacing tags
846        metadata = ApeTag([ApeTagItem(0, 0, b"Kelp", b"Spam")])
847        metadata[b"Foo"] = ApeTagItem(0, 0, b"Foo", b"Bar")
848        self.assertEqual(metadata.tags,
849                         [ApeTagItem(0, 0, b"Kelp", b"Spam"),
850                          ApeTagItem(0, 0, b"Foo", b"Bar")])
851
852        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar"),
853                           ApeTagItem(0, 0, b"Kelp", b"Spam")])
854        metadata[b"Foo"] = ApeTagItem(0, 0, b"Foo", b"Baz")
855        self.assertEqual(metadata.tags,
856                         [ApeTagItem(0, 0, b"Foo", b"Baz"),
857                          ApeTagItem(0, 0, b"Kelp", b"Spam")])
858
859        # setitem is case-sensitive
860        metadata = ApeTag([ApeTagItem(0, 0, b"foo", b"Spam")])
861        metadata[b"Foo"] = ApeTagItem(0, 0, b"Foo", b"Bar")
862        self.assertEqual(metadata.tags,
863                         [ApeTagItem(0, 0, b"foo", b"Spam"),
864                          ApeTagItem(0, 0, b"Foo", b"Bar")])
865
866    @METADATA_WAVPACK
867    def test_getattr(self):
868        from audiotools.ape import ApeTag, ApeTagItem
869
870        # track_number grabs the first available integer from "Track"
871        self.assertIsNone(ApeTag([]).track_number)
872
873        self.assertEqual(
874            ApeTag([ApeTagItem(0, 0, b"Track", b"2")]).track_number,
875            2)
876
877        self.assertEqual(
878            ApeTag([ApeTagItem(0, 0, b"Track", b"2/3")]).track_number,
879            2)
880
881        self.assertEqual(
882            ApeTag([ApeTagItem(0, 0, b"Track", b"foo 2 bar")]).track_number,
883            2)
884
885        # album_number grabs the first available from "Media"
886        self.assertIsNone(ApeTag([]).album_number)
887
888        self.assertEqual(
889            ApeTag([ApeTagItem(0, 0, b"Media", b"4")]).album_number,
890            4)
891
892        self.assertEqual(
893            ApeTag([ApeTagItem(0, 0, b"Media", b"4/5")]).album_number,
894            4)
895
896        self.assertEqual(
897            ApeTag([ApeTagItem(0, 0, b"Media", b"foo 4 bar")]).album_number,
898            4)
899
900        # track_total grabs the second number in a slashed field, if any
901        self.assertIsNone(ApeTag([]).track_total)
902
903        self.assertEqual(
904            ApeTag([ApeTagItem(0, 0, b"Track", b"2")]).track_total,
905            None)
906
907        self.assertEqual(
908            ApeTag([ApeTagItem(0, 0, b"Track", b"2/3")]).track_total,
909            3)
910
911        self.assertEqual(
912            ApeTag([ApeTagItem(0, 0,
913                               b"Track",
914                               b"foo 2 bar / baz 3 blah")]).track_total,
915            3)
916
917        # album_total grabs the second number in a slashed field, if any
918        self.assertIsNone(ApeTag([]).album_total)
919
920        self.assertEqual(
921            ApeTag([ApeTagItem(0, 0, b"Media", b"4")]).album_total,
922            None)
923
924        self.assertEqual(
925            ApeTag([ApeTagItem(0, 0, b"Media", b"4/5")]).album_total,
926            5)
927
928        self.assertEqual(
929            ApeTag([ApeTagItem(0, 0,
930                               b"Media",
931                               b"foo 4 bar / baz 5 blah")]).album_total,
932            5)
933
934        # other fields grab the first available item
935        # (though proper APEv2 tags should only contain one)
936        self.assertEqual(ApeTag([]).track_name,
937                         None)
938
939        self.assertEqual(
940            ApeTag([ApeTagItem(0, 0, b"Title", b"foo")]).track_name,
941            u"foo")
942
943        self.assertEqual(
944            ApeTag([ApeTagItem(0, 0, b"Title", b"foo"),
945                    ApeTagItem(0, 0, b"Title", b"bar")]).track_name,
946            u"foo")
947
948    @METADATA_WAVPACK
949    def test_setattr(self):
950        from audiotools.ape import ApeTag, ApeTagItem
951
952        # track_number adds new field if necessary
953        metadata = ApeTag([])
954        metadata.track_number = 2
955        self.assertEqual(metadata.tags,
956                         [ApeTagItem(0, 0, b"Track", b"2")])
957        self.assertEqual(metadata.track_number, 2)
958
959        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
960        metadata.track_number = 2
961        self.assertEqual(metadata.tags,
962                         [ApeTagItem(0, 0, b"Foo", b"Bar"),
963                          ApeTagItem(0, 0, b"Track", b"2")])
964        self.assertEqual(metadata.track_number, 2)
965
966        # track_number updates the first integer field
967        # and leaves other junk in that field alone
968        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1")])
969        metadata.track_number = 2
970        self.assertEqual(metadata.tags,
971                         [ApeTagItem(0, 0, b"Track", b"2")])
972        self.assertEqual(metadata.track_number, 2)
973
974        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/3")])
975        metadata.track_number = 2
976        self.assertEqual(metadata.tags,
977                         [ApeTagItem(0, 0, b"Track", b"2/3")])
978        self.assertEqual(metadata.track_number, 2)
979
980        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"foo 1 bar")])
981        metadata.track_number = 2
982        self.assertEqual(metadata.tags,
983                         [ApeTagItem(0, 0, b"Track", b"foo 2 bar")])
984        self.assertEqual(metadata.track_number, 2)
985
986        # album_number adds new field if necessary
987        metadata = ApeTag([])
988        metadata.album_number = 4
989        self.assertEqual(metadata.tags,
990                         [ApeTagItem(0, 0, b"Media", b"4")])
991        self.assertEqual(metadata.album_number, 4)
992
993        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
994        metadata.album_number = 4
995        self.assertEqual(metadata.tags,
996                         [ApeTagItem(0, 0, b"Foo", b"Bar"),
997                          ApeTagItem(0, 0, b"Media", b"4")])
998        self.assertEqual(metadata.album_number, 4)
999
1000        # album_number updates the first integer field
1001        # and leaves other junk in that field alone
1002        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3")])
1003        metadata.album_number = 4
1004        self.assertEqual(metadata.tags,
1005                         [ApeTagItem(0, 0, b"Media", b"4")])
1006        self.assertEqual(metadata.album_number, 4)
1007
1008        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3/5")])
1009        metadata.album_number = 4
1010        self.assertEqual(metadata.tags,
1011                         [ApeTagItem(0, 0, b"Media", b"4/5")])
1012        self.assertEqual(metadata.album_number, 4)
1013
1014        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"foo 3 bar")])
1015        metadata.album_number = 4
1016        self.assertEqual(metadata.tags,
1017                         [ApeTagItem(0, 0, b"Media", b"foo 4 bar")])
1018        self.assertEqual(metadata.album_number, 4)
1019
1020        # track_total adds a new field if necessary
1021        metadata = ApeTag([])
1022        metadata.track_total = 3
1023        self.assertEqual(metadata.tags,
1024                         [ApeTagItem(0, 0, b"Track", b"0/3")])
1025        self.assertEqual(metadata.track_total, 3)
1026
1027        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
1028        metadata.track_total = 3
1029        self.assertEqual(metadata.tags,
1030                         [ApeTagItem(0, 0, b"Foo", b"Bar"),
1031                          ApeTagItem(0, 0, b"Track", b"0/3")])
1032        self.assertEqual(metadata.track_total, 3)
1033
1034        # track_total adds a slashed side of the integer field
1035        # and leaves other junk in that field alone
1036        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1")])
1037        metadata.track_total = 3
1038        self.assertEqual(metadata.tags,
1039                         [ApeTagItem(0, 0, b"Track", b"1/3")])
1040        self.assertEqual(metadata.track_total, 3)
1041
1042        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1  ")])
1043        metadata.track_total = 3
1044        self.assertEqual(metadata.tags,
1045                         [ApeTagItem(0, 0, b"Track", b"1  /3")])
1046        self.assertEqual(metadata.track_total, 3)
1047
1048        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/2")])
1049        metadata.track_total = 3
1050        self.assertEqual(metadata.tags,
1051                         [ApeTagItem(0, 0, b"Track", b"1/3")])
1052        self.assertEqual(metadata.track_total, 3)
1053
1054        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1 / baz 2 blah")])
1055        metadata.track_total = 3
1056        self.assertEqual(metadata.tags,
1057                         [ApeTagItem(0, 0, b"Track", b"1 / baz 3 blah")])
1058        self.assertEqual(metadata.track_total, 3)
1059
1060        metadata = ApeTag([ApeTagItem(0, 0, b"Track",
1061                                      b"foo 1 bar / baz 2 blah")])
1062        metadata.track_total = 3
1063        self.assertEqual(metadata.tags,
1064                         [ApeTagItem(0, 0, b"Track",
1065                                     b"foo 1 bar / baz 3 blah")])
1066        self.assertEqual(metadata.track_total, 3)
1067
1068        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1 / 2 / 4")])
1069        metadata.track_total = 3
1070        self.assertEqual(metadata.tags,
1071                         [ApeTagItem(0, 0, b"Track", b"1 / 3 / 4")])
1072        self.assertEqual(metadata.track_total, 3)
1073
1074        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"foo / 2")])
1075        metadata.track_total = 3
1076        self.assertEqual(metadata.tags,
1077                         [ApeTagItem(0, 0, b"Track", b"foo / 3")])
1078        self.assertEqual(metadata.track_total, 3)
1079
1080        # album_total adds a new field if necessary
1081        metadata = ApeTag([])
1082        metadata.album_total = 5
1083        self.assertEqual(metadata.tags,
1084                         [ApeTagItem(0, 0, b"Media", b"0/5")])
1085        self.assertEqual(metadata.album_total, 5)
1086
1087        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
1088        metadata.album_total = 5
1089        self.assertEqual(metadata.tags,
1090                         [ApeTagItem(0, 0, b"Foo", b"Bar"),
1091                          ApeTagItem(0, 0, b"Media", b"0/5")])
1092        self.assertEqual(metadata.album_total, 5)
1093
1094        # album_total adds a slashed side of the integer field
1095        # and leaves other junk in that field alone
1096        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3")])
1097        metadata.album_total = 5
1098        self.assertEqual(metadata.tags,
1099                         [ApeTagItem(0, 0, b"Media", b"3/5")])
1100        self.assertEqual(metadata.album_total, 5)
1101
1102        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3  ")])
1103        metadata.album_total = 5
1104        self.assertEqual(metadata.tags,
1105                         [ApeTagItem(0, 0, b"Media", b"3  /5")])
1106        self.assertEqual(metadata.album_total, 5)
1107
1108        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3/4")])
1109        metadata.album_total = 5
1110        self.assertEqual(metadata.tags,
1111                         [ApeTagItem(0, 0, b"Media", b"3/5")])
1112        self.assertEqual(metadata.album_total, 5)
1113
1114        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"1 / baz 2 blah")])
1115        metadata.album_total = 5
1116        self.assertEqual(metadata.tags,
1117                         [ApeTagItem(0, 0, b"Media", b"1 / baz 5 blah")])
1118        self.assertEqual(metadata.album_total, 5)
1119
1120        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1121                                      b"foo 1 bar / baz 2 blah")])
1122        metadata.album_total = 5
1123        self.assertEqual(metadata.tags,
1124                         [ApeTagItem(0, 0, b"Media",
1125                                     b"foo 1 bar / baz 5 blah")])
1126        self.assertEqual(metadata.album_total, 5)
1127
1128        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3 / 4 / 6")])
1129        metadata.album_total = 5
1130        self.assertEqual(metadata.tags,
1131                         [ApeTagItem(0, 0, b"Media", b"3 / 5 / 6")])
1132        self.assertEqual(metadata.album_total, 5)
1133
1134        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"foo / 4")])
1135        metadata.album_total = 5
1136        self.assertEqual(metadata.tags,
1137                         [ApeTagItem(0, 0, b"Media", b"foo / 5")])
1138        self.assertEqual(metadata.album_total, 5)
1139
1140        # other fields add a new item if necessary
1141        # while leaving the rest alone
1142        metadata = ApeTag([])
1143        metadata.track_name = u"Track Name"
1144        self.assertEqual(metadata.tags,
1145                         [ApeTagItem(0, 0, b"Title", b"Track Name")])
1146        self.assertEqual(metadata.track_name, u"Track Name")
1147
1148        metadata = ApeTag([ApeTagItem(0, 0, b"Foo", b"Bar")])
1149        metadata.track_name = u"Track Name"
1150        self.assertEqual(metadata.tags,
1151                         [ApeTagItem(0, 0, b"Foo", b"Bar"),
1152                          ApeTagItem(0, 0, b"Title", b"Track Name")])
1153        self.assertEqual(metadata.track_name, u"Track Name")
1154
1155        # other fields update the first match
1156        # while leaving the rest alone
1157        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Blah")])
1158        metadata.track_name = u"Track Name"
1159        self.assertEqual(metadata.tags,
1160                         [ApeTagItem(0, 0, b"Title", b"Track Name")])
1161        self.assertEqual(metadata.track_name, u"Track Name")
1162
1163        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Blah"),
1164                           ApeTagItem(0, 0, b"Title", b"Spam")])
1165        metadata.track_name = u"Track Name"
1166        self.assertEqual(metadata.tags,
1167                         [ApeTagItem(0, 0, b"Title", b"Track Name"),
1168                          ApeTagItem(0, 0, b"Title", b"Spam")])
1169        self.assertEqual(metadata.track_name, u"Track Name")
1170
1171        # setting field to an empty string is okay
1172        metadata = ApeTag([])
1173        metadata.track_name = u""
1174        self.assertEqual(metadata.tags,
1175                         [ApeTagItem(0, 0, b"Title", b"")])
1176        self.assertEqual(metadata.track_name, u"")
1177
1178    @METADATA_WAVPACK
1179    def test_delattr(self):
1180        from audiotools.ape import ApeTag, ApeTagItem
1181
1182        # deleting nonexistent field is okay
1183        for field in audiotools.MetaData.FIELDS:
1184            metadata = ApeTag([])
1185            delattr(metadata, field)
1186            self.assertIsNone(getattr(metadata, field))
1187
1188        # deleting field removes all instances of it
1189        metadata = ApeTag([])
1190        del(metadata.track_name)
1191        self.assertEqual(metadata.tags, [])
1192        self.assertIsNone(metadata.track_name)
1193
1194        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Track Name")])
1195        del(metadata.track_name)
1196        self.assertEqual(metadata.tags, [])
1197        self.assertIsNone(metadata.track_name)
1198
1199        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Track Name"),
1200                           ApeTagItem(0, 0, b"Title", b"Track Name 2")])
1201        del(metadata.track_name)
1202        self.assertEqual(metadata.tags, [])
1203        self.assertIsNone(metadata.track_name)
1204
1205        # setting field to None is the same as deleting field
1206        metadata = ApeTag([])
1207        metadata.track_name = None
1208        self.assertEqual(metadata.tags, [])
1209        self.assertIsNone(metadata.track_name)
1210
1211        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Track Name")])
1212        metadata.track_name = None
1213        self.assertEqual(metadata.tags, [])
1214        self.assertIsNone(metadata.track_name)
1215
1216        metadata = ApeTag([ApeTagItem(0, 0, b"Title", b"Track Name"),
1217                           ApeTagItem(0, 0, b"Title", b"Track Name 2")])
1218        metadata.track_name = None
1219        self.assertEqual(metadata.tags, [])
1220        self.assertIsNone(metadata.track_name)
1221
1222        # deleting track_number without track_total removes "Track" field
1223        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1")])
1224        del(metadata.track_number)
1225        self.assertEqual(metadata.tags, [])
1226        self.assertIsNone(metadata.track_number)
1227
1228        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1")])
1229        metadata.track_number = None
1230        self.assertEqual(metadata.tags, [])
1231        self.assertIsNone(metadata.track_number)
1232
1233        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"foo 1 bar")])
1234        metadata.track_number = None
1235        self.assertEqual(metadata.tags, [])
1236        self.assertIsNone(metadata.track_number)
1237
1238        # deleting track_number with track_total converts track_number to 0
1239        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/2")])
1240        del(metadata.track_number)
1241        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Track", b"0/2")])
1242        self.assertIsNone(metadata.track_number)
1243        self.assertEqual(metadata.track_total, 2)
1244
1245        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/2")])
1246        metadata.track_number = None
1247        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Track", b"0/2")])
1248        self.assertIsNone(metadata.track_number)
1249        self.assertEqual(metadata.track_total, 2)
1250
1251        metadata = ApeTag([ApeTagItem(0, 0, b"Track",
1252                                      b"foo 1 bar / baz 2 blah")])
1253        metadata.track_number = None
1254        self.assertEqual(metadata.tags,
1255                         [ApeTagItem(0, 0, b"Track",
1256                                     b"foo 0 bar / baz 2 blah")])
1257        self.assertIsNone(metadata.track_number)
1258        self.assertEqual(metadata.track_total, 2)
1259
1260        # deleting track_total without track_number removes "Track" field
1261        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"0/2")])
1262        del(metadata.track_total)
1263        self.assertEqual(metadata.tags, [])
1264        self.assertIsNone(metadata.track_number)
1265        self.assertIsNone(metadata.track_total)
1266
1267        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"0/2")])
1268        metadata.track_total = None
1269        self.assertEqual(metadata.tags, [])
1270        self.assertIsNone(metadata.track_number)
1271        self.assertIsNone(metadata.track_total)
1272
1273        metadata = ApeTag([ApeTagItem(0, 0, b"Track",
1274                                      b"foo 0 bar / baz 2 blah")])
1275        metadata.track_total = None
1276        self.assertEqual(metadata.tags, [])
1277        self.assertIsNone(metadata.track_number)
1278        self.assertIsNone(metadata.track_total)
1279
1280        # deleting track_total with track_number removes slashed field
1281        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/2")])
1282        del(metadata.track_total)
1283        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Track", b"1")])
1284        self.assertEqual(metadata.track_number, 1)
1285        self.assertIsNone(metadata.track_total)
1286
1287        metadata = ApeTag([ApeTagItem(0, 0, b"Track", b"1/2/3")])
1288        del(metadata.track_total)
1289        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Track", b"1")])
1290        self.assertEqual(metadata.track_number, 1)
1291        self.assertIsNone(metadata.track_total)
1292
1293        metadata = ApeTag([ApeTagItem(0, 0, b"Track",
1294                                      b"foo 1 bar / baz 2 blah")])
1295        del(metadata.track_total)
1296        self.assertEqual(metadata.tags,
1297                         [ApeTagItem(0, 0, b"Track", b"foo 1 bar")])
1298        self.assertEqual(metadata.track_number, 1)
1299        self.assertIsNone(metadata.track_total)
1300
1301        metadata = ApeTag([ApeTagItem(0, 0, b"Track",
1302                                      b"foo 1 bar / baz 2 blah")])
1303        metadata.track_total = None
1304        self.assertEqual(metadata.tags,
1305                         [ApeTagItem(0, 0, b"Track", b"foo 1 bar")])
1306        self.assertEqual(metadata.track_number, 1)
1307        self.assertIsNone(metadata.track_total)
1308
1309        # deleting album_number without album_total removes "Media" field
1310        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"0/4")])
1311        del(metadata.album_total)
1312        self.assertEqual(metadata.tags, [])
1313        self.assertIsNone(metadata.album_number)
1314        self.assertIsNone(metadata.album_total)
1315
1316        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"0/4")])
1317        metadata.album_total = None
1318        self.assertEqual(metadata.tags, [])
1319        self.assertIsNone(metadata.album_number)
1320        self.assertIsNone(metadata.album_total)
1321
1322        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1323                                      b"foo 0 bar / baz 4 blah")])
1324        metadata.album_total = None
1325        self.assertEqual(metadata.tags, [])
1326        self.assertIsNone(metadata.album_number)
1327        self.assertIsNone(metadata.album_total)
1328
1329        # deleting album_number with album_total converts album_number to 0
1330        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3/4")])
1331        del(metadata.album_number)
1332        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Media", b"0/4")])
1333        self.assertIsNone(metadata.album_number)
1334        self.assertEqual(metadata.album_total, 4)
1335
1336        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"3/4")])
1337        metadata.album_number = None
1338        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Media", b"0/4")])
1339        self.assertIsNone(metadata.album_number)
1340        self.assertEqual(metadata.album_total, 4)
1341
1342        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1343                                      b"foo 3 bar / baz 4 blah")])
1344        metadata.album_number = None
1345        self.assertEqual(metadata.tags,
1346                         [ApeTagItem(0, 0, b"Media",
1347                                     b"foo 0 bar / baz 4 blah")])
1348        self.assertIsNone(metadata.album_number)
1349        self.assertEqual(metadata.album_total, 4)
1350
1351        # deleting album_total without album_number removes "Media" field
1352        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"0/4")])
1353        del(metadata.album_total)
1354        self.assertEqual(metadata.tags, [])
1355        self.assertIsNone(metadata.album_number)
1356        self.assertIsNone(metadata.album_total)
1357
1358        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"0/4")])
1359        metadata.album_total = None
1360        self.assertEqual(metadata.tags, [])
1361        self.assertIsNone(metadata.album_number)
1362        self.assertIsNone(metadata.album_total)
1363
1364        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1365                                      b"foo 0 bar / baz 4 blah")])
1366        metadata.album_total = None
1367        self.assertEqual(metadata.tags, [])
1368        self.assertIsNone(metadata.album_number)
1369        self.assertIsNone(metadata.album_total)
1370
1371        # deleting album_total with album_number removes slashed field
1372        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"1/2")])
1373        del(metadata.album_total)
1374        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Media", b"1")])
1375        self.assertEqual(metadata.album_number, 1)
1376        self.assertIsNone(metadata.album_total)
1377
1378        metadata = ApeTag([ApeTagItem(0, 0, b"Media", b"1/2/3")])
1379        del(metadata.album_total)
1380        self.assertEqual(metadata.tags, [ApeTagItem(0, 0, b"Media", b"1")])
1381        self.assertEqual(metadata.album_number, 1)
1382        self.assertIsNone(metadata.album_total)
1383
1384        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1385                                      b"foo 1 bar / baz 2 blah")])
1386        del(metadata.album_total)
1387        self.assertEqual(metadata.tags,
1388                         [ApeTagItem(0, 0, b"Media", b"foo 1 bar")])
1389        self.assertEqual(metadata.album_number, 1)
1390        self.assertIsNone(metadata.album_total)
1391
1392        metadata = ApeTag([ApeTagItem(0, 0, b"Media",
1393                                      b"foo 1 bar / baz 2 blah")])
1394        metadata.album_total = None
1395        self.assertEqual(metadata.tags,
1396                         [ApeTagItem(0, 0, b"Media", b"foo 1 bar")])
1397        self.assertEqual(metadata.album_number, 1)
1398        self.assertIsNone(metadata.album_total)
1399
1400    @METADATA_WAVPACK
1401    def test_update(self):
1402        import os
1403
1404        for audio_class in self.supported_formats:
1405            with tempfile.NamedTemporaryFile(
1406                suffix="." + audio_class.SUFFIX) as temp_file:
1407                track = audio_class.from_pcm(temp_file.name,
1408                                             BLANK_PCM_Reader(10))
1409                temp_file_stat = os.stat(temp_file.name)[0]
1410
1411                # update_metadata on file's internal metadata round-trips okay
1412                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
1413                metadata = track.get_metadata()
1414                self.assertEqual(metadata.track_name, u"Foo")
1415                metadata.track_name = u"Bar"
1416                track.update_metadata(metadata)
1417                metadata = track.get_metadata()
1418                self.assertEqual(metadata.track_name, u"Bar")
1419
1420                # update_metadata on unwritable file generates IOError
1421                metadata = track.get_metadata()
1422                os.chmod(temp_file.name, 0)
1423                self.assertRaises(IOError,
1424                                  track.update_metadata,
1425                                  metadata)
1426                os.chmod(temp_file.name, temp_file_stat)
1427
1428                # update_metadata with foreign MetaData generates ValueError
1429                self.assertRaises(ValueError,
1430                                  track.update_metadata,
1431                                  audiotools.MetaData(track_name=u"Foo"))
1432
1433                # update_metadata with None makes no changes
1434                track.update_metadata(None)
1435                metadata = track.get_metadata()
1436                self.assertEqual(metadata.track_name, u"Bar")
1437
1438                # replaygain strings not updated with set_metadata()
1439                # but can be updated with update_metadata()
1440                self.assertRaises(KeyError,
1441                                  track.get_metadata().__getitem__,
1442                                  b"replaygain_track_gain")
1443                metadata[b"replaygain_track_gain"] = \
1444                    audiotools.ape.ApeTagItem.string(
1445                        b"replaygain_track_gain", u"???")
1446                track.set_metadata(metadata)
1447                self.assertRaises(KeyError,
1448                                  track.get_metadata().__getitem__,
1449                                  b"replaygain_track_gain")
1450                track.update_metadata(metadata)
1451                self.assertEqual(
1452                    track.get_metadata()[b"replaygain_track_gain"],
1453                    audiotools.ape.ApeTagItem.string(
1454                        b"replaygain_track_gain", u"???"))
1455
1456                # cuesheet not updated with set_metadata()
1457                # but can be updated with update_metadata()
1458                metadata[b"Cuesheet"] = \
1459                    audiotools.ape.ApeTagItem.string(
1460                        b"Cuesheet", u"???")
1461                track.set_metadata(metadata)
1462                self.assertRaises(KeyError,
1463                                  track.get_metadata().__getitem__,
1464                                  b"Cuesheet")
1465                track.update_metadata(metadata)
1466                self.assertEqual(
1467                    track.get_metadata()[b"Cuesheet"],
1468                    audiotools.ape.ApeTagItem.string(
1469                        b"Cuesheet", u"???"))
1470
1471    @METADATA_WAVPACK
1472    def test_foreign_field(self):
1473        metadata = audiotools.ApeTag(
1474            [audiotools.ape.ApeTagItem(0, False, b"Title", b'Track Name'),
1475             audiotools.ape.ApeTagItem(0, False, b"Album", b'Album Name'),
1476             audiotools.ape.ApeTagItem(0, False, b"Track", b"1/3"),
1477             audiotools.ape.ApeTagItem(0, False, b"Media", b"2/4"),
1478             audiotools.ape.ApeTagItem(0, False, b"Foo", b"Bar")])
1479        for format in self.supported_formats:
1480            temp_file = tempfile.NamedTemporaryFile(
1481                suffix="." + format.SUFFIX)
1482            try:
1483                track = format.from_pcm(temp_file.name,
1484                                        BLANK_PCM_Reader(1))
1485                track.set_metadata(metadata)
1486                metadata2 = track.get_metadata()
1487                self.assertEqual(metadata, metadata2)
1488                self.assertEqual(metadata.__class__, metadata2.__class__)
1489                self.assertEqual(metadata2[b"Foo"].__unicode__(), u"Bar")
1490            finally:
1491                temp_file.close()
1492
1493    @METADATA_WAVPACK
1494    def test_field_mapping(self):
1495        mapping = [('track_name', b'Title', u'a'),
1496                   ('album_name', b'Album', u'b'),
1497                   ('artist_name', b'Artist', u'c'),
1498                   ('performer_name', b'Performer', u'd'),
1499                   ('composer_name', b'Composer', u'e'),
1500                   ('conductor_name', b'Conductor', u'f'),
1501                   ('ISRC', b'ISRC', u'g'),
1502                   ('catalog', b'Catalog', u'h'),
1503                   ('publisher', b'Publisher', u'i'),
1504                   ('year', b'Year', u'j'),
1505                   ('date', b'Record Date', u'k'),
1506                   ('comment', b'Comment', u'l')]
1507
1508        for format in self.supported_formats:
1509            with tempfile.NamedTemporaryFile(
1510                suffix="." + format.SUFFIX) as temp_file:
1511                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
1512
1513                # ensure that setting a class field
1514                # updates its corresponding low-level implementation
1515                for (field, key, value) in mapping:
1516                    track.delete_metadata()
1517                    metadata = self.empty_metadata()
1518                    setattr(metadata, field, value)
1519                    self.assertEqual(getattr(metadata, field), value)
1520                    self.assertEqual(metadata[key].__unicode__(), value)
1521                    track.set_metadata(metadata)
1522                    metadata2 = track.get_metadata()
1523                    self.assertEqual(getattr(metadata2, field), value)
1524                    self.assertEqual(metadata2[key].__unicode__(), value)
1525
1526                # ensure that updating the low-level implementation
1527                # is reflected in the class field
1528                for (field, key, value) in mapping:
1529                    track.delete_metadata()
1530                    metadata = self.empty_metadata()
1531                    metadata[key] = audiotools.ape.ApeTagItem.string(
1532                        key, value)
1533                    self.assertEqual(getattr(metadata, field), value)
1534                    self.assertEqual(metadata[key].__unicode__(), value)
1535                    track.set_metadata(metadata)
1536                    metadata2 = track.get_metadata()
1537                    self.assertEqual(getattr(metadata, field), value)
1538                    self.assertEqual(metadata[key].__unicode__(), value)
1539
1540                # ensure that setting numerical fields also
1541                # updates the low-level implementation
1542                track.delete_metadata()
1543                metadata = self.empty_metadata()
1544                metadata.track_number = 1
1545                track.set_metadata(metadata)
1546                metadata = track.get_metadata()
1547                self.assertEqual(metadata[b'Track'].__unicode__(), u'1')
1548                metadata.track_total = 2
1549                track.set_metadata(metadata)
1550                metadata = track.get_metadata()
1551                self.assertEqual(metadata[b'Track'].__unicode__(), u'1/2')
1552                del(metadata.track_total)
1553                track.set_metadata(metadata)
1554                metadata = track.get_metadata()
1555                self.assertEqual(metadata[b'Track'].__unicode__(), u'1')
1556                del(metadata.track_number)
1557                track.set_metadata(metadata)
1558                metadata = track.get_metadata()
1559                self.assertRaises(KeyError,
1560                                  metadata.__getitem__,
1561                                  b'Track')
1562
1563                track.delete_metadata()
1564                metadata = self.empty_metadata()
1565                metadata.album_number = 3
1566                track.set_metadata(metadata)
1567                metadata = track.get_metadata()
1568                self.assertEqual(metadata[b'Media'].__unicode__(), u'3')
1569                metadata.album_total = 4
1570                track.set_metadata(metadata)
1571                metadata = track.get_metadata()
1572                self.assertEqual(metadata[b'Media'].__unicode__(), u'3/4')
1573                del(metadata.album_total)
1574                track.set_metadata(metadata)
1575                metadata = track.get_metadata()
1576                self.assertEqual(metadata[b'Media'].__unicode__(), u'3')
1577                del(metadata.album_number)
1578                track.set_metadata(metadata)
1579                metadata = track.get_metadata()
1580                self.assertRaises(KeyError,
1581                                  metadata.__getitem__,
1582                                  b'Media')
1583
1584                # and ensure updating the low-level implementation
1585                # updates the numerical fields
1586                track.delete_metadata()
1587                metadata = self.empty_metadata()
1588                metadata[b'Track'] = audiotools.ape.ApeTagItem.string(
1589                    b'Track', u"1")
1590                track.set_metadata(metadata)
1591                metadata = track.get_metadata()
1592                self.assertEqual(metadata.track_number, 1)
1593                self.assertIsNone(metadata.track_total)
1594                metadata[b'Track'] = audiotools.ape.ApeTagItem.string(
1595                    b'Track', u"1/2")
1596                track.set_metadata(metadata)
1597                metadata = track.get_metadata()
1598                self.assertEqual(metadata.track_number, 1)
1599                self.assertEqual(metadata.track_total, 2)
1600                metadata[b'Track'] = audiotools.ape.ApeTagItem.string(
1601                    b'Track', u"0/2")
1602                track.set_metadata(metadata)
1603                metadata = track.get_metadata()
1604                self.assertIsNone(metadata.track_number)
1605                self.assertEqual(metadata.track_total, 2)
1606                del(metadata[b'Track'])
1607                track.set_metadata(metadata)
1608                metadata = track.get_metadata()
1609                self.assertIsNone(metadata.track_number)
1610                self.assertIsNone(metadata.track_total)
1611
1612                track.delete_metadata()
1613                metadata = self.empty_metadata()
1614                metadata[b'Media'] = audiotools.ape.ApeTagItem.string(
1615                    b'Media', u"3")
1616                track.set_metadata(metadata)
1617                metadata = track.get_metadata()
1618                self.assertEqual(metadata.album_number, 3)
1619                self.assertIsNone(metadata.album_total)
1620                metadata[b'Media'] = audiotools.ape.ApeTagItem.string(
1621                    b'Media', u"3/4")
1622                track.set_metadata(metadata)
1623                metadata = track.get_metadata()
1624                self.assertEqual(metadata.album_number, 3)
1625                self.assertEqual(metadata.album_total, 4)
1626                metadata[b'Media'] = audiotools.ape.ApeTagItem.string(
1627                    b'Media', u"0/4")
1628                track.set_metadata(metadata)
1629                metadata = track.get_metadata()
1630                self.assertIsNone(metadata.album_number)
1631                self.assertEqual(metadata.album_total, 4)
1632                del(metadata[b'Media'])
1633                track.set_metadata(metadata)
1634                metadata = track.get_metadata()
1635                self.assertIsNone(metadata.album_number)
1636                self.assertIsNone(metadata.album_total)
1637
1638    @METADATA_WAVPACK
1639    def test_converted(self):
1640        # build a generic MetaData with everything
1641        image1 = audiotools.Image.new(TEST_COVER1, u"Text 1", 0)
1642        image2 = audiotools.Image.new(TEST_COVER2, u"Text 2", 1)
1643
1644        metadata_orig = audiotools.MetaData(track_name=u"a",
1645                                            track_number=1,
1646                                            track_total=2,
1647                                            album_name=u"b",
1648                                            artist_name=u"c",
1649                                            performer_name=u"d",
1650                                            composer_name=u"e",
1651                                            conductor_name=u"f",
1652                                            media=u"g",
1653                                            ISRC=u"h",
1654                                            catalog=u"i",
1655                                            copyright=u"j",
1656                                            publisher=u"k",
1657                                            year=u"l",
1658                                            date=u"m",
1659                                            album_number=3,
1660                                            album_total=4,
1661                                            comment=u"n",
1662                                            images=[image1, image2])
1663
1664        # ensure converted() builds something with our class
1665        metadata_new = self.metadata_class.converted(metadata_orig)
1666        self.assertEqual(metadata_new.__class__, self.metadata_class)
1667
1668        # ensure our fields match
1669        for field in audiotools.MetaData.FIELDS:
1670            if field in self.supported_fields:
1671                self.assertEqual(getattr(metadata_orig, field),
1672                                 getattr(metadata_new, field))
1673            else:
1674                self.assertIsNone(getattr(metadata_new, field))
1675
1676        # ensure images match, if supported
1677        self.assertEqual(metadata_new.images(), [image1, image2])
1678
1679        # ensure non-MetaData fields are converted
1680        metadata_orig = self.empty_metadata()
1681        metadata_orig[b'Foo'] = audiotools.ape.ApeTagItem.string(
1682            b'Foo', u'Bar')
1683        metadata_new = self.metadata_class.converted(metadata_orig)
1684        self.assertEqual(metadata_orig[b'Foo'].data,
1685                         metadata_new[b'Foo'].data)
1686
1687        # ensure that convert() builds a whole new object
1688        metadata_new.track_name = u"Foo"
1689        self.assertEqual(metadata_new.track_name, u"Foo")
1690        metadata_new2 = self.metadata_class.converted(metadata_new)
1691        self.assertEqual(metadata_new2.track_name, u"Foo")
1692        metadata_new2.track_name = u"Bar"
1693        self.assertEqual(metadata_new2.track_name, u"Bar")
1694        self.assertEqual(metadata_new.track_name, u"Foo")
1695
1696    @METADATA_WAVPACK
1697    def test_images(self):
1698        for audio_class in self.supported_formats:
1699            with tempfile.NamedTemporaryFile(
1700                suffix="." + audio_class.SUFFIX) as temp_file:
1701                track = audio_class.from_pcm(temp_file.name,
1702                                             BLANK_PCM_Reader(1))
1703
1704                metadata = self.empty_metadata()
1705                self.assertEqual(metadata.images(), [])
1706
1707                image1 = audiotools.Image.new(TEST_COVER1,
1708                                              u"Text 1", 0)
1709                image2 = audiotools.Image.new(TEST_COVER2,
1710                                              u"Text 2", 1)
1711
1712                track.set_metadata(metadata)
1713                metadata = track.get_metadata()
1714
1715                # ensure that adding one image works
1716                metadata.add_image(image1)
1717                track.set_metadata(metadata)
1718                metadata = track.get_metadata()
1719                self.assertEqual(metadata.images(), [image1])
1720
1721                # ensure that adding a second image works
1722                metadata.add_image(image2)
1723                track.set_metadata(metadata)
1724                metadata = track.get_metadata()
1725                self.assertEqual(metadata.images(), [image1,
1726                                                     image2])
1727
1728                # ensure that deleting the first image works
1729                metadata.delete_image(image1)
1730                track.set_metadata(metadata)
1731                metadata = track.get_metadata()
1732                self.assertEqual(metadata.images(), [image2])
1733
1734                # ensure that deleting the second image works
1735                metadata.delete_image(image2)
1736                track.set_metadata(metadata)
1737                metadata = track.get_metadata()
1738                self.assertEqual(metadata.images(), [])
1739
1740    @METADATA_WAVPACK
1741    def test_clean(self):
1742        from audiotools.ape import ApeTag, ApeTagItem
1743        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
1744                                     CLEAN_REMOVE_LEADING_WHITESPACE,
1745                                     CLEAN_REMOVE_EMPTY_TAG,
1746                                     CLEAN_REMOVE_DUPLICATE_TAG,
1747                                     CLEAN_FIX_TAG_FORMATTING)
1748
1749        # although the spec says APEv2 tags should be sorted
1750        # ascending by size, I don't think anybody does this in practice
1751
1752        # check trailing whitespace
1753        metadata = ApeTag(
1754            [ApeTagItem.string(b'Title', u'Foo ')])
1755        self.assertEqual(metadata.track_name, u'Foo ')
1756        self.assertEqual(metadata[b'Title'].data, u'Foo '.encode('utf-8'))
1757        (cleaned, fixes) = metadata.clean()
1758        self.assertEqual(fixes,
1759                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
1760                          {"field": b'Title'.decode('ascii')}])
1761        self.assertEqual(cleaned.track_name, u'Foo')
1762        self.assertEqual(cleaned[b'Title'].data, u'Foo'.encode('utf-8'))
1763
1764        # check leading whitespace
1765        metadata = ApeTag(
1766            [ApeTagItem.string(b'Title', u' Foo')])
1767        self.assertEqual(metadata.track_name, u' Foo')
1768        self.assertEqual(metadata[b'Title'].data, u' Foo'.encode('utf-8'))
1769        (cleaned, fixes) = metadata.clean()
1770        self.assertEqual(fixes,
1771                         [CLEAN_REMOVE_LEADING_WHITESPACE %
1772                          {"field": b'Title'.decode('ascii')}])
1773        self.assertEqual(cleaned.track_name, u'Foo')
1774        self.assertEqual(cleaned[b'Title'].data, u'Foo'.encode('utf-8'))
1775
1776        # check empty fields
1777        metadata = ApeTag(
1778            [ApeTagItem.string(b'Title', u'')])
1779        self.assertEqual(metadata.track_name, u'')
1780        self.assertEqual(metadata[b'Title'].data, u''.encode('utf-8'))
1781        (cleaned, fixes) = metadata.clean()
1782        self.assertEqual(fixes,
1783                         [CLEAN_REMOVE_EMPTY_TAG %
1784                          {"field": b'Title'.decode('ascii')}])
1785        self.assertIsNone(cleaned.track_name)
1786        self.assertRaises(KeyError,
1787                          cleaned.__getitem__,
1788                          b'Title')
1789
1790        # check duplicate fields
1791        metadata = ApeTag(
1792            [ApeTagItem.string(b'Title', u'Track Name 1'),
1793             ApeTagItem.string(b'Title', u'Track Name 2')])
1794        (cleaned, fixes) = metadata.clean()
1795        self.assertEqual(fixes,
1796                         [CLEAN_REMOVE_DUPLICATE_TAG %
1797                          {"field": b'Title'.decode('ascii')}])
1798        self.assertEqual(cleaned.tags,
1799                         [ApeTagItem.string(b'Title', u'Track Name 1')])
1800
1801        # check fields that differ only by case
1802        metadata = ApeTag(
1803            [ApeTagItem.string(b'title', u'Track Name 1'),
1804             ApeTagItem.string(b'Title', u'Track Name 2')])
1805        (cleaned, fixes) = metadata.clean()
1806        self.assertEqual(fixes,
1807                         [CLEAN_REMOVE_DUPLICATE_TAG %
1808                          {"field": b'Title'.decode('ascii')}])
1809        self.assertEqual(cleaned.tags,
1810                         [ApeTagItem.string(b'title', u'Track Name 1')])
1811
1812        # check leading zeroes
1813        metadata = ApeTag(
1814            [ApeTagItem.string(b'Track', u'01')])
1815        self.assertEqual(metadata.track_number, 1)
1816        self.assertIsNone(metadata.track_total)
1817        self.assertEqual(metadata[b'Track'].data, u'01'.encode('utf-8'))
1818        (cleaned, fixes) = metadata.clean()
1819        self.assertEqual(fixes,
1820                         [CLEAN_FIX_TAG_FORMATTING %
1821                          {"field": b'Track'.decode('ascii')}])
1822        self.assertEqual(cleaned.track_number, 1)
1823        self.assertIsNone(cleaned.track_total)
1824        self.assertEqual(cleaned[b'Track'].data, u'1'.encode('utf-8'))
1825
1826        metadata = ApeTag(
1827            [ApeTagItem.string(b'Track', u'01/2')])
1828        self.assertEqual(metadata.track_number, 1)
1829        self.assertEqual(metadata.track_total, 2)
1830        self.assertEqual(metadata[b'Track'].data, u'01/2'.encode('utf-8'))
1831        (cleaned, fixes) = metadata.clean()
1832        self.assertEqual(fixes,
1833                         [CLEAN_FIX_TAG_FORMATTING %
1834                          {"field": b'Track'.decode('ascii')}])
1835        self.assertEqual(cleaned.track_number, 1)
1836        self.assertEqual(cleaned.track_total, 2)
1837        self.assertEqual(cleaned[b'Track'].data, u'1/2'.encode('utf-8'))
1838
1839        metadata = ApeTag(
1840            [ApeTagItem.string(b'Track', u'1/02')])
1841        self.assertEqual(metadata.track_number, 1)
1842        self.assertEqual(metadata.track_total, 2)
1843        self.assertEqual(metadata[b'Track'].data, u'1/02'.encode('utf-8'))
1844        (cleaned, fixes) = metadata.clean()
1845        self.assertEqual(fixes,
1846                         [CLEAN_FIX_TAG_FORMATTING %
1847                          {"field": b'Track'.decode('ascii')}])
1848        self.assertEqual(cleaned.track_number, 1)
1849        self.assertEqual(cleaned.track_total, 2)
1850        self.assertEqual(cleaned[b'Track'].data, u'1/2'.encode('utf-8'))
1851
1852        metadata = ApeTag(
1853            [ApeTagItem.string(b'Track', u'01/02')])
1854        self.assertEqual(metadata.track_number, 1)
1855        self.assertEqual(metadata.track_total, 2)
1856        self.assertEqual(metadata[b'Track'].data, u'01/02'.encode('utf-8'))
1857        (cleaned, fixes) = metadata.clean()
1858        self.assertEqual(fixes,
1859                         [CLEAN_FIX_TAG_FORMATTING %
1860                          {"field": b'Track'.decode('ascii')}])
1861        self.assertEqual(cleaned.track_number, 1)
1862        self.assertEqual(cleaned.track_total, 2)
1863        self.assertEqual(cleaned[b'Track'].data, u'1/2'.encode('utf-8'))
1864
1865        # check junk in slashed fields
1866        metadata = ApeTag(
1867            [ApeTagItem.string(b'Track', u'1/foo')])
1868        (cleaned, fixes) = metadata.clean()
1869        self.assertEqual(fixes,
1870                         [CLEAN_FIX_TAG_FORMATTING %
1871                          {"field": b'Track'.decode('ascii')}])
1872        self.assertEqual(cleaned.tags,
1873                         [ApeTagItem.string(b'Track', u'1')])
1874
1875        metadata = ApeTag(
1876            [ApeTagItem.string(b'Track', u'foo/2')])
1877        (cleaned, fixes) = metadata.clean()
1878        self.assertEqual(fixes,
1879                         [CLEAN_FIX_TAG_FORMATTING %
1880                          {"field": b'Track'.decode('ascii')}])
1881        self.assertEqual(cleaned.tags,
1882                         [ApeTagItem.string(b'Track', u'0/2')])
1883
1884        metadata = ApeTag(
1885            [ApeTagItem.string(b'Track', u'1/ baz 2 blah')])
1886        (cleaned, fixes) = metadata.clean()
1887        self.assertEqual(fixes,
1888                         [CLEAN_FIX_TAG_FORMATTING %
1889                          {"field": b'Track'.decode('ascii')}])
1890        self.assertEqual(cleaned.tags,
1891                         [ApeTagItem.string(b'Track', u'1/2')])
1892
1893        metadata = ApeTag(
1894            [ApeTagItem.string(b'Track', u'foo 1 bar /2')])
1895        (cleaned, fixes) = metadata.clean()
1896        self.assertEqual(fixes,
1897                         [CLEAN_FIX_TAG_FORMATTING %
1898                          {"field": b'Track'.decode('ascii')}])
1899        self.assertEqual(cleaned.tags,
1900                         [ApeTagItem.string(b'Track', u'1/2')])
1901
1902        metadata = ApeTag(
1903            [ApeTagItem.string(b'Track', u'foo 1 bar / baz 2 blah')])
1904        (cleaned, fixes) = metadata.clean()
1905        self.assertEqual(fixes,
1906                         [CLEAN_FIX_TAG_FORMATTING %
1907                          {"field": b'Track'.decode('ascii')}])
1908        self.assertEqual(cleaned.tags,
1909                         [ApeTagItem.string(b'Track', u'1/2')])
1910
1911        metadata = ApeTag(
1912            [ApeTagItem.string(b'Track', u'1/2/3')])
1913        (cleaned, fixes) = metadata.clean()
1914        self.assertEqual(fixes,
1915                         [CLEAN_FIX_TAG_FORMATTING %
1916                          {"field": b'Track'.decode('ascii')}])
1917        self.assertEqual(cleaned.tags,
1918                         [ApeTagItem.string(b'Track', u'1/2')])
1919
1920        metadata = ApeTag(
1921            [ApeTagItem.string(b'Track', u'1 / 2 / 3')])
1922        (cleaned, fixes) = metadata.clean()
1923        self.assertEqual(fixes,
1924                         [CLEAN_FIX_TAG_FORMATTING %
1925                          {"field": b'Track'.decode('ascii')}])
1926        self.assertEqual(cleaned.tags,
1927                         [ApeTagItem.string(b'Track', u'1/2')])
1928
1929        # images don't store metadata,
1930        # so no need to check their fields
1931
1932    @METADATA_WAVPACK
1933    def test_replay_gain(self):
1934        import test_streams
1935
1936        for input_class in [audiotools.WavPackAudio]:
1937            with tempfile.NamedTemporaryFile(
1938                suffix="." + input_class.SUFFIX) as temp1:
1939                track1 = input_class.from_pcm(
1940                    temp1.name,
1941                    test_streams.Sine16_Stereo(44100, 44100,
1942                                               441.0, 0.50,
1943                                               4410.0, 0.49, 1.0))
1944                self.assertIsNone(track1.get_replay_gain(),
1945                                  "ReplayGain present for class %s" %
1946                                  (input_class.NAME))
1947                track1.set_metadata(audiotools.MetaData(track_name=u"Foo"))
1948                audiotools.add_replay_gain([track1])
1949                self.assertEqual(track1.get_metadata().track_name, u"Foo")
1950                self.assertIsNotNone(track1.get_replay_gain(),
1951                                     "ReplayGain not present for class %s" %
1952                                     (input_class.NAME))
1953
1954                for output_class in [audiotools.WavPackAudio]:
1955                    with tempfile.NamedTemporaryFile(
1956                        suffix="." + input_class.SUFFIX) as temp2:
1957                        track2 = output_class.from_pcm(
1958                            temp2.name,
1959                            test_streams.Sine16_Stereo(66150, 44100,
1960                                                       8820.0, 0.70,
1961                                                       4410.0, 0.29, 1.0))
1962
1963                        # ensure that ReplayGain doesn't get ported
1964                        # via set_metadata()
1965                        self.assertIsNone(
1966                            track2.get_replay_gain(),
1967                            "ReplayGain present for class %s" %
1968                            (output_class.NAME))
1969                        track2.set_metadata(track1.get_metadata())
1970                        self.assertEqual(track2.get_metadata().track_name,
1971                                         u"Foo")
1972                        self.assertIsNone(
1973                            track2.get_replay_gain(),
1974                            "ReplayGain present for class %s from %s" %
1975                            (output_class.NAME, input_class.NAME))
1976
1977                        # and if ReplayGain is already set,
1978                        # ensure set_metadata() doesn't remove it
1979                        audiotools.add_replay_gain([track2])
1980                        old_replay_gain = track2.get_replay_gain()
1981                        self.assertIsNotNone(old_replay_gain)
1982                        track2.set_metadata(
1983                            audiotools.MetaData(track_name=u"Bar"))
1984                        self.assertEqual(track2.get_metadata().track_name,
1985                                         u"Bar")
1986                        self.assertEqual(track2.get_replay_gain(),
1987                                         old_replay_gain)
1988
1989
1990class ID3v1MetaData(MetaDataTest):
1991    def setUp(self):
1992        self.metadata_class = audiotools.ID3v1Comment
1993        self.supported_fields = ["track_name",
1994                                 "track_number",
1995                                 "album_name",
1996                                 "artist_name",
1997                                 "year",
1998                                 "comment"]
1999        self.supported_formats = [audiotools.MP3Audio,
2000                                  audiotools.MP2Audio]
2001
2002    def empty_metadata(self):
2003        return self.metadata_class()
2004
2005    @METADATA_ID3V1
2006    def test_update(self):
2007        import os
2008
2009        for audio_class in self.supported_formats:
2010            temp_file = tempfile.NamedTemporaryFile(
2011                suffix="." + audio_class.SUFFIX)
2012            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
2013            temp_file_stat = os.stat(temp_file.name)[0]
2014            try:
2015                # update_metadata on file's internal metadata round-trips okay
2016                metadata = self.empty_metadata()
2017                metadata.track_name = u"Foo"
2018                track.set_metadata(metadata)
2019                metadata = track.get_metadata()
2020                self.assertEqual(metadata.track_name, u"Foo")
2021                metadata.track_name = u"Bar"
2022                track.update_metadata(metadata)
2023                metadata = track.get_metadata()
2024                self.assertEqual(metadata.track_name, u"Bar")
2025
2026                # update_metadata on unwritable file generates IOError
2027                metadata = track.get_metadata()
2028                os.chmod(temp_file.name, 0)
2029                self.assertRaises(IOError,
2030                                  track.update_metadata,
2031                                  metadata)
2032                os.chmod(temp_file.name, temp_file_stat)
2033
2034                # update_metadata with foreign MetaData generates ValueError
2035                self.assertRaises(ValueError,
2036                                  track.update_metadata,
2037                                  audiotools.MetaData(track_name=u"Foo"))
2038
2039                # update_metadata with None makes no changes
2040                track.update_metadata(None)
2041                metadata = track.get_metadata()
2042                self.assertEqual(metadata.track_name, u"Bar")
2043            finally:
2044                temp_file.close()
2045
2046    @METADATA_ID3V1
2047    def test_supports_images(self):
2048        self.assertEqual(self.metadata_class.supports_images(), False)
2049
2050    @METADATA_ID3V1
2051    def test_attribs(self):
2052        import sys
2053        import string
2054        import random
2055
2056        # ID3v1 only supports ASCII characters
2057        # and not very many of them
2058        chars = u"".join([u"".join(map(chr if (sys.version_info[0] >= 3)
2059                                       else unichr, l))
2060                          for l in [range(0x30, 0x39 + 1),
2061                                    range(0x41, 0x5A + 1),
2062                                    range(0x61, 0x7A + 1)]])
2063
2064        for audio_class in self.supported_formats:
2065            temp_file = tempfile.NamedTemporaryFile(
2066                suffix="." + audio_class.SUFFIX)
2067            try:
2068                track = audio_class.from_pcm(temp_file.name,
2069                                             BLANK_PCM_Reader(1))
2070
2071                # check that setting the fields to random values works
2072                for field in self.supported_fields:
2073                    metadata = self.empty_metadata()
2074                    if field not in audiotools.MetaData.INTEGER_FIELDS:
2075                        unicode_string = u"".join(
2076                            [random.choice(chars)
2077                             for i in range(random.choice(range(1, 5)))])
2078                        setattr(metadata, field, unicode_string)
2079                        track.set_metadata(metadata)
2080                        metadata = track.get_metadata()
2081                        self.assertEqual(getattr(metadata, field),
2082                                         unicode_string)
2083                    else:
2084                        number = random.choice(range(1, 100))
2085                        setattr(metadata, field, number)
2086                        track.set_metadata(metadata)
2087                        metadata = track.get_metadata()
2088                        self.assertEqual(getattr(metadata, field), number)
2089
2090                # check that overlong fields are truncated
2091                for field in self.supported_fields:
2092                    metadata = self.empty_metadata()
2093                    if field not in audiotools.MetaData.INTEGER_FIELDS:
2094                        unicode_string = u"a" * 50
2095                        setattr(metadata, field, unicode_string)
2096                        track.set_metadata(metadata)
2097                        metadata = track.get_metadata()
2098                        if field == "comment":
2099                            self.assertEqual(getattr(metadata, field),
2100                                             u"a" * 28)
2101                        elif field == "year":
2102                            self.assertEqual(getattr(metadata, field),
2103                                             u"a" * 4)
2104                        else:
2105                            self.assertEqual(getattr(metadata, field),
2106                                             u"a" * 30)
2107
2108                # check that blanking out the fields works
2109                for field in self.supported_fields:
2110                    metadata = self.empty_metadata()
2111                    if field not in audiotools.MetaData.INTEGER_FIELDS:
2112                        setattr(metadata, field, u"")
2113                        track.set_metadata(metadata)
2114                        metadata = track.get_metadata()
2115                        self.assertIsNone(getattr(metadata, field))
2116                    else:
2117                        setattr(metadata, field, 0)
2118                        track.set_metadata(metadata)
2119                        metadata = track.get_metadata()
2120                        self.assertIsNone(getattr(metadata, field))
2121
2122                # re-set the fields with random values
2123                for field in self.supported_fields:
2124                    metadata = self.empty_metadata()
2125                    if field not in audiotools.MetaData.INTEGER_FIELDS:
2126                        unicode_string = u"".join(
2127                            [random.choice(chars)
2128                             for i in range(random.choice(range(1, 5)))])
2129                        setattr(metadata, field, unicode_string)
2130                        track.set_metadata(metadata)
2131                        metadata = track.get_metadata()
2132                        self.assertEqual(getattr(metadata, field),
2133                                         unicode_string)
2134                    else:
2135                        number = random.choice(range(1, 100))
2136                        setattr(metadata, field, number)
2137                        track.set_metadata(metadata)
2138                        metadata = track.get_metadata()
2139                        self.assertEqual(getattr(metadata, field), number)
2140
2141                # check that deleting the fields works
2142                for field in self.supported_fields:
2143                    metadata = self.empty_metadata()
2144                    delattr(metadata, field)
2145                    track.set_metadata(metadata)
2146                    metadata = track.get_metadata()
2147                    self.assertIsNone(getattr(metadata, field))
2148
2149            finally:
2150                temp_file.close()
2151
2152    @METADATA_ID3V1
2153    def test_field_mapping(self):
2154        mapping = [('track_name', u'a'),
2155                   ('artist_name', u'b'),
2156                   ('album_name', u'c'),
2157                   ('year', u'1234'),
2158                   ('comment', u'd'),
2159                   ('track_number', 1)]
2160
2161        for format in self.supported_formats:
2162            temp_file = tempfile.NamedTemporaryFile(suffix="." + format.SUFFIX)
2163            try:
2164                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
2165
2166                # ensure that setting a class field
2167                # updates its corresponding low-level implementation
2168                for (field, value) in mapping:
2169                    track.delete_metadata()
2170                    metadata = self.empty_metadata()
2171                    setattr(metadata, field, value)
2172                    self.assertEqual(getattr(metadata, field), value)
2173                    track.set_metadata(metadata)
2174                    metadata2 = track.get_metadata()
2175                    self.assertEqual(getattr(metadata2, field), value)
2176
2177                # ID3v1 no longer has a low-level implementation
2178                # since it builds and parses directly on strings
2179            finally:
2180                temp_file.close()
2181
2182    @METADATA_ID3V1
2183    def test_clean(self):
2184        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
2185                                     CLEAN_REMOVE_LEADING_WHITESPACE)
2186
2187        # check trailing whitespace
2188        metadata = audiotools.ID3v1Comment(track_name=u"Title ")
2189        (cleaned, results) = metadata.clean()
2190        self.assertEqual(results,
2191                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
2192                          {"field": u"title"}])
2193        self.assertEqual(
2194            cleaned,
2195            audiotools.ID3v1Comment(track_name=u"Title"))
2196
2197        # check leading whitespace
2198        metadata = audiotools.ID3v1Comment(track_name=u" Title")
2199        (cleaned, results) = metadata.clean()
2200        self.assertEqual(results,
2201                         [CLEAN_REMOVE_LEADING_WHITESPACE %
2202                          {"field": u"title"}])
2203        self.assertEqual(
2204            cleaned,
2205            audiotools.ID3v1Comment(track_name=u"Title"))
2206
2207        # ID3v1 has no empty fields, image data or leading zeroes
2208        # so those can be safely ignored
2209
2210
2211class ID3v22MetaData(MetaDataTest):
2212    def setUp(self):
2213        self.metadata_class = audiotools.ID3v22Comment
2214        self.supported_fields = ["track_name",
2215                                 "track_number",
2216                                 "track_total",
2217                                 "album_name",
2218                                 "artist_name",
2219                                 "performer_name",
2220                                 "composer_name",
2221                                 "conductor_name",
2222                                 "media",
2223                                 "ISRC",
2224                                 "copyright",
2225                                 "publisher",
2226                                 "year",
2227                                 "date",
2228                                 "album_number",
2229                                 "album_total",
2230                                 "comment"]
2231        self.supported_formats = [audiotools.MP3Audio,
2232                                  audiotools.MP2Audio,
2233                                  audiotools.AiffAudio]
2234
2235    def empty_metadata(self):
2236        return self.metadata_class([])
2237
2238    def text_tag(self, attribute, unicode_text):
2239        return self.metadata_class.TEXT_FRAME.converted(
2240            self.metadata_class.ATTRIBUTE_MAP[attribute],
2241            unicode_text)
2242
2243    def unknown_tag(self, binary_string):
2244        from audiotools.id3 import ID3v22_Frame
2245
2246        return ID3v22_Frame(b"XXX", binary_string)
2247
2248    @METADATA_ID3V2
2249    def test_update(self):
2250        import os
2251
2252        for audio_class in self.supported_formats:
2253            temp_file = tempfile.NamedTemporaryFile(
2254                suffix="." + audio_class.SUFFIX)
2255            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
2256            temp_file_stat = os.stat(temp_file.name)[0]
2257            try:
2258                # update_metadata on file's internal metadata round-trips okay
2259                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
2260                metadata = track.get_metadata()
2261                self.assertEqual(metadata.track_name, u"Foo")
2262                metadata.track_name = u"Bar"
2263                track.update_metadata(metadata)
2264                metadata = track.get_metadata()
2265                self.assertEqual(metadata.track_name, u"Bar")
2266
2267                # update_metadata on unwritable file generates IOError
2268                metadata = track.get_metadata()
2269                os.chmod(temp_file.name, 0)
2270                self.assertRaises(IOError,
2271                                  track.update_metadata,
2272                                  metadata)
2273                os.chmod(temp_file.name, temp_file_stat)
2274
2275                # update_metadata with foreign MetaData generates ValueError
2276                self.assertRaises(ValueError,
2277                                  track.update_metadata,
2278                                  audiotools.MetaData(track_name=u"Foo"))
2279
2280                # update_metadata with None makes no changes
2281                track.update_metadata(None)
2282                metadata = track.get_metadata()
2283                self.assertEqual(metadata.track_name, u"Bar")
2284            finally:
2285                temp_file.close()
2286
2287    @METADATA_ID3V2
2288    def test_foreign_field(self):
2289        metadata = audiotools.ID3v22Comment(
2290            [audiotools.id3.ID3v22_T__Frame(b"TT2", 0, b"Track Name"),
2291             audiotools.id3.ID3v22_T__Frame(b"TAL", 0, b"Album Name"),
2292             audiotools.id3.ID3v22_T__Frame(b"TRK", 0, b"1/3"),
2293             audiotools.id3.ID3v22_T__Frame(b"TPA", 0, b"2/4"),
2294             audiotools.id3.ID3v22_T__Frame(b"TFO", 0, b"Bar")])
2295        for format in self.supported_formats:
2296            temp_file = tempfile.NamedTemporaryFile(
2297                suffix="." + format.SUFFIX)
2298            try:
2299                track = format.from_pcm(temp_file.name,
2300                                        BLANK_PCM_Reader(1))
2301                track.set_metadata(metadata)
2302                metadata2 = track.get_metadata()
2303                self.assertEqual(metadata, metadata2)
2304                self.assertEqual(metadata.__class__, metadata2.__class__)
2305                self.assertEqual(metadata[b"TFO"][0].data, b"Bar")
2306            finally:
2307                temp_file.close()
2308
2309    @METADATA_ID3V2
2310    def test_field_mapping(self):
2311        from audiotools.id3 import __padded__ as padded
2312        from audiotools.id3 import __number_pair__ as number_pair
2313
2314        id3_class = self.metadata_class
2315
2316        INTEGER_ATTRIBS = ('track_number',
2317                           'track_total',
2318                           'album_number',
2319                           'album_total')
2320
2321        attribs1 = {}  # a dict of attribute -> value pairs
2322                       # ("track_name":u"foo")
2323        attribs2 = {}  # a dict of ID3v2 -> value pairs
2324                       # ("TT2":u"foo")
2325        for (i,
2326             (attribute, key)) in enumerate(id3_class.ATTRIBUTE_MAP.items()):
2327            if attribute not in INTEGER_ATTRIBS:
2328                attribs1[attribute] = attribs2[key] = u"value %d" % (i)
2329        attribs1["track_number"] = 2
2330        attribs1["track_total"] = 10
2331        attribs1["album_number"] = 1
2332        attribs1["album_total"] = 3
2333
2334        id3 = id3_class.converted(audiotools.MetaData(**attribs1))
2335
2336        # ensure that all the attributes match up
2337        for (attribute, value) in attribs1.items():
2338            self.assertEqual(getattr(id3, attribute), value)
2339
2340        # ensure that all the keys for non-integer items match up
2341        for (key, value) in attribs2.items():
2342            self.assertEqual(u"%s" % (id3[key][0],), value)
2343
2344        # ensure the keys for integer items match up
2345        self.assertEqual(
2346            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]][0].number(),
2347            attribs1["track_number"])
2348        self.assertEqual(
2349            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]][0].total(),
2350            attribs1["track_total"])
2351        self.assertEqual(
2352            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]][0].number(),
2353            attribs1["album_number"])
2354        self.assertEqual(
2355            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]][0].total(),
2356            attribs1["album_total"])
2357
2358        # ensure that changing attributes changes the underlying frame
2359        # >>> id3.track_name = u"bar"
2360        # >>> id3['TT2'][0] == u"bar"
2361        for (i,
2362             (attribute, key)) in enumerate(id3_class.ATTRIBUTE_MAP.items()):
2363            if key not in id3_class.TEXT_FRAME.NUMERICAL_IDS:
2364                setattr(id3, attribute, u"new value %d" % (i))
2365                self.assertEqual(u"%s" % (id3[key][0],),
2366                                 u"new value %d" % (i))
2367
2368        # ensure that changing integer attributes changes the underlying frame
2369        # >>> id3.track_number = 2
2370        # >>> id3['TRK'][0] == u"2"
2371        id3.track_number = 3
2372        id3.track_total = None
2373        self.assertEqual(
2374            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]][0].__unicode__(),
2375            padded(3))
2376
2377        id3.track_total = 8
2378        self.assertEqual(
2379            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]][0].__unicode__(),
2380            number_pair(3, 8))
2381
2382        id3.album_number = 2
2383        id3.album_total = None
2384        self.assertEqual(
2385            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]][0].__unicode__(),
2386            padded(2))
2387
2388        id3.album_total = 4
2389        self.assertEqual(
2390            id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]][0].__unicode__(),
2391            number_pair(2, 4))
2392
2393        # reset and re-check everything for the next round
2394        id3 = id3_class.converted(audiotools.MetaData(**attribs1))
2395
2396        # ensure that all the attributes match up
2397        for (attribute, value) in attribs1.items():
2398            self.assertEqual(getattr(id3, attribute), value)
2399
2400        for (key, value) in attribs2.items():
2401            if key not in id3_class.TEXT_FRAME.NUMERICAL_IDS:
2402                self.assertEqual(id3[key][0].__unicode__(), value)
2403            else:
2404                self.assertEqual(int(id3[key][0]), value)
2405
2406        # ensure that changing the underlying frames changes attributes
2407        # >>> id3['TT2'] = [ID3v22_T__Frame('TT2, u"bar")]
2408        # >>> id3.track_name == u"bar"
2409        for (i,
2410             (attribute, key)) in enumerate(id3_class.ATTRIBUTE_MAP.items()):
2411            if attribute not in INTEGER_ATTRIBS:
2412                id3[key] = [id3_class.TEXT_FRAME(
2413                    key, 0, (u"new value %d" % (i)).encode("ascii"))]
2414                self.assertEqual(getattr(id3, attribute),
2415                                 u"new value %d" % (i))
2416
2417        # ensure that changing the underlying integer frames changes attributes
2418        key = id3_class.TEXT_FRAME.NUMERICAL_IDS[0]
2419        id3[key] = [id3_class.TEXT_FRAME(key, 0, b"7")]
2420        self.assertEqual(id3.track_number, 7)
2421
2422        id3[key] = [id3_class.TEXT_FRAME(key, 0, b"8/9")]
2423        self.assertEqual(id3.track_number, 8)
2424        self.assertEqual(id3.track_total, 9)
2425
2426        key = id3_class.TEXT_FRAME.NUMERICAL_IDS[1]
2427        id3[key] = [id3_class.TEXT_FRAME(key, 0, b"4")]
2428        self.assertEqual(id3.album_number, 4)
2429
2430        id3[key] = [id3_class.TEXT_FRAME(key, 0, b"5/6")]
2431        self.assertEqual(id3.album_number, 5)
2432        self.assertEqual(id3.album_total, 6)
2433
2434        # finally, just for kicks, ensure that explicitly setting
2435        # frames also changes attributes
2436        # >>> id3['TT2'] = [id3_class.TEXT_FRAME.from_unicode('TT2',u"foo")]
2437        # >>> id3.track_name = u"foo"
2438        for (i,
2439             (attribute, key)) in enumerate(id3_class.ATTRIBUTE_MAP.items()):
2440            if attribute not in INTEGER_ATTRIBS:
2441                id3[key] = [id3_class.TEXT_FRAME.converted(key, u"%s" % (i,))]
2442                self.assertEqual(getattr(id3, attribute), u"%s" % (i,))
2443
2444        # and ensure explicitly setting integer frames also changes attribs
2445        id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]] = [
2446            id3_class.TEXT_FRAME.converted(
2447                id3_class.TEXT_FRAME.NUMERICAL_IDS[0],
2448                u"4")]
2449        self.assertEqual(id3.track_number, 4)
2450        self.assertIsNone(id3.track_total)
2451
2452        id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[0]] = [
2453            id3_class.TEXT_FRAME.converted(
2454                id3_class.TEXT_FRAME.NUMERICAL_IDS[0],
2455                u"2/10")]
2456        self.assertEqual(id3.track_number, 2)
2457        self.assertEqual(id3.track_total, 10)
2458
2459        id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]] = [
2460            id3_class.TEXT_FRAME.converted(
2461                id3_class.TEXT_FRAME.NUMERICAL_IDS[1],
2462                u"3")]
2463        self.assertEqual(id3.album_number, 3)
2464        self.assertIsNone(id3.album_total)
2465
2466        id3[id3_class.TEXT_FRAME.NUMERICAL_IDS[1]] = [
2467            id3_class.TEXT_FRAME.converted(
2468                id3_class.TEXT_FRAME.NUMERICAL_IDS[1],
2469                u"5/7")]
2470        self.assertEqual(id3.album_number, 5)
2471        self.assertEqual(id3.album_total, 7)
2472
2473    @METADATA_ID3V2
2474    def test_getitem(self):
2475        field = self.metadata_class.ATTRIBUTE_MAP["track_name"]
2476
2477        # getitem with no matches raises KeyError
2478        metadata = self.metadata_class([])
2479        self.assertRaises(KeyError,
2480                          metadata.__getitem__,
2481                          field)
2482
2483        metadata = self.metadata_class([self.unknown_tag(b"FOO")])
2484        self.assertRaises(KeyError,
2485                          metadata.__getitem__,
2486                          field)
2487
2488        # getitem with one match returns that item
2489        metadata = self.metadata_class([self.text_tag("track_name",
2490                                                      u"Track Name")])
2491        self.assertEqual(metadata[field],
2492                         [self.text_tag("track_name",
2493                                        u"Track Name")])
2494
2495        metadata = self.metadata_class([self.text_tag("track_name",
2496                                                      u"Track Name"),
2497                                        self.unknown_tag(b"FOO")])
2498        self.assertEqual(metadata[field],
2499                         [self.text_tag("track_name",
2500                                        u"Track Name")])
2501
2502        # getitem with multiple matches returns all items, in order
2503        metadata = self.metadata_class([self.text_tag("track_name", u"1"),
2504                                        self.text_tag("track_name", u"2"),
2505                                        self.text_tag("track_name", u"3")])
2506        self.assertEqual(metadata[field],
2507                         [self.text_tag("track_name", u"1"),
2508                          self.text_tag("track_name", u"2"),
2509                          self.text_tag("track_name", u"3")])
2510
2511        metadata = self.metadata_class([self.text_tag("track_name", u"1"),
2512                                        self.unknown_tag(b"FOO"),
2513                                        self.text_tag("track_name", u"2"),
2514                                        self.unknown_tag(b"BAR"),
2515                                        self.text_tag("track_name", u"3")])
2516        self.assertEqual(metadata[field],
2517                         [self.text_tag("track_name", u"1"),
2518                          self.text_tag("track_name", u"2"),
2519                          self.text_tag("track_name", u"3")])
2520
2521    @METADATA_ID3V2
2522    def test_setitem(self):
2523        field = self.metadata_class.ATTRIBUTE_MAP["track_name"]
2524
2525        # setitem replaces all keys with new values
2526        # - zero new values
2527        metadata = self.metadata_class([])
2528        metadata[field] = []
2529        self.assertIsNone(metadata.track_name)
2530        self.assertEqual(metadata.frames, [])
2531
2532        metadata = self.metadata_class([self.text_tag("track_name", u"X")])
2533        metadata[field] = []
2534        self.assertIsNone(metadata.track_name)
2535        self.assertEqual(metadata.frames, [])
2536
2537        metadata = self.metadata_class([self.text_tag("track_name", u"X"),
2538                                        self.text_tag("track_name", u"Y")])
2539        metadata[field] = []
2540        self.assertIsNone(metadata.track_name)
2541        self.assertEqual(metadata.frames, [])
2542
2543        # - one new value
2544        metadata = self.metadata_class([])
2545        metadata[field] = [self.text_tag("track_name", u"A")]
2546        self.assertEqual(metadata.track_name, u"A")
2547        self.assertEqual(metadata.frames,
2548                         [self.text_tag("track_name", u"A")])
2549
2550        metadata = self.metadata_class([self.text_tag("track_name", u"X")])
2551        metadata[field] = [self.text_tag("track_name", u"A")]
2552        self.assertEqual(metadata.track_name, u"A")
2553        self.assertEqual(metadata.frames,
2554                         [self.text_tag("track_name", u"A")])
2555
2556        metadata = self.metadata_class([self.text_tag("track_name", u"X"),
2557                                        self.text_tag("track_name", u"Y")])
2558        metadata[field] = [self.text_tag("track_name", u"A")]
2559        self.assertEqual(metadata.track_name, u"A")
2560        self.assertEqual(metadata.frames,
2561                         [self.text_tag("track_name", u"A")])
2562
2563        # - two new values
2564        metadata = self.metadata_class([])
2565        metadata[field] = [self.text_tag("track_name", u"A"),
2566                           self.text_tag("track_name", u"B")]
2567        self.assertEqual(metadata.track_name, u"A")
2568        self.assertEqual(metadata.frames,
2569                         [self.text_tag("track_name", u"A"),
2570                          self.text_tag("track_name", u"B")])
2571
2572        metadata = self.metadata_class([self.text_tag("track_name", u"X")])
2573        metadata[field] = [self.text_tag("track_name", u"A"),
2574                           self.text_tag("track_name", u"B")]
2575        self.assertEqual(metadata.track_name, u"A")
2576        self.assertEqual(metadata.frames,
2577                         [self.text_tag("track_name", u"A"),
2578                          self.text_tag("track_name", u"B")])
2579
2580        metadata = self.metadata_class([self.text_tag("track_name", u"X"),
2581                                        self.text_tag("track_name", u"Y")])
2582        metadata[field] = [self.text_tag("track_name", u"A"),
2583                           self.text_tag("track_name", u"B")]
2584        self.assertEqual(metadata.track_name, u"A")
2585        self.assertEqual(metadata.frames,
2586                         [self.text_tag("track_name", u"A"),
2587                          self.text_tag("track_name", u"B")])
2588
2589        # setitem leaves other items alone
2590        metadata = self.metadata_class([self.unknown_tag(b"FOO")])
2591        metadata[field] = []
2592        self.assertIsNone(metadata.track_name)
2593        self.assertEqual(metadata.frames, [self.unknown_tag(b"FOO")])
2594
2595        metadata = self.metadata_class([self.unknown_tag(b"FOO"),
2596                                        self.text_tag("track_name", u"X")])
2597        metadata[field] = [self.text_tag("track_name", u"A")]
2598        self.assertEqual(metadata.track_name, u"A")
2599        self.assertEqual(metadata.frames,
2600                         [self.unknown_tag(b"FOO"),
2601                          self.text_tag("track_name", u"A")])
2602
2603        metadata = self.metadata_class([self.text_tag("track_name", u"X"),
2604                                        self.unknown_tag(b"FOO"),
2605                                        self.text_tag("track_name", u"Y")])
2606        metadata[field] = [self.text_tag("track_name", u"A"),
2607                           self.text_tag("track_name", u"B")]
2608        self.assertEqual(metadata.track_name, u"A")
2609        self.assertEqual(metadata.frames,
2610                         [self.text_tag("track_name", u"A"),
2611                          self.unknown_tag(b"FOO"),
2612                          self.text_tag("track_name", u"B")])
2613
2614    @METADATA_ID3V2
2615    def test_getattr(self):
2616        # track_number grabs the first available integer, if any
2617        metadata = self.metadata_class([])
2618        self.assertIsNone(metadata.track_number)
2619
2620        metadata = self.metadata_class([
2621            self.text_tag("track_number", u"1")])
2622        self.assertEqual(metadata.track_number, 1)
2623
2624        metadata = self.metadata_class([
2625            self.text_tag("track_number", u"foo")])
2626        self.assertIsNone(metadata.track_number)
2627
2628        metadata = self.metadata_class([
2629            self.text_tag("track_number", u"1/2")])
2630        self.assertEqual(metadata.track_number, 1)
2631
2632        metadata = self.metadata_class([
2633            self.text_tag("track_number", u"foo 1 bar")])
2634        self.assertEqual(metadata.track_number, 1)
2635
2636        # album_number grabs the first available integer, if any
2637        metadata = self.metadata_class([])
2638        self.assertIsNone(metadata.album_number)
2639
2640        metadata = self.metadata_class([
2641            self.text_tag("album_number", u"2")])
2642        self.assertEqual(metadata.album_number, 2)
2643
2644        metadata = self.metadata_class([
2645            self.text_tag("album_number", u"foo")])
2646        self.assertIsNone(metadata.album_number)
2647
2648        metadata = self.metadata_class([
2649            self.text_tag("album_number", u"2/4")])
2650        self.assertEqual(metadata.album_number, 2)
2651
2652        metadata = self.metadata_class([
2653            self.text_tag("album_number", u"foo 2 bar")])
2654        self.assertEqual(metadata.album_number, 2)
2655
2656        # track_total grabs the first slashed field integer, if any
2657        metadata = self.metadata_class([])
2658        self.assertIsNone(metadata.track_total)
2659
2660        metadata = self.metadata_class([
2661            self.text_tag("track_number", u"1")])
2662        self.assertIsNone(metadata.track_total)
2663
2664        metadata = self.metadata_class([
2665            self.text_tag("track_number", u"foo")])
2666        self.assertIsNone(metadata.track_total)
2667
2668        metadata = self.metadata_class([
2669            self.text_tag("track_number", u"1/2")])
2670        self.assertEqual(metadata.track_total, 2)
2671
2672        metadata = self.metadata_class([
2673            self.text_tag("track_number", u"foo 1 bar / baz 2 blah")])
2674        self.assertEqual(metadata.track_total, 2)
2675
2676        # album_total grabs the first slashed field integer, if any
2677        metadata = self.metadata_class([])
2678        self.assertIsNone(metadata.album_total)
2679
2680        metadata = self.metadata_class([
2681            self.text_tag("album_number", u"2")])
2682        self.assertIsNone(metadata.album_total)
2683
2684        metadata = self.metadata_class([
2685            self.text_tag("album_number", u"foo")])
2686        self.assertIsNone(metadata.album_total)
2687
2688        metadata = self.metadata_class([
2689            self.text_tag("album_number", u"2/4")])
2690        self.assertEqual(metadata.album_total, 4)
2691
2692        metadata = self.metadata_class([
2693            self.text_tag("album_number", u"foo 2 bar / baz 4 blah")])
2694        self.assertEqual(metadata.album_total, 4)
2695
2696        # other fields grab the first available item, if any
2697        metadata = self.metadata_class([])
2698        self.assertIsNone(metadata.track_name)
2699
2700        metadata = self.metadata_class([self.text_tag("track_name", u"1")])
2701        self.assertEqual(metadata.track_name, u"1")
2702
2703        metadata = self.metadata_class([self.text_tag("track_name", u"1"),
2704                                        self.text_tag("track_name", u"2")])
2705        self.assertEqual(metadata.track_name, u"1")
2706
2707    @METADATA_ID3V2
2708    def test_setattr(self):
2709        from audiotools.id3 import __padded__ as padded
2710        from audiotools.id3 import __number_pair__ as number_pair
2711
2712        # track_number adds new field if necessary
2713        metadata = self.metadata_class([])
2714        metadata.track_number = 1
2715        self.assertEqual(metadata.track_number, 1)
2716        self.assertEqual(metadata.frames,
2717                         [self.text_tag("track_number",
2718                                        number_pair(1, None))])
2719
2720        # track_number updates the first integer field
2721        # and leaves other junk in that field alone
2722        metadata = self.metadata_class([
2723            self.text_tag("track_number", u"6")])
2724        metadata.track_number = 1
2725        self.assertEqual(metadata.track_number, 1)
2726        self.assertEqual(metadata.frames,
2727                         [self.text_tag("track_number",
2728                                        number_pair(1, None))])
2729
2730        metadata = self.metadata_class([
2731            self.text_tag("track_number", u"6"),
2732            self.text_tag("track_number", u"10")])
2733        metadata.track_number = 1
2734        self.assertEqual(metadata.track_number, 1)
2735        self.assertEqual(metadata.frames,
2736                         [self.text_tag("track_number",
2737                                        number_pair(1, None)),
2738                          self.text_tag("track_number", u"10")])
2739
2740        metadata = self.metadata_class([
2741            self.text_tag("track_number", u"6/2")])
2742        metadata.track_number = 1
2743        self.assertEqual(metadata.track_number, 1)
2744        self.assertEqual(metadata.frames,
2745                         [self.text_tag("track_number",
2746                                        u"%s/2" % (padded(1)))])
2747
2748        metadata = self.metadata_class([
2749            self.text_tag("track_number", u"foo 6 bar")])
2750        metadata.track_number = 1
2751        self.assertEqual(metadata.track_number, 1)
2752        self.assertEqual(metadata.frames,
2753                         [self.text_tag("track_number",
2754                                        u"foo %s bar" % (padded(1)))])
2755
2756        metadata = self.metadata_class([
2757            self.text_tag("track_number", u"foo 6 bar / blah 7 baz")])
2758        metadata.track_number = 1
2759        self.assertEqual(metadata.track_number, 1)
2760        self.assertEqual(metadata.frames,
2761                         [self.text_tag(
2762                             "track_number",
2763                             u"foo %s bar / blah 7 baz" % (padded(1)))])
2764
2765        # album_number adds new field if necessary
2766        metadata = self.metadata_class([])
2767        metadata.album_number = 3
2768        self.assertEqual(metadata.album_number, 3)
2769        self.assertEqual(metadata.frames,
2770                         [self.text_tag("album_number",
2771                                        padded(3))])
2772
2773        # album_number updates the first integer field
2774        # and leaves other junk in that field alone
2775        metadata = self.metadata_class([
2776            self.text_tag("album_number", u"7")])
2777        metadata.album_number = 3
2778        self.assertEqual(metadata.album_number, 3)
2779        self.assertEqual(metadata.frames,
2780                         [self.text_tag("album_number",
2781                                        padded(3))])
2782
2783        metadata = self.metadata_class([
2784            self.text_tag("album_number", u"7"),
2785            self.text_tag("album_number", u"10")])
2786        metadata.album_number = 3
2787        self.assertEqual(metadata.album_number, 3)
2788        self.assertEqual(metadata.frames,
2789                         [self.text_tag("album_number",
2790                                        padded(3)),
2791                          self.text_tag("album_number", u"10")])
2792
2793        metadata = self.metadata_class([
2794            self.text_tag("album_number", u"7/4")])
2795        metadata.album_number = 3
2796        self.assertEqual(metadata.album_number, 3)
2797        self.assertEqual(metadata.frames,
2798                         [self.text_tag("album_number",
2799                                        u"%s/4" % (padded(3)))])
2800
2801        metadata = self.metadata_class([
2802            self.text_tag("album_number", u"foo 7 bar")])
2803        metadata.album_number = 3
2804        self.assertEqual(metadata.album_number, 3)
2805        self.assertEqual(metadata.frames,
2806                         [self.text_tag("album_number",
2807                                        u"foo %s bar" % (padded(3)))])
2808
2809        metadata = self.metadata_class([
2810            self.text_tag("album_number", u"foo 7 bar / blah 8 baz")])
2811        metadata.album_number = 3
2812        self.assertEqual(metadata.album_number, 3)
2813        self.assertEqual(metadata.frames,
2814                         [self.text_tag(
2815                             "album_number",
2816                             u"foo %s bar / blah 8 baz" % (padded(3)))])
2817
2818        # track_total adds new field if necessary
2819        metadata = self.metadata_class([])
2820        metadata.track_total = 2
2821        self.assertEqual(metadata.track_total, 2)
2822        self.assertEqual(metadata.frames,
2823                         [self.text_tag("track_number",
2824                                        number_pair(0, 2))])
2825
2826        # track_total updates the second integer field
2827        # and leaves other junk in that field alone
2828        metadata = self.metadata_class([
2829            self.text_tag("track_number", u"6")])
2830        metadata.track_total = 2
2831        self.assertEqual(metadata.track_total, 2)
2832        self.assertEqual(metadata.frames,
2833                         [self.text_tag("track_number",
2834                                        u"6/%s" % (padded(2)))])
2835
2836        metadata = self.metadata_class([
2837            self.text_tag("track_number", u"6"),
2838            self.text_tag("track_number", u"10")])
2839        metadata.track_total = 2
2840        self.assertEqual(metadata.track_total, 2)
2841        self.assertEqual(metadata.frames,
2842                         [self.text_tag("track_number",
2843                                         u"6/%s" % (padded(2))),
2844                          self.text_tag("track_number", u"10")])
2845
2846        metadata = self.metadata_class([
2847            self.text_tag("track_number", u"6/7")])
2848        metadata.track_total = 2
2849        self.assertEqual(metadata.track_total, 2)
2850        self.assertEqual(metadata.frames,
2851                         [self.text_tag("track_number",
2852                                        u"6/%s" % (padded(2)))])
2853
2854        metadata = self.metadata_class([
2855            self.text_tag("track_number", u"foo 6 bar / blah 7 baz")])
2856        metadata.track_total = 2
2857        self.assertEqual(metadata.track_total, 2)
2858        self.assertEqual(metadata.frames,
2859                         [self.text_tag(
2860                             "track_number",
2861                             u"foo 6 bar / blah %s baz" % (padded(2)))])
2862
2863        # album_total adds new field if necessary
2864        metadata = self.metadata_class([])
2865        metadata.album_total = 4
2866        self.assertEqual(metadata.album_total, 4)
2867        self.assertEqual(metadata.frames,
2868                         [self.text_tag("album_number",
2869                                        number_pair(0, 4))])
2870
2871        # album_total updates the second integer field
2872        # and leaves other junk in that field alone
2873        metadata = self.metadata_class([
2874            self.text_tag("album_number", u"9")])
2875        metadata.album_total = 4
2876        self.assertEqual(metadata.album_total, 4)
2877        self.assertEqual(metadata.frames,
2878                         [self.text_tag("album_total",
2879                                        u"9/%s" % (padded(4)))])
2880
2881        metadata = self.metadata_class([
2882            self.text_tag("album_number", u"9"),
2883            self.text_tag("album_number", u"10")])
2884        metadata.album_total = 4
2885        self.assertEqual(metadata.album_total, 4)
2886        self.assertEqual(metadata.frames,
2887                         [self.text_tag("album_number", u"9/%s" %
2888                                        (padded(4))),
2889                          self.text_tag("album_number", u"10")])
2890
2891        metadata = self.metadata_class([
2892            self.text_tag("album_number", u"9/10")])
2893        metadata.album_total = 4
2894        self.assertEqual(metadata.album_total, 4)
2895        self.assertEqual(metadata.frames,
2896                         [self.text_tag("album_number",
2897                                        u"9/%s" % (padded(4)))])
2898
2899        metadata = self.metadata_class([
2900            self.text_tag("album_total", u"foo 9 bar / blah 10 baz")])
2901        metadata.album_total = 4
2902        self.assertEqual(metadata.album_total, 4)
2903        self.assertEqual(metadata.frames,
2904                         [self.text_tag(
2905                             "album_number",
2906                             u"foo 9 bar / blah %s baz" % (padded(4)))])
2907
2908        # other fields update the first match
2909        # while leaving the rest alone
2910        metadata = self.metadata_class([])
2911        metadata.track_name = u"A"
2912        self.assertEqual(metadata.track_name, u"A")
2913        self.assertEqual(metadata.frames,
2914                         [self.text_tag("track_name", u"A")])
2915
2916        metadata = self.metadata_class([self.text_tag("track_name", u"X")])
2917        metadata.track_name = u"A"
2918        self.assertEqual(metadata.track_name, u"A")
2919        self.assertEqual(metadata.frames,
2920                         [self.text_tag("track_name", u"A")])
2921
2922        metadata = self.metadata_class([self.text_tag("track_name", u"X"),
2923                                        self.text_tag("track_name", u"Y")])
2924        metadata.track_name = u"A"
2925        self.assertEqual(metadata.track_name, u"A")
2926        self.assertEqual(metadata.frames,
2927                         [self.text_tag("track_name", u"A"),
2928                          self.text_tag("track_name", u"Y")])
2929
2930        # setting field to an empty string is okay
2931        metadata = self.metadata_class([])
2932        metadata.track_name = u""
2933        self.assertEqual(metadata.track_name, u"")
2934        self.assertEqual(metadata.frames,
2935                         [self.text_tag("track_name", u"")])
2936
2937    @METADATA_ID3V2
2938    def test_delattr(self):
2939        # deleting nonexistent field is okay
2940        for field in audiotools.MetaData.FIELDS:
2941            metadata = self.metadata_class([])
2942            delattr(metadata, field)
2943            self.assertIsNone(getattr(metadata, field))
2944
2945        # deleting field removes all instances of it
2946        metadata = self.metadata_class([self.text_tag("track_name", u"A")])
2947        del(metadata.track_name)
2948        self.assertIsNone(metadata.track_name)
2949        self.assertEqual(metadata.frames, [])
2950
2951        metadata = self.metadata_class([self.text_tag("track_name", u"A"),
2952                                        self.text_tag("track_name", u"B")])
2953        del(metadata.track_name)
2954        self.assertIsNone(metadata.track_name)
2955        self.assertEqual(metadata.frames, [])
2956
2957        # setting field to None is the same as deleting field
2958        for field in audiotools.MetaData.FIELDS:
2959            metadata = self.metadata_class([])
2960            setattr(metadata, field, None)
2961            self.assertIsNone(getattr(metadata, field))
2962
2963        metadata = self.metadata_class([self.text_tag("track_name", u"A")])
2964        metadata.track_name = None
2965        self.assertIsNone(metadata.track_name)
2966        self.assertEqual(metadata.frames, [])
2967
2968        metadata = self.metadata_class([self.text_tag("track_name", u"A"),
2969                                        self.text_tag("track_name", u"B")])
2970        metadata.track_name = None
2971        self.assertIsNone(metadata.track_name)
2972        self.assertEqual(metadata.frames, [])
2973
2974        # deleting track_number without track_total removes field
2975        metadata = self.metadata_class([self.text_tag("track_number", u"1")])
2976        del(metadata.track_number)
2977        self.assertIsNone(metadata.track_number)
2978        self.assertEqual(metadata.frames, [])
2979
2980        metadata = self.metadata_class([self.text_tag("track_number", u"1"),
2981                                        self.text_tag("track_number", u"2")])
2982        del(metadata.track_number)
2983        self.assertIsNone(metadata.track_number)
2984        self.assertEqual(metadata.frames, [])
2985
2986        metadata = self.metadata_class([self.text_tag("track_number",
2987                                                      u"foo 1 bar")])
2988        del(metadata.track_number)
2989        self.assertIsNone(metadata.track_number)
2990        self.assertEqual(metadata.frames, [])
2991
2992        # deleting track_number with track_total converts track_number to None
2993        metadata = self.metadata_class([self.text_tag("track_number", u"1/2")])
2994        del(metadata.track_number)
2995        self.assertIsNone(metadata.track_number)
2996        self.assertEqual(metadata.track_total, 2)
2997        self.assertEqual(metadata.frames,
2998                         [self.text_tag("track_number", u"0/2")])
2999
3000        metadata = self.metadata_class([self.text_tag(
3001            "track_number", u"foo 1 bar / blah 2 baz")])
3002        del(metadata.track_number)
3003        self.assertIsNone(metadata.track_number)
3004        self.assertEqual(metadata.track_total, 2)
3005        self.assertEqual(metadata.frames,
3006                         [self.text_tag("track_number",
3007                                        u"foo 0 bar / blah 2 baz")])
3008
3009        # deleting track_total without track_number removes field
3010        metadata = self.metadata_class([self.text_tag(
3011            "track_number", u"0/1")])
3012        del(metadata.track_total)
3013        self.assertIsNone(metadata.track_total)
3014        self.assertEqual(metadata.frames, [])
3015
3016        metadata = self.metadata_class([self.text_tag(
3017            "track_number", u"foo 0 bar / 1")])
3018        del(metadata.track_total)
3019        self.assertIsNone(metadata.track_total)
3020        self.assertEqual(metadata.frames, [])
3021
3022        metadata = self.metadata_class([self.text_tag(
3023            "track_number", u"foo / 1")])
3024        del(metadata.track_total)
3025        self.assertIsNone(metadata.track_total)
3026        self.assertEqual(metadata.frames, [])
3027
3028        # deleting track_total with track_number removes slashed field
3029        metadata = self.metadata_class([self.text_tag(
3030            "track_number", u"1/2")])
3031        del(metadata.track_total)
3032        self.assertEqual(metadata.track_number, 1)
3033        self.assertIsNone(metadata.track_total)
3034        self.assertEqual(metadata.frames,
3035                         [self.text_tag("track_number", u"1")])
3036
3037        metadata = self.metadata_class([self.text_tag(
3038            "track_number", u"1 / 2")])
3039        del(metadata.track_total)
3040        self.assertEqual(metadata.track_number, 1)
3041        self.assertIsNone(metadata.track_total)
3042        self.assertEqual(metadata.frames,
3043                         [self.text_tag("track_number", u"1")])
3044
3045        metadata = self.metadata_class([self.text_tag(
3046            "track_number", u"foo 1 bar / baz 2 blah")])
3047        del(metadata.track_total)
3048        self.assertEqual(metadata.track_number, 1)
3049        self.assertIsNone(metadata.track_total)
3050        self.assertEqual(metadata.frames,
3051                         [self.text_tag("track_number", u"foo 1 bar")])
3052
3053        # deleting album_number without album_total removes field
3054        metadata = self.metadata_class([self.text_tag("album_number", u"3")])
3055        del(metadata.album_number)
3056        self.assertIsNone(metadata.album_number)
3057        self.assertEqual(metadata.frames, [])
3058
3059        metadata = self.metadata_class([self.text_tag("album_number", u"3"),
3060                                        self.text_tag("album_number", u"4")])
3061        del(metadata.album_number)
3062        self.assertIsNone(metadata.album_number)
3063        self.assertEqual(metadata.frames, [])
3064
3065        metadata = self.metadata_class([self.text_tag("album_number",
3066                                                      u"foo 3 bar")])
3067        del(metadata.album_number)
3068        self.assertIsNone(metadata.album_number)
3069        self.assertEqual(metadata.frames, [])
3070
3071        # deleting album_number with album_total converts album_number to None
3072        metadata = self.metadata_class([self.text_tag("album_number", u"3/4")])
3073        del(metadata.album_number)
3074        self.assertIsNone(metadata.album_number)
3075        self.assertEqual(metadata.album_total, 4)
3076        self.assertEqual(metadata.frames,
3077                         [self.text_tag("album_number", u"0/4")])
3078
3079        metadata = self.metadata_class([self.text_tag(
3080            "album_number", u"foo 3 bar / blah 4 baz")])
3081        del(metadata.album_number)
3082        self.assertIsNone(metadata.album_number)
3083        self.assertEqual(metadata.album_total, 4)
3084        self.assertEqual(metadata.frames,
3085                         [self.text_tag("album_number",
3086                                        u"foo 0 bar / blah 4 baz")])
3087
3088        # deleting album_total without album_number removes field
3089        metadata = self.metadata_class([self.text_tag(
3090            "album_number", u"0/1")])
3091        del(metadata.album_total)
3092        self.assertIsNone(metadata.album_total)
3093        self.assertEqual(metadata.frames, [])
3094
3095        metadata = self.metadata_class([self.text_tag(
3096            "album_number", u"foo 0 bar / 1")])
3097        del(metadata.album_total)
3098        self.assertIsNone(metadata.album_total)
3099        self.assertEqual(metadata.frames, [])
3100
3101        metadata = self.metadata_class([self.text_tag(
3102            "album_number", u"foo / 1")])
3103        del(metadata.album_total)
3104        self.assertIsNone(metadata.album_total)
3105        self.assertEqual(metadata.frames, [])
3106
3107        # deleting album_total with album_number removes slashed field
3108        metadata = self.metadata_class([self.text_tag(
3109            "album_number", u"3/4")])
3110        del(metadata.album_total)
3111        self.assertEqual(metadata.album_number, 3)
3112        self.assertIsNone(metadata.album_total)
3113        self.assertEqual(metadata.frames,
3114                         [self.text_tag("album_number", u"3")])
3115
3116        metadata = self.metadata_class([self.text_tag(
3117            "album_number", u"3 / 4")])
3118        del(metadata.album_total)
3119        self.assertEqual(metadata.album_number, 3)
3120        self.assertIsNone(metadata.album_total)
3121        self.assertEqual(metadata.frames,
3122                         [self.text_tag("album_number", u"3")])
3123
3124        metadata = self.metadata_class([self.text_tag(
3125            "album_number", u"foo 3 bar / baz 4 blah")])
3126        del(metadata.album_total)
3127        self.assertEqual(metadata.album_number, 3)
3128        self.assertIsNone(metadata.album_total)
3129        self.assertEqual(metadata.frames,
3130                         [self.text_tag("album_number", u"foo 3 bar")])
3131
3132    @METADATA_ID3V2
3133    def test_sync_safe(self):
3134        from audiotools.id3 import decode_syncsafe32, encode_syncsafe32
3135
3136        # ensure values round-trip correctly across several bytes
3137        for value in range(16384):
3138            self.assertEqual(decode_syncsafe32(encode_syncsafe32(value)),
3139                             value)
3140
3141        self.assertEqual(decode_syncsafe32(encode_syncsafe32(2 ** 28 - 1)),
3142                         2 ** 28 - 1)
3143
3144        # ensure values that are too large don't decode
3145        self.assertRaises(ValueError, decode_syncsafe32, 2 ** 32)
3146
3147        # ensure negative values don't decode
3148        self.assertRaises(ValueError, decode_syncsafe32, -1)
3149
3150        # ensure values with invalid padding don't decode
3151        self.assertRaises(ValueError, decode_syncsafe32, 0x80)
3152        self.assertRaises(ValueError, decode_syncsafe32, 0x80 << 8)
3153        self.assertRaises(ValueError, decode_syncsafe32, 0x80 << 16)
3154        self.assertRaises(ValueError, decode_syncsafe32, 0x80 << 24)
3155
3156        # ensure values that are too large don't encode
3157        self.assertRaises(ValueError, encode_syncsafe32, 2 ** 28)
3158
3159        # ensure values that are negative don't encode
3160        self.assertRaises(ValueError, encode_syncsafe32, -1)
3161
3162    @METADATA_ID3V2
3163    def test_padding(self):
3164        from os.path import getsize
3165        from operator import or_
3166
3167        with open("sine.mp3", "rb") as f:
3168            mp3_data = f.read()
3169
3170        # build temporary track with no metadata
3171        temp_file = tempfile.NamedTemporaryFile(suffix=".mp3")
3172        temp_file_name = temp_file.name
3173        temp_file.write(mp3_data)
3174        temp_file.flush()
3175
3176        mp3_track = audiotools.open(temp_file_name)
3177        self.assertIsNone(mp3_track.get_metadata())
3178
3179        # tag track with our metadata
3180        metadata = self.empty_metadata()
3181        metadata.track_name = u"Track Name"
3182        mp3_track.update_metadata(metadata)
3183
3184        self.assertEqual(mp3_track.get_metadata().track_name, u"Track Name")
3185
3186        self.assertEqual(getsize(temp_file_name),
3187                         metadata.size() + len(mp3_data))
3188
3189        # add a bunch of padding to track's metadata
3190        # and ensure it still works
3191        for padding in range(1024):
3192            # grab existing tag from file
3193            metadata = mp3_track.get_metadata()
3194            old_metadata_size = metadata.total_size
3195
3196            # add another padding byte
3197            metadata.total_size += 1
3198            mp3_track.update_metadata(metadata)
3199
3200            # ensure file isn't broken
3201            self.assertTrue(
3202                audiotools.pcm_cmp(
3203                    audiotools.open("sine.mp3").to_pcm(),
3204                    audiotools.open(temp_file_name).to_pcm()))
3205
3206            # ensure metadata is unchanged
3207            # and has the expected buffer size
3208            new_metadata = audiotools.open(temp_file_name).get_metadata()
3209            self.assertEqual(new_metadata.total_size, old_metadata_size + 1)
3210            self.assertEqual(new_metadata, metadata)
3211        temp_file.close()
3212
3213    @METADATA_ID3V2
3214    def test_clean(self):
3215        # check trailing whitespace
3216        metadata = audiotools.ID3v22Comment(
3217            [audiotools.id3.ID3v22_T__Frame.converted(b"TT2", u"Title ")])
3218        self.assertEqual(metadata.track_name, u"Title ")
3219        (cleaned, fixes) = metadata.clean()
3220        self.assertEqual(fixes,
3221                         ["removed trailing whitespace from %(field)s" %
3222                          {"field": u"TT2"}])
3223        self.assertEqual(cleaned.track_name, u"Title")
3224
3225        # check leading whitespace
3226        metadata = audiotools.ID3v22Comment(
3227            [audiotools.id3.ID3v22_T__Frame.converted(b"TT2", u" Title")])
3228        self.assertEqual(metadata.track_name, u" Title")
3229        (cleaned, fixes) = metadata.clean()
3230        self.assertEqual(fixes,
3231                         ["removed leading whitespace from %(field)s" %
3232                          {"field": u"TT2"}])
3233        self.assertEqual(cleaned.track_name, u"Title")
3234
3235        # check empty fields
3236        metadata = audiotools.ID3v22Comment(
3237            [audiotools.id3.ID3v22_T__Frame.converted(b"TT2", u"")])
3238        self.assertEqual(metadata[b"TT2"][0].data, b"")
3239        (cleaned, fixes) = metadata.clean()
3240        self.assertEqual(fixes,
3241                         ["removed empty field %(field)s" %
3242                          {"field": u"TT2"}])
3243        self.assertRaises(KeyError,
3244                          cleaned.__getitem__,
3245                          b"TT2")
3246
3247        # check leading zeroes,
3248        # depending on whether we're preserving them or not
3249
3250        id3_pad = audiotools.config.get_default("ID3", "pad", "off")
3251        try:
3252            # pad ID3v2 tags with 0
3253            audiotools.config.set_default("ID3", "pad", "on")
3254            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3255                             True)
3256
3257            metadata = audiotools.ID3v22Comment(
3258                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"1")])
3259            self.assertEqual(metadata.track_number, 1)
3260            self.assertIsNone(metadata.track_total)
3261            self.assertEqual(metadata[b"TRK"][0].data, b"1")
3262            (cleaned, fixes) = metadata.clean()
3263            self.assertEqual(fixes,
3264                             ["added leading zeroes to %(field)s" %
3265                              {"field": u"TRK"}])
3266            self.assertEqual(cleaned.track_number, 1)
3267            self.assertIsNone(cleaned.track_total)
3268            self.assertEqual(cleaned[b"TRK"][0].data, b"01")
3269
3270            metadata = audiotools.ID3v22Comment(
3271                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"1/2")])
3272            self.assertEqual(metadata.track_number, 1)
3273            self.assertEqual(metadata.track_total, 2)
3274            self.assertEqual(metadata[b"TRK"][0].data, b"1/2")
3275            (cleaned, fixes) = metadata.clean()
3276            self.assertEqual(fixes,
3277                             ["added leading zeroes to %(field)s" %
3278                              {"field": u"TRK"}])
3279            self.assertEqual(cleaned.track_number, 1)
3280            self.assertEqual(cleaned.track_total, 2)
3281            self.assertEqual(cleaned[b"TRK"][0].data, b"01/02")
3282
3283            # don't pad ID3v2 tags with 0
3284            audiotools.config.set_default("ID3", "pad", "off")
3285            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3286                             False)
3287
3288            metadata = audiotools.ID3v22Comment(
3289                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"01")])
3290            self.assertEqual(metadata.track_number, 1)
3291            self.assertIsNone(metadata.track_total)
3292            self.assertEqual(metadata[b"TRK"][0].data, b"01")
3293            (cleaned, fixes) = metadata.clean()
3294            self.assertEqual(fixes,
3295                             ["removed leading zeroes from %(field)s" %
3296                              {"field": u"TRK"}])
3297            self.assertEqual(cleaned.track_number, 1)
3298            self.assertIsNone(cleaned.track_total)
3299            self.assertEqual(cleaned[b"TRK"][0].data, b"1")
3300
3301            metadata = audiotools.ID3v22Comment(
3302                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"01/2")])
3303            self.assertEqual(metadata.track_number, 1)
3304            self.assertEqual(metadata.track_total, 2)
3305            self.assertEqual(metadata[b"TRK"][0].data, b"01/2")
3306            (cleaned, fixes) = metadata.clean()
3307            self.assertEqual(fixes,
3308                             ["removed leading zeroes from %(field)s" %
3309                              {"field": u"TRK"}])
3310            self.assertEqual(cleaned.track_number, 1)
3311            self.assertEqual(cleaned.track_total, 2)
3312            self.assertEqual(cleaned[b"TRK"][0].data, b"1/2")
3313
3314            metadata = audiotools.ID3v22Comment(
3315                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"1/02")])
3316            self.assertEqual(metadata.track_number, 1)
3317            self.assertEqual(metadata.track_total, 2)
3318            self.assertEqual(metadata[b"TRK"][0].data, b"1/02")
3319            (cleaned, fixes) = metadata.clean()
3320            self.assertEqual(fixes,
3321                             ["removed leading zeroes from %(field)s" %
3322                              {"field": u"TRK"}])
3323            self.assertEqual(cleaned.track_number, 1)
3324            self.assertEqual(cleaned.track_total, 2)
3325            self.assertEqual(cleaned[b"TRK"][0].data, b"1/2")
3326
3327            metadata = audiotools.ID3v22Comment(
3328                [audiotools.id3.ID3v22_T__Frame.converted(b"TRK", u"01/02")])
3329            self.assertEqual(metadata.track_number, 1)
3330            self.assertEqual(metadata.track_total, 2)
3331            self.assertEqual(metadata[b"TRK"][0].data, b"01/02")
3332            (cleaned, fixes) = metadata.clean()
3333            self.assertEqual(fixes,
3334                             ["removed leading zeroes from %(field)s" %
3335                              {"field": u"TRK"}])
3336            self.assertEqual(cleaned.track_number, 1)
3337            self.assertEqual(cleaned.track_total, 2)
3338            self.assertEqual(cleaned[b"TRK"][0].data, b"1/2")
3339        finally:
3340            audiotools.config.set_default("ID3", "pad", id3_pad)
3341
3342
3343class ID3v23MetaData(ID3v22MetaData):
3344    def setUp(self):
3345        self.metadata_class = audiotools.ID3v23Comment
3346        self.supported_fields = ["track_name",
3347                                 "track_number",
3348                                 "track_total",
3349                                 "album_name",
3350                                 "artist_name",
3351                                 "performer_name",
3352                                 "composer_name",
3353                                 "conductor_name",
3354                                 "media",
3355                                 "ISRC",
3356                                 "copyright",
3357                                 "publisher",
3358                                 "year",
3359                                 "date",
3360                                 "album_number",
3361                                 "album_total",
3362                                 "comment"]
3363        self.supported_formats = [audiotools.MP3Audio,
3364                                  audiotools.MP2Audio]
3365
3366    def unknown_tag(self, binary_string):
3367        from audiotools.id3 import ID3v23_Frame
3368
3369        return ID3v23_Frame(b"XXXX", binary_string)
3370
3371    @METADATA_ID3V2
3372    def test_foreign_field(self):
3373        metadata = self.metadata_class(
3374            [audiotools.id3.ID3v23_T___Frame(b"TIT2", 0, b"Track Name"),
3375             audiotools.id3.ID3v23_T___Frame(b"TALB", 0, b"Album Name"),
3376             audiotools.id3.ID3v23_T___Frame(b"TRCK", 0, b"1/3"),
3377             audiotools.id3.ID3v23_T___Frame(b"TPOS", 0, b"2/4"),
3378             audiotools.id3.ID3v23_T___Frame(b"TFOO", 0, b"Bar")])
3379        for format in self.supported_formats:
3380            temp_file = tempfile.NamedTemporaryFile(
3381                suffix="." + format.SUFFIX)
3382            try:
3383                track = format.from_pcm(temp_file.name,
3384                                        BLANK_PCM_Reader(1))
3385                track.set_metadata(metadata)
3386                metadata2 = track.get_metadata()
3387                self.assertEqual(metadata, metadata2)
3388                self.assertEqual(metadata.__class__, metadata2.__class__)
3389                self.assertEqual(metadata[b"TFOO"][0].data, b"Bar")
3390            finally:
3391                temp_file.close()
3392
3393    def empty_metadata(self):
3394        return self.metadata_class([])
3395
3396    @METADATA_ID3V2
3397    def test_sync_safe(self):
3398        # this is tested by ID3v22 and doesn't need to be tested again
3399        self.assertTrue(True)
3400
3401    @METADATA_ID3V2
3402    def test_clean(self):
3403        from audiotools.text import (CLEAN_REMOVE_LEADING_WHITESPACE,
3404                                     CLEAN_REMOVE_TRAILING_WHITESPACE,
3405                                     CLEAN_REMOVE_EMPTY_TAG,
3406                                     CLEAN_REMOVE_LEADING_ZEROES,
3407                                     CLEAN_ADD_LEADING_ZEROES)
3408
3409        # check trailing whitespace
3410        metadata = audiotools.ID3v23Comment(
3411            [audiotools.id3.ID3v23_T___Frame.converted(b"TIT2", u"Title ")])
3412        self.assertEqual(metadata.track_name, u"Title ")
3413        (cleaned, fixes) = metadata.clean()
3414        self.assertEqual(fixes,
3415                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
3416                          {"field": u"TIT2"}])
3417        self.assertEqual(cleaned.track_name, u"Title")
3418
3419        # check leading whitespace
3420        metadata = audiotools.ID3v23Comment(
3421            [audiotools.id3.ID3v23_T___Frame.converted(b"TIT2", u" Title")])
3422        self.assertEqual(metadata.track_name, u" Title")
3423        (cleaned, fixes) = metadata.clean()
3424        self.assertEqual(fixes,
3425                         [CLEAN_REMOVE_LEADING_WHITESPACE %
3426                          {"field": u"TIT2"}])
3427        self.assertEqual(cleaned.track_name, u"Title")
3428
3429        # check empty fields
3430        metadata = audiotools.ID3v23Comment(
3431            [audiotools.id3.ID3v23_T___Frame.converted(b"TIT2", u"")])
3432        self.assertEqual(metadata[b"TIT2"][0].data, b"")
3433        (cleaned, fixes) = metadata.clean()
3434        self.assertEqual(fixes,
3435                         [CLEAN_REMOVE_EMPTY_TAG %
3436                          {"field": u"TIT2"}])
3437        self.assertRaises(KeyError,
3438                          cleaned.__getitem__,
3439                          b"TIT2")
3440
3441        # check leading zeroes,
3442        # depending on whether we're preserving them or not
3443
3444        id3_pad = audiotools.config.get_default("ID3", "pad", "off")
3445        try:
3446            # pad ID3v2 tags with 0
3447            audiotools.config.set_default("ID3", "pad", "on")
3448            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3449                             True)
3450
3451            metadata = audiotools.ID3v23Comment(
3452                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"1")])
3453            self.assertEqual(metadata.track_number, 1)
3454            self.assertIsNone(metadata.track_total)
3455            self.assertEqual(metadata[b"TRCK"][0].data, b"1")
3456            (cleaned, fixes) = metadata.clean()
3457            self.assertEqual(fixes,
3458                             [CLEAN_ADD_LEADING_ZEROES %
3459                              {"field": u"TRCK"}])
3460            self.assertEqual(cleaned.track_number, 1)
3461            self.assertIsNone(cleaned.track_total)
3462            self.assertEqual(cleaned[b"TRCK"][0].data, b"01")
3463
3464            metadata = audiotools.ID3v23Comment(
3465                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"1/2")])
3466            self.assertEqual(metadata.track_number, 1)
3467            self.assertEqual(metadata.track_total, 2)
3468            self.assertEqual(metadata[b"TRCK"][0].data, b"1/2")
3469            (cleaned, fixes) = metadata.clean()
3470            self.assertEqual(fixes,
3471                             [CLEAN_ADD_LEADING_ZEROES %
3472                              {"field": u"TRCK"}])
3473            self.assertEqual(cleaned.track_number, 1)
3474            self.assertEqual(cleaned.track_total, 2)
3475            self.assertEqual(cleaned[b"TRCK"][0].data, b"01/02")
3476
3477            # don't pad ID3v2 tags with 0
3478            audiotools.config.set_default("ID3", "pad", "off")
3479            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3480                             False)
3481
3482            metadata = audiotools.ID3v23Comment(
3483                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"01")])
3484            self.assertEqual(metadata.track_number, 1)
3485            self.assertIsNone(metadata.track_total)
3486            self.assertEqual(metadata[b"TRCK"][0].data, b"01")
3487            (cleaned, fixes) = metadata.clean()
3488            self.assertEqual(fixes,
3489                             [CLEAN_REMOVE_LEADING_ZEROES %
3490                              {"field": u"TRCK"}])
3491            self.assertEqual(cleaned.track_number, 1)
3492            self.assertIsNone(cleaned.track_total)
3493            self.assertEqual(cleaned[b"TRCK"][0].data, b"1")
3494
3495            metadata = audiotools.ID3v23Comment(
3496                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"01/2")])
3497            self.assertEqual(metadata.track_number, 1)
3498            self.assertEqual(metadata.track_total, 2)
3499            self.assertEqual(metadata[b"TRCK"][0].data, b"01/2")
3500            (cleaned, fixes) = metadata.clean()
3501            self.assertEqual(fixes,
3502                             [CLEAN_REMOVE_LEADING_ZEROES %
3503                              {"field": u"TRCK"}])
3504            self.assertEqual(cleaned.track_number, 1)
3505            self.assertEqual(cleaned.track_total, 2)
3506            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3507
3508            metadata = audiotools.ID3v23Comment(
3509                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"1/02")])
3510            self.assertEqual(metadata.track_number, 1)
3511            self.assertEqual(metadata.track_total, 2)
3512            self.assertEqual(metadata[b"TRCK"][0].data, b"1/02")
3513            (cleaned, fixes) = metadata.clean()
3514            self.assertEqual(fixes,
3515                             [CLEAN_REMOVE_LEADING_ZEROES %
3516                              {"field": u"TRCK"}])
3517            self.assertEqual(cleaned.track_number, 1)
3518            self.assertEqual(cleaned.track_total, 2)
3519            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3520
3521            metadata = audiotools.ID3v23Comment(
3522                [audiotools.id3.ID3v23_T___Frame.converted(b"TRCK", u"01/02")])
3523            self.assertEqual(metadata.track_number, 1)
3524            self.assertEqual(metadata.track_total, 2)
3525            self.assertEqual(metadata[b"TRCK"][0].data, b"01/02")
3526            (cleaned, fixes) = metadata.clean()
3527            self.assertEqual(fixes,
3528                             [CLEAN_REMOVE_LEADING_ZEROES %
3529                              {"field": u"TRCK"}])
3530            self.assertEqual(cleaned.track_number, 1)
3531            self.assertEqual(cleaned.track_total, 2)
3532            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3533        finally:
3534            audiotools.config.set_default("ID3", "pad", id3_pad)
3535
3536
3537class ID3v24MetaData(ID3v22MetaData):
3538    def setUp(self):
3539        self.metadata_class = audiotools.ID3v24Comment
3540        self.supported_fields = ["track_name",
3541                                 "track_number",
3542                                 "track_total",
3543                                 "album_name",
3544                                 "artist_name",
3545                                 "performer_name",
3546                                 "composer_name",
3547                                 "conductor_name",
3548                                 "media",
3549                                 "ISRC",
3550                                 "copyright",
3551                                 "publisher",
3552                                 "year",
3553                                 "date",
3554                                 "album_number",
3555                                 "album_total",
3556                                 "comment"]
3557        self.supported_formats = [audiotools.MP3Audio,
3558                                  audiotools.MP2Audio]
3559
3560    def unknown_tag(self, binary_string):
3561        from audiotools.id3 import ID3v24_Frame
3562
3563        return ID3v24_Frame(b"XXXX", binary_string)
3564
3565    def empty_metadata(self):
3566        return self.metadata_class([])
3567
3568    @METADATA_ID3V2
3569    def test_sync_safe(self):
3570        # this is tested by ID3v22 and doesn't need to be tested again
3571        self.assertTrue(True)
3572
3573    @METADATA_ID3V2
3574    def test_clean(self):
3575        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
3576                                     CLEAN_REMOVE_LEADING_WHITESPACE,
3577                                     CLEAN_REMOVE_EMPTY_TAG,
3578                                     CLEAN_ADD_LEADING_ZEROES,
3579                                     CLEAN_REMOVE_LEADING_ZEROES)
3580
3581        # check trailing whitespace
3582        metadata = audiotools.ID3v24Comment(
3583            [audiotools.id3.ID3v24_T___Frame.converted(b"TIT2", u"Title ")])
3584        self.assertEqual(metadata.track_name, u"Title ")
3585        (cleaned, fixes) = metadata.clean()
3586        self.assertEqual(fixes,
3587                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
3588                          {"field": u"TIT2"}])
3589        self.assertEqual(cleaned.track_name, u"Title")
3590
3591        # check leading whitespace
3592        metadata = audiotools.ID3v24Comment(
3593            [audiotools.id3.ID3v24_T___Frame.converted(b"TIT2", u" Title")])
3594        self.assertEqual(metadata.track_name, u" Title")
3595        (cleaned, fixes) = metadata.clean()
3596        self.assertEqual(fixes,
3597                         [CLEAN_REMOVE_LEADING_WHITESPACE %
3598                          {"field": u"TIT2"}])
3599        self.assertEqual(cleaned.track_name, u"Title")
3600
3601        # check empty fields
3602        metadata = audiotools.ID3v24Comment(
3603            [audiotools.id3.ID3v24_T___Frame.converted(b"TIT2", u"")])
3604        self.assertEqual(metadata[b"TIT2"][0].data, b"")
3605        (cleaned, fixes) = metadata.clean()
3606        self.assertEqual(fixes,
3607                         [CLEAN_REMOVE_EMPTY_TAG %
3608                          {"field": u"TIT2"}])
3609        self.assertRaises(KeyError,
3610                          cleaned.__getitem__,
3611                          b"TIT2")
3612
3613        # check leading zeroes,
3614        # depending on whether we're preserving them or not
3615
3616        id3_pad = audiotools.config.get_default("ID3", "pad", "off")
3617        try:
3618            # pad ID3v2 tags with 0
3619            audiotools.config.set_default("ID3", "pad", "on")
3620            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3621                             True)
3622
3623            metadata = audiotools.ID3v24Comment(
3624                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"1")])
3625            self.assertEqual(metadata.track_number, 1)
3626            self.assertIsNone(metadata.track_total)
3627            self.assertEqual(metadata[b"TRCK"][0].data, b"1")
3628            (cleaned, fixes) = metadata.clean()
3629            self.assertEqual(fixes,
3630                             [CLEAN_ADD_LEADING_ZEROES %
3631                              {"field": u"TRCK"}])
3632            self.assertEqual(cleaned.track_number, 1)
3633            self.assertIsNone(cleaned.track_total)
3634            self.assertEqual(cleaned[b"TRCK"][0].data, b"01")
3635
3636            metadata = audiotools.ID3v24Comment(
3637                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"1/2")])
3638            self.assertEqual(metadata.track_number, 1)
3639            self.assertEqual(metadata.track_total, 2)
3640            self.assertEqual(metadata[b"TRCK"][0].data, b"1/2")
3641            (cleaned, fixes) = metadata.clean()
3642            self.assertEqual(fixes,
3643                             [CLEAN_ADD_LEADING_ZEROES %
3644                              {"field": u"TRCK"}])
3645            self.assertEqual(cleaned.track_number, 1)
3646            self.assertEqual(cleaned.track_total, 2)
3647            self.assertEqual(cleaned[b"TRCK"][0].data, b"01/02")
3648
3649            # don't pad ID3v2 tags with 0
3650            audiotools.config.set_default("ID3", "pad", "off")
3651            self.assertEqual(audiotools.config.getboolean("ID3", "pad"),
3652                             False)
3653
3654            metadata = audiotools.ID3v24Comment(
3655                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"01")])
3656            self.assertEqual(metadata.track_number, 1)
3657            self.assertIsNone(metadata.track_total)
3658            self.assertEqual(metadata[b"TRCK"][0].data, b"01")
3659            (cleaned, fixes) = metadata.clean()
3660            self.assertEqual(fixes,
3661                             [CLEAN_REMOVE_LEADING_ZEROES %
3662                              {"field": u"TRCK"}])
3663            self.assertEqual(cleaned.track_number, 1)
3664            self.assertIsNone(cleaned.track_total)
3665            self.assertEqual(cleaned[b"TRCK"][0].data, b"1")
3666
3667            metadata = audiotools.ID3v24Comment(
3668                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"01/2")])
3669            self.assertEqual(metadata.track_number, 1)
3670            self.assertEqual(metadata.track_total, 2)
3671            self.assertEqual(metadata[b"TRCK"][0].data, b"01/2")
3672            (cleaned, fixes) = metadata.clean()
3673            self.assertEqual(fixes,
3674                             [CLEAN_REMOVE_LEADING_ZEROES %
3675                              {"field": u"TRCK"}])
3676            self.assertEqual(cleaned.track_number, 1)
3677            self.assertEqual(cleaned.track_total, 2)
3678            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3679
3680            metadata = audiotools.ID3v24Comment(
3681                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"1/02")])
3682            self.assertEqual(metadata.track_number, 1)
3683            self.assertEqual(metadata.track_total, 2)
3684            self.assertEqual(metadata[b"TRCK"][0].data, b"1/02")
3685            (cleaned, fixes) = metadata.clean()
3686            self.assertEqual(fixes,
3687                             [CLEAN_REMOVE_LEADING_ZEROES %
3688                              {"field": u"TRCK"}])
3689            self.assertEqual(cleaned.track_number, 1)
3690            self.assertEqual(cleaned.track_total, 2)
3691            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3692
3693            metadata = audiotools.ID3v24Comment(
3694                [audiotools.id3.ID3v24_T___Frame.converted(b"TRCK", u"01/02")])
3695            self.assertEqual(metadata.track_number, 1)
3696            self.assertEqual(metadata.track_total, 2)
3697            self.assertEqual(metadata[b"TRCK"][0].data, b"01/02")
3698            (cleaned, fixes) = metadata.clean()
3699            self.assertEqual(fixes,
3700                             [CLEAN_REMOVE_LEADING_ZEROES %
3701                              {"field": u"TRCK"}])
3702            self.assertEqual(cleaned.track_number, 1)
3703            self.assertEqual(cleaned.track_total, 2)
3704            self.assertEqual(cleaned[b"TRCK"][0].data, b"1/2")
3705        finally:
3706            audiotools.config.set_default("ID3", "pad", id3_pad)
3707
3708
3709class ID3CommentPairMetaData(MetaDataTest):
3710    def setUp(self):
3711        self.metadata_class = audiotools.ID3CommentPair
3712        self.supported_fields = ["track_name",
3713                                 "track_number",
3714                                 "track_total",
3715                                 "album_name",
3716                                 "artist_name",
3717                                 "performer_name",
3718                                 "composer_name",
3719                                 "conductor_name",
3720                                 "media",
3721                                 "ISRC",
3722                                 "copyright",
3723                                 "publisher",
3724                                 "year",
3725                                 "date",
3726                                 "album_number",
3727                                 "album_total",
3728                                 "comment"]
3729        self.supported_formats = [audiotools.MP3Audio,
3730                                  audiotools.MP2Audio]
3731
3732    def empty_metadata(self):
3733        return self.metadata_class.converted(audiotools.MetaData())
3734
3735    @METADATA_ID3V2
3736    def test_field_mapping(self):
3737        pass
3738
3739
3740class FlacMetaData(MetaDataTest):
3741    def setUp(self):
3742        self.metadata_class = audiotools.FlacMetaData
3743        self.supported_fields = ["track_name",
3744                                 "track_number",
3745                                 "track_total",
3746                                 "album_name",
3747                                 "artist_name",
3748                                 "performer_name",
3749                                 "composer_name",
3750                                 "conductor_name",
3751                                 "media",
3752                                 "ISRC",
3753                                 "catalog",
3754                                 "copyright",
3755                                 "publisher",
3756                                 "year",
3757                                 "album_number",
3758                                 "album_total",
3759                                 "comment"]
3760        self.supported_formats = [audiotools.FlacAudio,
3761                                  audiotools.OggFlacAudio]
3762
3763    def empty_metadata(self):
3764        return self.metadata_class.converted(audiotools.MetaData())
3765
3766    @METADATA_FLAC
3767    def test_update(self):
3768        import os
3769
3770        for audio_class in self.supported_formats:
3771            temp_file = tempfile.NamedTemporaryFile(
3772                suffix="." + audio_class.SUFFIX)
3773            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
3774            temp_file_stat = os.stat(temp_file.name)[0]
3775            try:
3776                # update_metadata on file's internal metadata round-trips okay
3777                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
3778                metadata = track.get_metadata()
3779                self.assertEqual(metadata.track_name, u"Foo")
3780                metadata.track_name = u"Bar"
3781                track.update_metadata(metadata)
3782                metadata = track.get_metadata()
3783                self.assertEqual(metadata.track_name, u"Bar")
3784
3785                # update_metadata on unwritable file generates IOError
3786                metadata = track.get_metadata()
3787                os.chmod(temp_file.name, 0)
3788                self.assertRaises(IOError,
3789                                  track.update_metadata,
3790                                  metadata)
3791                os.chmod(temp_file.name, temp_file_stat)
3792
3793                # update_metadata with foreign MetaData generates ValueError
3794                self.assertRaises(ValueError,
3795                                  track.update_metadata,
3796                                  audiotools.MetaData(track_name=u"Foo"))
3797
3798                # update_metadata with None makes no changes
3799                track.update_metadata(None)
3800                metadata = track.get_metadata()
3801                self.assertEqual(metadata.track_name, u"Bar")
3802
3803                # streaminfo not updated with set_metadata()
3804                # but can be updated with update_metadata()
3805                old_streaminfo = metadata.get_block(
3806                    audiotools.flac.Flac_STREAMINFO.BLOCK_ID)
3807                new_streaminfo = audiotools.flac.Flac_STREAMINFO(
3808                    minimum_block_size=old_streaminfo.minimum_block_size,
3809                    maximum_block_size=old_streaminfo.maximum_block_size,
3810                    minimum_frame_size=0,
3811                    maximum_frame_size=old_streaminfo.maximum_frame_size,
3812                    sample_rate=old_streaminfo.sample_rate,
3813                    channels=old_streaminfo.channels,
3814                    bits_per_sample=old_streaminfo.bits_per_sample,
3815                    total_samples=old_streaminfo.total_samples,
3816                    md5sum=old_streaminfo.md5sum)
3817                metadata.replace_blocks(
3818                    audiotools.flac.Flac_STREAMINFO.BLOCK_ID, [new_streaminfo])
3819                track.set_metadata(metadata)
3820                self.assertEqual(track.get_metadata().get_block(
3821                    audiotools.flac.Flac_STREAMINFO.BLOCK_ID),
3822                    old_streaminfo)
3823                track.update_metadata(metadata)
3824                self.assertEqual(track.get_metadata().get_block(
3825                    audiotools.flac.Flac_STREAMINFO.BLOCK_ID),
3826                    new_streaminfo)
3827
3828                # vendor_string not updated with set_metadata()
3829                # but can be updated with update_metadata()
3830                old_vorbiscomment = metadata.get_block(
3831                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)
3832                new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT(
3833                    comment_strings=old_vorbiscomment.comment_strings[:],
3834                    vendor_string=u"Vendor String")
3835                metadata.replace_blocks(
3836                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID,
3837                    [new_vorbiscomment])
3838                track.set_metadata(metadata)
3839                self.assertEqual(track.get_metadata().get_block(
3840                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID).vendor_string,
3841                    old_vorbiscomment.vendor_string)
3842                track.update_metadata(metadata)
3843                self.assertEqual(track.get_metadata().get_block(
3844                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID).vendor_string,
3845                    new_vorbiscomment.vendor_string)
3846
3847                # REPLAYGAIN_* tags not updated with set_metadata()
3848                # but can be updated with update_metadata()
3849                old_vorbiscomment = metadata.get_block(
3850                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)
3851                new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT(
3852                    comment_strings=old_vorbiscomment.comment_strings +
3853                    [u"REPLAYGAIN_REFERENCE_LOUDNESS=89.0 dB"],
3854                    vendor_string=old_vorbiscomment.vendor_string)
3855                self.assertEqual(
3856                    new_vorbiscomment[u"REPLAYGAIN_REFERENCE_LOUDNESS"],
3857                    [u"89.0 dB"])
3858                metadata.replace_blocks(
3859                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID,
3860                    [new_vorbiscomment])
3861                track.set_metadata(metadata)
3862                self.assertRaises(
3863                    KeyError,
3864                    track.get_metadata().get_block(
3865                        audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
3866                        ).__getitem__,
3867                    u"REPLAYGAIN_REFERENCE_LOUDNESS")
3868                track.update_metadata(metadata)
3869                self.assertEqual(
3870                    track.get_metadata().get_block(
3871                        audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
3872                        )[u"REPLAYGAIN_REFERENCE_LOUDNESS"],
3873                    [u"89.0 dB"])
3874
3875                # WAVEFORMATEXTENSIBLE_CHANNEL_MASK
3876                # not updated with set_metadata()
3877                # but can be updated with update_metadata()
3878                old_vorbiscomment = metadata.get_block(
3879                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)
3880                new_vorbiscomment = audiotools.flac.Flac_VORBISCOMMENT(
3881                    comment_strings=old_vorbiscomment.comment_strings +
3882                    [u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK=0x0003"],
3883                    vendor_string=old_vorbiscomment.vendor_string)
3884                self.assertEqual(
3885                    new_vorbiscomment[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"],
3886                    [u"0x0003"])
3887                metadata.replace_blocks(
3888                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID,
3889                    [new_vorbiscomment])
3890                track.set_metadata(metadata)
3891                self.assertRaises(
3892                    KeyError,
3893                    track.get_metadata().get_block(
3894                        audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
3895                        ).__getitem__,
3896                    u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK")
3897                track.update_metadata(metadata)
3898                self.assertEqual(
3899                    track.get_metadata().get_block(
3900                        audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
3901                        )[u"WAVEFORMATEXTENSIBLE_CHANNEL_MASK"],
3902                    [u"0x0003"])
3903
3904                # cuesheet not updated with set_metadata()
3905                # but can be updated with update_metadata()
3906                new_cuesheet = audiotools.flac.Flac_CUESHEET(
3907                    catalog_number=b"\x00" * 128,
3908                    lead_in_samples=0,
3909                    is_cdda=1,
3910                    tracks=[audiotools.flac.Flac_CUESHEET_track(
3911                            offset=0,
3912                            number=0,
3913                            ISRC=b" " * 12,
3914                            track_type=0,
3915                            pre_emphasis=0,
3916                            index_points=[audiotools.flac.Flac_CUESHEET_index(
3917                                track_offset=0,
3918                                offset=0,
3919                                number=0)])])
3920                metadata = track.get_metadata()
3921                self.assertRaises(IndexError,
3922                                  metadata.get_block,
3923                                  audiotools.flac.Flac_CUESHEET.BLOCK_ID)
3924                metadata.add_block(new_cuesheet)
3925                track.set_metadata(metadata)
3926                self.assertRaises(IndexError,
3927                                  track.get_metadata().get_block,
3928                                  audiotools.flac.Flac_CUESHEET.BLOCK_ID)
3929                track.update_metadata(metadata)
3930                self.assertEqual(
3931                    track.get_metadata().get_block(
3932                        audiotools.flac.Flac_CUESHEET.BLOCK_ID),
3933                    new_cuesheet)
3934
3935                if audio_class is not audiotools.OggFlacAudio:
3936                    # seektable not updated with set_metadata()
3937                    # but can be updated with update_metadata()
3938
3939                    # Ogg FLAC doesn't really support seektables as such
3940
3941                    metadata = track.get_metadata()
3942
3943                    old_seektable = metadata.get_block(
3944                        audiotools.flac.Flac_SEEKTABLE.BLOCK_ID)
3945
3946                    new_seektable = audiotools.flac.Flac_SEEKTABLE(
3947                        seekpoints=[(1, 1, 4096)] +
3948                        old_seektable.seekpoints[1:])
3949                    metadata.replace_blocks(
3950                        audiotools.flac.Flac_SEEKTABLE.BLOCK_ID,
3951                        [new_seektable])
3952                    track.set_metadata(metadata)
3953                    self.assertEqual(
3954                        track.get_metadata().get_block(
3955                            audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
3956                        old_seektable)
3957                    track.update_metadata(metadata)
3958                    self.assertEqual(
3959                        track.get_metadata().get_block(
3960                            audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
3961                        new_seektable)
3962
3963                # application blocks not updated with set_metadata()
3964                # but can be updated with update_metadata()
3965                application = audiotools.flac.Flac_APPLICATION(
3966                    application_id=b"fooz",
3967                    data=b"kelp")
3968                metadata = track.get_metadata()
3969                self.assertRaises(IndexError,
3970                                  metadata.get_block,
3971                                  audiotools.flac.Flac_APPLICATION.BLOCK_ID)
3972                metadata.add_block(application)
3973                track.set_metadata(metadata)
3974                self.assertRaises(IndexError,
3975                                  track.get_metadata().get_block,
3976                                  audiotools.flac.Flac_APPLICATION.BLOCK_ID)
3977                track.update_metadata(metadata)
3978                self.assertEqual(track.get_metadata().get_block(
3979                    audiotools.flac.Flac_APPLICATION.BLOCK_ID),
3980                    application)
3981            finally:
3982                temp_file.close()
3983
3984    @METADATA_FLAC
3985    def test_foreign_field(self):
3986        metadata = audiotools.FlacMetaData([
3987            audiotools.flac.Flac_VORBISCOMMENT(
3988                [u"TITLE=Track Name",
3989                 u"ALBUM=Album Name",
3990                 u"TRACKNUMBER=1",
3991                 u"TRACKTOTAL=3",
3992                 u"DISCNUMBER=2",
3993                 u"DISCTOTAL=4",
3994                 u"FOO=Bar"], u"")])
3995        for format in self.supported_formats:
3996            temp_file = tempfile.NamedTemporaryFile(
3997                suffix="." + format.SUFFIX)
3998            try:
3999                track = format.from_pcm(temp_file.name,
4000                                        BLANK_PCM_Reader(1))
4001                track.set_metadata(metadata)
4002                metadata2 = track.get_metadata()
4003                self.assertEqual(metadata, metadata2)
4004                self.assertIsInstance(metadata, audiotools.FlacMetaData)
4005                self.assertEqual(track.get_metadata().get_block(
4006                    audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u"FOO"],
4007                    [u"Bar"])
4008            finally:
4009                temp_file.close()
4010
4011    @METADATA_FLAC
4012    def test_field_mapping(self):
4013        mapping = [('track_name', u'TITLE', u'a'),
4014                   ('track_number', u'TRACKNUMBER', 1),
4015                   ('track_total', u'TRACKTOTAL', 2),
4016                   ('album_name', u'ALBUM', u'b'),
4017                   ('artist_name', u'ARTIST', u'c'),
4018                   ('performer_name', u'PERFORMER', u'd'),
4019                   ('composer_name', u'COMPOSER', u'e'),
4020                   ('conductor_name', u'CONDUCTOR', u'f'),
4021                   ('media', u'SOURCE MEDIUM', u'g'),
4022                   ('ISRC', u'ISRC', u'h'),
4023                   ('catalog', u'CATALOG', u'i'),
4024                   ('copyright', u'COPYRIGHT', u'j'),
4025                   ('year', u'DATE', u'k'),
4026                   ('album_number', u'DISCNUMBER', 3),
4027                   ('album_total', u'DISCTOTAL', 4),
4028                   ('comment', u'COMMENT', u'l')]
4029
4030        for format in self.supported_formats:
4031            temp_file = tempfile.NamedTemporaryFile(suffix="." + format.SUFFIX)
4032            try:
4033                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
4034
4035                # ensure that setting a class field
4036                # updates its corresponding low-level implementation
4037                for (field, key, value) in mapping:
4038                    track.delete_metadata()
4039                    metadata = self.empty_metadata()
4040                    setattr(metadata, field, value)
4041                    self.assertEqual(getattr(metadata, field), value)
4042                    self.assertEqual(
4043                        metadata.get_block(
4044                            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4045                            )[key][0],
4046                        u"%s" % (value))
4047                    track.set_metadata(metadata)
4048                    metadata2 = track.get_metadata()
4049                    self.assertEqual(getattr(metadata2, field), value)
4050                    self.assertEqual(
4051                        metadata2.get_block(
4052                            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4053                            )[key][0],
4054                        u"%s" % (value))
4055
4056                # ensure that updating the low-level implementation
4057                # is reflected in the class field
4058                for (field, key, value) in mapping:
4059                    track.delete_metadata()
4060                    metadata = self.empty_metadata()
4061                    metadata.get_block(
4062                        audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[key] = \
4063                        [u"%s" % (value)]
4064                    self.assertEqual(getattr(metadata, field), value)
4065                    self.assertEqual(
4066                        metadata.get_block(
4067                            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4068                            )[key][0],
4069                        u"%s" % (value))
4070                    track.set_metadata(metadata)
4071                    metadata2 = track.get_metadata()
4072                    self.assertEqual(getattr(metadata2, field), value)
4073                    self.assertEqual(
4074                        metadata2.get_block(
4075                            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4076                            )[key][0],
4077                        u"%s" % (value))
4078            finally:
4079                # temp_file.close()
4080                pass
4081
4082    @METADATA_FLAC
4083    def test_converted(self):
4084        MetaDataTest.test_converted(self)
4085
4086        metadata_orig = self.empty_metadata()
4087        metadata_orig.get_block(
4088            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'] = [u'bar']
4089
4090        self.assertEqual(metadata_orig.get_block(
4091            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'], [u'bar'])
4092
4093        metadata_new = self.metadata_class.converted(metadata_orig)
4094
4095        self.assertEqual(metadata_orig.get_block(
4096            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'],
4097            metadata_new.get_block(
4098                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u'FOO'])
4099
4100        # ensure that convert() builds a whole new object
4101        metadata_new.track_name = u"Foo"
4102        self.assertEqual(metadata_new.track_name, u"Foo")
4103        metadata_new2 = self.metadata_class.converted(metadata_new)
4104        self.assertEqual(metadata_new2, metadata_new)
4105        metadata_new2.track_name = u"Bar"
4106        self.assertEqual(metadata_new2.track_name, u"Bar")
4107        self.assertEqual(metadata_new.track_name, u"Foo")
4108
4109    @METADATA_FLAC
4110    def test_oversized(self):
4111        from bz2 import decompress
4112
4113        oversized_image = audiotools.Image.new(decompress(HUGE_BMP), u'', 0)
4114        oversized_text = u"a" * 16777216
4115
4116        for audio_class in self.supported_formats:
4117            temp_file = tempfile.NamedTemporaryFile(
4118                suffix="." + audio_class.SUFFIX)
4119            try:
4120                track = audio_class.from_pcm(temp_file.name,
4121                                             BLANK_PCM_Reader(1))
4122
4123                # check that setting an oversized field fails properly
4124                metadata = self.empty_metadata()
4125                metadata.track_name = oversized_text
4126                track.set_metadata(metadata)
4127                metadata = track.get_metadata()
4128                self.assertNotEqual(metadata.track_name, oversized_text)
4129
4130                # check that setting an oversized image fails properly
4131                metadata = self.empty_metadata()
4132                metadata.add_image(oversized_image)
4133                track.set_metadata(metadata)
4134                metadata = track.get_metadata()
4135                self.assertNotEqual(metadata.images(), [oversized_image])
4136            finally:
4137                temp_file.close()
4138
4139    @METADATA_FLAC
4140    def test_totals(self):
4141        metadata = self.empty_metadata()
4142        metadata.get_block(
4143            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4144            )[u"TRACKNUMBER"] = [u"2/4"]
4145        self.assertEqual(metadata.track_number, 2)
4146        self.assertEqual(metadata.track_total, 4)
4147
4148        metadata = self.empty_metadata()
4149        metadata.get_block(
4150            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID
4151            )[u"DISCNUMBER"] = [u"1/3"]
4152        self.assertEqual(metadata.album_number, 1)
4153        self.assertEqual(metadata.album_total, 3)
4154
4155    @METADATA_FLAC
4156    def test_clean(self):
4157        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
4158                                     CLEAN_REMOVE_LEADING_WHITESPACE,
4159                                     CLEAN_REMOVE_LEADING_ZEROES,
4160                                     CLEAN_REMOVE_EMPTY_TAG,
4161                                     CLEAN_FIX_IMAGE_FIELDS,
4162                                     CLEAN_FLAC_REMOVE_SEEKPOINTS,
4163                                     CLEAN_FLAC_REORDER_SEEKPOINTS,
4164                                     CLEAN_FLAC_MULITPLE_STREAMINFO,
4165                                     CLEAN_FLAC_MULTIPLE_VORBISCOMMENT,
4166                                     CLEAN_FLAC_MULTIPLE_SEEKTABLE,
4167                                     CLEAN_FLAC_MULTIPLE_CUESHEET)
4168        # check no blocks
4169        metadata = audiotools.FlacMetaData([])
4170        (cleaned, results) = metadata.clean()
4171        self.assertEqual(metadata, cleaned)
4172        self.assertEqual(results, [])
4173
4174        # check trailing whitespace
4175        metadata = audiotools.FlacMetaData([
4176            audiotools.flac.Flac_VORBISCOMMENT([u"TITLE=Foo "], u"")])
4177        self.assertEqual(metadata.track_name, u'Foo ')
4178        (cleaned, results) = metadata.clean()
4179        self.assertEqual(cleaned.track_name, u'Foo')
4180        self.assertEqual(results,
4181                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
4182                          {"field": u"TITLE"}])
4183
4184        # check leading whitespace
4185        metadata = audiotools.FlacMetaData([
4186            audiotools.flac.Flac_VORBISCOMMENT([u"TITLE= Foo"], u"")])
4187        self.assertEqual(metadata.track_name, u' Foo')
4188        (cleaned, results) = metadata.clean()
4189        self.assertEqual(cleaned.track_name, u'Foo')
4190        self.assertEqual(results,
4191                         [CLEAN_REMOVE_LEADING_WHITESPACE %
4192                          {"field": u"TITLE"}])
4193
4194        # check leading zeroes
4195        metadata = audiotools.FlacMetaData([
4196            audiotools.flac.Flac_VORBISCOMMENT([u"TRACKNUMBER=01"], u"")])
4197        self.assertEqual(
4198            metadata.get_block(
4199                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u"TRACKNUMBER"],
4200            [u"01"])
4201        (cleaned, results) = metadata.clean()
4202        self.assertEqual(
4203            cleaned.get_block(
4204                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u"TRACKNUMBER"],
4205            [u"1"])
4206        self.assertEqual(results,
4207                         [CLEAN_REMOVE_LEADING_ZEROES %
4208                          {"field": u"TRACKNUMBER"}])
4209
4210        # check empty fields
4211        metadata = audiotools.FlacMetaData([
4212            audiotools.flac.Flac_VORBISCOMMENT([u"TITLE=  "], u"")])
4213        self.assertEqual(
4214            metadata.get_block(
4215                audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID)[u"TITLE"], [u'  '])
4216        (cleaned, results) = metadata.clean()
4217        self.assertEqual(cleaned,
4218                         audiotools.FlacMetaData([
4219                             audiotools.flac.Flac_VORBISCOMMENT([], u"")]))
4220
4221        self.assertEqual(results,
4222                         [CLEAN_REMOVE_EMPTY_TAG %
4223                          {"field": u"TITLE"}])
4224
4225        # check mis-tagged images
4226        with open("metadata_flac_clean.jpg", "rb") as jpg:
4227            metadata = audiotools.FlacMetaData(
4228                [audiotools.flac.Flac_PICTURE(
4229                 0, u"image/jpeg", u"", 20, 20, 24, 10,
4230                 jpg.read())])
4231        self.assertEqual(
4232            len(metadata.get_blocks(audiotools.flac.Flac_PICTURE.BLOCK_ID)), 1)
4233        image = metadata.images()[0]
4234        self.assertEqual(image.mime_type, u"image/jpeg")
4235        self.assertEqual(image.width, 20)
4236        self.assertEqual(image.height, 20)
4237        self.assertEqual(image.color_depth, 24)
4238        self.assertEqual(image.color_count, 10)
4239
4240        (cleaned, results) = metadata.clean()
4241        self.assertEqual(results,
4242                         [CLEAN_FIX_IMAGE_FIELDS])
4243        self.assertEqual(
4244            len(cleaned.get_blocks(audiotools.flac.Flac_PICTURE.BLOCK_ID)), 1)
4245        image = cleaned.images()[0]
4246        self.assertEqual(image.mime_type, u"image/png")
4247        self.assertEqual(image.width, 10)
4248        self.assertEqual(image.height, 10)
4249        self.assertEqual(image.color_depth, 8)
4250        self.assertEqual(image.color_count, 1)
4251
4252        # check seektable with empty seekpoints
4253        metadata = audiotools.FlacMetaData(
4254            [audiotools.flac.Flac_SEEKTABLE([(0, 10, 10),
4255                                             (10, 20, 0),
4256                                             (10, 20, 0),
4257                                             (10, 20, 0),
4258                                             (10, 20, 20)])])
4259        (cleaned, results) = metadata.clean()
4260        self.assertEqual(results,
4261                         [CLEAN_FLAC_REMOVE_SEEKPOINTS])
4262        self.assertEqual(
4263            cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
4264            audiotools.flac.Flac_SEEKTABLE([(0, 10, 10),
4265                                            (10, 20, 20)]))
4266
4267        # check seektable with duplicate seekpoints
4268        metadata = audiotools.FlacMetaData(
4269            [audiotools.flac.Flac_SEEKTABLE([(0, 0, 10),
4270                                             (2, 20, 10),
4271                                             (2, 20, 10),
4272                                             (2, 20, 10),
4273                                             (4, 40, 10)])])
4274        (cleaned, results) = metadata.clean()
4275        self.assertEqual(results,
4276                         [CLEAN_FLAC_REORDER_SEEKPOINTS])
4277        self.assertEqual(
4278            cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
4279            audiotools.flac.Flac_SEEKTABLE([(0, 0, 10),
4280                                            (2, 20, 10),
4281                                            (4, 40, 10)]))
4282
4283        # check seektable with mis-ordered seekpoints
4284        metadata = audiotools.FlacMetaData(
4285            [audiotools.flac.Flac_SEEKTABLE([(0, 0, 10),
4286                                             (6, 60, 10),
4287                                             (4, 40, 10),
4288                                             (2, 20, 10),
4289                                             (8, 80, 10)])])
4290        (cleaned, results) = metadata.clean()
4291        self.assertEqual(results,
4292                         [CLEAN_FLAC_REORDER_SEEKPOINTS])
4293        self.assertEqual(
4294            cleaned.get_block(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
4295            audiotools.flac.Flac_SEEKTABLE([(0, 0, 10),
4296                                            (2, 20, 10),
4297                                            (4, 40, 10),
4298                                            (6, 60, 10),
4299                                            (8, 80, 10)]))
4300
4301        # check that cleanup doesn't disturb other metadata blocks
4302        # FIXME
4303        metadata = audiotools.FlacMetaData([
4304            audiotools.flac.Flac_STREAMINFO(
4305                minimum_block_size=4096,
4306                maximum_block_size=4096,
4307                minimum_frame_size=14,
4308                maximum_frame_size=18,
4309                sample_rate=44100,
4310                channels=2,
4311                bits_per_sample=16,
4312                total_samples=149606016,
4313                md5sum=(b'\xae\x87\x1c\x8e\xe1\xfc\x16\xde' +
4314                        b'\x86\x81&\x8e\xc8\xd52\xff')),
4315            audiotools.flac.Flac_APPLICATION(application_id=b"FOOZ",
4316                                             data=b"KELP"),
4317            audiotools.flac.Flac_SEEKTABLE([
4318                (0, 0, 4096),
4319                (8335360, 30397, 4096),
4320                (8445952, 30816, 4096),
4321                (17379328, 65712, 4096),
4322                (17489920, 66144, 4096),
4323                (28041216, 107360, 4096),
4324                (28151808, 107792, 4096),
4325                (41672704, 160608, 4096),
4326                (41783296, 161040, 4096),
4327                (54444032, 210496, 4096),
4328                (54558720, 210944, 4096),
4329                (65687552, 254416, 4096),
4330                (65802240, 254864, 4096),
4331                (76267520, 295744, 4096),
4332                (76378112, 296176, 4096),
4333                (89624576, 347920, 4096),
4334                (89739264, 348368, 4096),
4335                (99688448, 387232, 4096),
4336                (99803136, 387680, 4096),
4337                (114176000, 443824, 4096),
4338                (114286592, 444256, 4096),
4339                (125415424, 487728, 4096),
4340                (125526016, 488160, 4096),
4341                (138788864, 539968, 4096),
4342                (138903552, 540416, 4096)]),
4343            audiotools.flac.Flac_VORBISCOMMENT([u"TITLE=Foo "], u""),
4344            audiotools.flac.Flac_CUESHEET(
4345                catalog_number=b'4560248013904' + b"\x00" * (128 - 13),
4346                lead_in_samples=88200,
4347                is_cdda=1,
4348                tracks=[
4349                    audiotools.flac.Flac_CUESHEET_track(
4350                        offset=0,
4351                        number=1,
4352                        ISRC=b'JPK631002201',
4353                        track_type=0,
4354                        pre_emphasis=0,
4355                        index_points=[
4356                            audiotools.flac.Flac_CUESHEET_index(
4357                                track_offset=0,
4358                                offset=0,
4359                                number=1)]),
4360                    audiotools.flac.Flac_CUESHEET_track(
4361                        offset=8336076,
4362                        number=2,
4363                        ISRC=b'JPK631002202',
4364                        track_type=0,
4365                        pre_emphasis=0,
4366                        index_points=[
4367                            audiotools.flac.Flac_CUESHEET_index(
4368                                track_offset=8336076,
4369                                offset=0,
4370                                number=0),
4371                            audiotools.flac.Flac_CUESHEET_index(
4372                                track_offset=8336076,
4373                                offset=113484,
4374                                number=1)]),
4375                    audiotools.flac.Flac_CUESHEET_track(
4376                        offset=17379516,
4377                        number=3,
4378                        ISRC=b'JPK631002203',
4379                        track_type=0,
4380                        pre_emphasis=0,
4381                        index_points=[
4382                            audiotools.flac.Flac_CUESHEET_index(
4383                                track_offset=17379516,
4384                                offset=0,
4385                                number=0),
4386                            audiotools.flac.Flac_CUESHEET_index(
4387                                track_offset=17379516,
4388                                offset=113484,
4389                                number=1)]),
4390                    audiotools.flac.Flac_CUESHEET_track(
4391                        offset=28042308,
4392                        number=4,
4393                        ISRC=b'JPK631002204',
4394                        track_type=0,
4395                        pre_emphasis=0,
4396                        index_points=[
4397                            audiotools.flac.Flac_CUESHEET_index(
4398                                track_offset=28042308,
4399                                offset=0,
4400                                number=0),
4401                            audiotools.flac.Flac_CUESHEET_index(
4402                                track_offset=28042308,
4403                                offset=113484,
4404                                number=1)]),
4405                    audiotools.flac.Flac_CUESHEET_track(
4406                        offset=41672736,
4407                        number=5,
4408                        ISRC=b'JPK631002205',
4409                        track_type=0,
4410                        pre_emphasis=0,
4411                        index_points=[
4412                            audiotools.flac.Flac_CUESHEET_index(
4413                                track_offset=41672736,
4414                                offset=0,
4415                                number=0),
4416                            audiotools.flac.Flac_CUESHEET_index(
4417                                track_offset=41672736,
4418                                offset=113484,
4419                                number=1)]),
4420                    audiotools.flac.Flac_CUESHEET_track(
4421                        offset=54447624,
4422                        number=6,
4423                        ISRC=b'JPK631002206',
4424                        track_type=0,
4425                        pre_emphasis=0,
4426                        index_points=[
4427                            audiotools.flac.Flac_CUESHEET_index(
4428                                track_offset=54447624,
4429                                offset=0,
4430                                number=0),
4431                            audiotools.flac.Flac_CUESHEET_index(
4432                                track_offset=54447624,
4433                                offset=113484,
4434                                number=1)]),
4435                    audiotools.flac.Flac_CUESHEET_track(
4436                        offset=65689596,
4437                        number=7,
4438                        ISRC=b'JPK631002207',
4439                        track_type=0,
4440                        pre_emphasis=0,
4441                        index_points=[
4442                            audiotools.flac.Flac_CUESHEET_index(
4443                                track_offset=65689596,
4444                                offset=0,
4445                                number=0),
4446                            audiotools.flac.Flac_CUESHEET_index(
4447                                track_offset=65689596,
4448                                offset=113484,
4449                                number=1)]),
4450                    audiotools.flac.Flac_CUESHEET_track(
4451                        offset=76267716,
4452                        number=8,
4453                        ISRC=b'JPK631002208',
4454                        track_type=0,
4455                        pre_emphasis=0,
4456                        index_points=[
4457                            audiotools.flac.Flac_CUESHEET_index(
4458                                track_offset=76267716,
4459                                offset=0,
4460                                number=0),
4461                            audiotools.flac.Flac_CUESHEET_index(
4462                                track_offset=76267716,
4463                                offset=113484,
4464                                number=1)]),
4465                    audiotools.flac.Flac_CUESHEET_track(
4466                        offset=89627076,
4467                        number=9,
4468                        ISRC=b'JPK631002209',
4469                        track_type=0,
4470                        pre_emphasis=0,
4471                        index_points=[
4472                            audiotools.flac.Flac_CUESHEET_index(
4473                                track_offset=89627076,
4474                                offset=0,
4475                                number=0),
4476                            audiotools.flac.Flac_CUESHEET_index(
4477                                track_offset=89627076,
4478                                offset=113484,
4479                                number=1)]),
4480                    audiotools.flac.Flac_CUESHEET_track(
4481                        offset=99691872,
4482                        number=10,
4483                        ISRC=b'JPK631002210',
4484                        track_type=0,
4485                        pre_emphasis=0,
4486                        index_points=[
4487                            audiotools.flac.Flac_CUESHEET_index(
4488                                track_offset=99691872,
4489                                offset=0,
4490                                number=0),
4491                            audiotools.flac.Flac_CUESHEET_index(
4492                                track_offset=99691872,
4493                                offset=113484,
4494                                number=1)]),
4495                    audiotools.flac.Flac_CUESHEET_track(
4496                        offset=114176076,
4497                        number=11,
4498                        ISRC=b'JPK631002211',
4499                        track_type=0,
4500                        pre_emphasis=0,
4501                        index_points=[
4502                            audiotools.flac.Flac_CUESHEET_index(
4503                                track_offset=114176076,
4504                                offset=0,
4505                                number=0),
4506                            audiotools.flac.Flac_CUESHEET_index(
4507                                track_offset=114176076,
4508                                offset=113484,
4509                                number=1)]),
4510                    audiotools.flac.Flac_CUESHEET_track(
4511                        offset=125415696,
4512                        number=12,
4513                        ISRC=b'JPK631002212',
4514                        track_type=0,
4515                        pre_emphasis=0,
4516                        index_points=[
4517                            audiotools.flac.Flac_CUESHEET_index(
4518                                track_offset=125415696,
4519                                offset=0,
4520                                number=0),
4521                            audiotools.flac.Flac_CUESHEET_index(
4522                                track_offset=125415696,
4523                                offset=114072,
4524                                number=1)]),
4525                    audiotools.flac.Flac_CUESHEET_track(
4526                        offset=138791520,
4527                        number=13,
4528                        ISRC=b'JPK631002213',
4529                        track_type=0,
4530                        pre_emphasis=0,
4531                        index_points=[
4532                            audiotools.flac.Flac_CUESHEET_index(
4533                                track_offset=138791520,
4534                                offset=0,
4535                                number=0),
4536                            audiotools.flac.Flac_CUESHEET_index(
4537                                track_offset=138791520,
4538                                offset=114072,
4539                                number=1)]),
4540                    audiotools.flac.Flac_CUESHEET_track(
4541                        offset=149606016,
4542                        number=170,
4543                        ISRC=b"\x00" * 12,
4544                        track_type=0,
4545                        pre_emphasis=0,
4546                        index_points=[])]),
4547            audiotools.flac.Flac_PICTURE(0, u"image/jpeg", u"",
4548                                         500, 500, 24, 0, TEST_COVER1)])
4549
4550        self.assertEqual([b.BLOCK_ID for b in metadata.block_list],
4551                         [audiotools.flac.Flac_STREAMINFO.BLOCK_ID,
4552                          audiotools.flac.Flac_APPLICATION.BLOCK_ID,
4553                          audiotools.flac.Flac_SEEKTABLE.BLOCK_ID,
4554                          audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID,
4555                          audiotools.flac.Flac_CUESHEET.BLOCK_ID,
4556                          audiotools.flac.Flac_PICTURE.BLOCK_ID])
4557
4558        (cleaned, results) = metadata.clean()
4559        self.assertEqual(results,
4560                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
4561                          {"field": u"TITLE"}])
4562
4563        for block_id in [audiotools.flac.Flac_STREAMINFO.BLOCK_ID,
4564                         audiotools.flac.Flac_APPLICATION.BLOCK_ID,
4565                         audiotools.flac.Flac_SEEKTABLE.BLOCK_ID,
4566                         audiotools.flac.Flac_CUESHEET.BLOCK_ID,
4567                         audiotools.flac.Flac_PICTURE.BLOCK_ID]:
4568            self.assertEqual(metadata.get_blocks(block_id),
4569                             cleaned.get_blocks(block_id))
4570        self.assertNotEqual(
4571            metadata.get_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID),
4572            cleaned.get_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID))
4573
4574        # ensure second STREAMINFO block is removed, if present
4575        streaminfo1 = audiotools.flac.Flac_STREAMINFO(
4576            1, 10, 1, 20, 44100, 2, 16, 5000, chr(0) * 16)
4577        streaminfo2 = audiotools.flac.Flac_STREAMINFO(
4578            1, 20, 1, 30, 88200, 4, 24, 5000, chr(0) * 16)
4579        self.assertNotEqual(streaminfo1, streaminfo2)
4580        metadata = audiotools.flac.FlacMetaData([streaminfo1, streaminfo2])
4581        self.assertEqual(
4582            metadata.get_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID),
4583            [streaminfo1, streaminfo2])
4584
4585        (cleaned, results) = metadata.clean()
4586        self.assertEqual(results,
4587                         [CLEAN_FLAC_MULITPLE_STREAMINFO])
4588        self.assertEqual(
4589            cleaned.get_blocks(audiotools.flac.Flac_STREAMINFO.BLOCK_ID),
4590            [streaminfo1])
4591
4592        # ensure second VORBISCOMMENT block is removed, if present
4593        comment1 = audiotools.flac.Flac_VORBISCOMMENT(
4594            [u"TITLE=Foo"],
4595            u"vendor string")
4596
4597        comment2 = audiotools.flac.Flac_VORBISCOMMENT(
4598            [u"TITLE=Bar"],
4599            u"vendor string")
4600        self.assertNotEqual(comment1, comment2)
4601        metadata = audiotools.flac.FlacMetaData([streaminfo1,
4602                                                 comment1,
4603                                                 comment2])
4604        self.assertEqual(metadata.get_blocks(
4605            audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID),
4606            [comment1, comment2])
4607
4608        (cleaned, results) = metadata.clean()
4609        self.assertEqual(results,
4610                         [CLEAN_FLAC_MULTIPLE_VORBISCOMMENT])
4611        self.assertEqual(
4612            cleaned.get_blocks(audiotools.flac.Flac_VORBISCOMMENT.BLOCK_ID),
4613            [comment1])
4614
4615        # ensure second SEEKTABLE block is removed, if present
4616        seektable1 = audiotools.flac.Flac_SEEKTABLE([(0, 0, 4096),
4617                                                     (4096, 10, 4096)])
4618        seektable2 = audiotools.flac.Flac_SEEKTABLE([(0, 0, 4096),
4619                                                     (4096, 10, 4096),
4620                                                     (8192, 20, 4096)])
4621        self.assertNotEqual(seektable1, seektable2)
4622        metadata = audiotools.flac.FlacMetaData([streaminfo1,
4623                                                 seektable1,
4624                                                 seektable2])
4625
4626        self.assertEqual(metadata.get_blocks(
4627            audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
4628            [seektable1, seektable2])
4629
4630        (cleaned, results) = metadata.clean()
4631        self.assertEqual(results,
4632                         [CLEAN_FLAC_MULTIPLE_SEEKTABLE])
4633        self.assertEqual(
4634            cleaned.get_blocks(audiotools.flac.Flac_SEEKTABLE.BLOCK_ID),
4635            [seektable1])
4636
4637        # ensure second CUESHEET block is removed, if present
4638        cuesheet1 = audiotools.flac.Flac_CUESHEET.converted(
4639            audiotools.read_sheet("metadata_flac_cuesheet-1.cue"),
4640            160107696,
4641            44100)
4642        cuesheet2 = audiotools.flac.Flac_CUESHEET.converted(
4643            audiotools.read_sheet("metadata_flac_cuesheet-2.cue"),
4644            119882616,
4645            44100)
4646
4647        self.assertNotEqual(cuesheet1, cuesheet2)
4648        metadata = audiotools.flac.FlacMetaData([streaminfo1,
4649                                                 cuesheet1,
4650                                                 cuesheet2])
4651
4652        self.assertEqual(metadata.get_blocks(
4653            audiotools.flac.Flac_CUESHEET.BLOCK_ID),
4654            [cuesheet1, cuesheet2])
4655
4656        (cleaned, results) = metadata.clean()
4657        self.assertEqual(results,
4658                         [CLEAN_FLAC_MULTIPLE_CUESHEET])
4659        self.assertEqual(
4660            cleaned.get_blocks(audiotools.flac.Flac_CUESHEET.BLOCK_ID),
4661            [cuesheet1])
4662
4663    @METADATA_FLAC
4664    def test_replay_gain(self):
4665        import test_streams
4666
4667        for input_class in [audiotools.FlacAudio,
4668                            audiotools.OggFlacAudio,
4669                            audiotools.VorbisAudio]:
4670            temp1 = tempfile.NamedTemporaryFile(
4671                suffix="." + input_class.SUFFIX)
4672            try:
4673                track1 = input_class.from_pcm(
4674                    temp1.name,
4675                    test_streams.Sine16_Stereo(44100, 44100,
4676                                               441.0, 0.50,
4677                                               4410.0, 0.49, 1.0))
4678                self.assertIsNone(track1.get_replay_gain(),
4679                                  "ReplayGain present for class %s" %
4680                                  (input_class.NAME))
4681                track1.set_metadata(audiotools.MetaData(track_name=u"Foo"))
4682                audiotools.add_replay_gain([track1])
4683                self.assertEqual(track1.get_metadata().track_name, u"Foo")
4684                self.assertIsNotNone(track1.get_replay_gain(),
4685                                     "ReplayGain not present for class %s" %
4686                                     (input_class.NAME))
4687
4688                for output_class in [audiotools.FlacAudio,
4689                                     audiotools.OggFlacAudio]:
4690                    temp2 = tempfile.NamedTemporaryFile(
4691                        suffix="." + input_class.SUFFIX)
4692                    try:
4693                        # ensure file with no metadata blocks
4694                        # has metadata set correctly
4695                        track2 = output_class.from_pcm(
4696                            temp2.name,
4697                            test_streams.Sine16_Stereo(66150, 44100,
4698                                                       8820.0, 0.70,
4699                                                       4410.0, 0.29, 1.0))
4700                        metadata = track2.get_metadata()
4701                        for block_id in range(1, 7):
4702                            metadata.replace_blocks(block_id, [])
4703                        track2.update_metadata(metadata)
4704                        self.assertIsNone(track2.get_replay_gain())
4705
4706                        audiotools.add_replay_gain([track2])
4707                        self.assertIsNotNone(track2.get_replay_gain())
4708
4709                        track2 = output_class.from_pcm(
4710                            temp2.name,
4711                            test_streams.Sine16_Stereo(66150, 44100,
4712                                                       8820.0, 0.70,
4713                                                       4410.0, 0.29, 1.0))
4714
4715                        # ensure that ReplayGain doesn't get ported
4716                        # via set_metadata()
4717                        self.assertIsNone(
4718                            track2.get_replay_gain(),
4719                            "ReplayGain present for class %s" %
4720                            (output_class.NAME))
4721                        track2.set_metadata(track1.get_metadata())
4722                        self.assertEqual(track2.get_metadata().track_name,
4723                                         u"Foo")
4724                        self.assertIsNone(
4725                            track2.get_replay_gain(),
4726                            "ReplayGain present for class %s from %s" %
4727                            (output_class.NAME,
4728                             input_class.NAME))
4729
4730                        # and if ReplayGain is already set,
4731                        # ensure set_metadata() doesn't remove it
4732                        audiotools.add_replay_gain([track2])
4733                        old_replay_gain = track2.get_replay_gain()
4734                        self.assertIsNotNone(old_replay_gain)
4735                        track2.set_metadata(
4736                            audiotools.MetaData(track_name=u"Bar"))
4737                        self.assertEqual(track2.get_metadata().track_name,
4738                                         u"Bar")
4739                        self.assertEqual(track2.get_replay_gain(),
4740                                         old_replay_gain)
4741                    finally:
4742                        temp2.close()
4743            finally:
4744                temp1.close()
4745
4746    @METADATA_FLAC
4747    def test_getattr(self):
4748        # track_number grabs the first available integer, if any
4749        self.assertEqual(
4750            audiotools.FlacMetaData([]).track_number, None)
4751
4752        self.assertEqual(
4753            audiotools.FlacMetaData([
4754                audiotools.flac.Flac_VORBISCOMMENT(
4755                    [u"TRACKNUMBER=10"],
4756                    u"vendor")]).track_number,
4757            10)
4758
4759        self.assertEqual(
4760            audiotools.FlacMetaData(
4761                [audiotools.flac.Flac_VORBISCOMMENT(
4762                    [u"TRACKNUMBER=10",
4763                     u"TRACKNUMBER=5"],
4764                    u"vendor")]).track_number,
4765            10)
4766
4767        self.assertEqual(
4768            audiotools.FlacMetaData(
4769                [audiotools.flac.Flac_VORBISCOMMENT(
4770                    [u"TRACKNUMBER=foo 10 bar"],
4771                    u"vendor")]).track_number,
4772            10)
4773
4774        self.assertEqual(
4775            audiotools.FlacMetaData(
4776                [audiotools.flac.Flac_VORBISCOMMENT(
4777                    [u"TRACKNUMBER=foo",
4778                     u"TRACKNUMBER=10"],
4779                    u"vendor")]).track_number,
4780            10)
4781
4782        self.assertEqual(
4783            audiotools.FlacMetaData(
4784                [audiotools.flac.Flac_VORBISCOMMENT(
4785                    [u"TRACKNUMBER=foo",
4786                     u"TRACKNUMBER=foo 10 bar"],
4787                    u"vendor")]).track_number,
4788            10)
4789
4790        # track_number is case-insensitive
4791        self.assertEqual(
4792            audiotools.FlacMetaData(
4793                [audiotools.flac.Flac_VORBISCOMMENT(
4794                    [u"tRaCkNuMbEr=10"],
4795                    u"vendor")]).track_number,
4796            10)
4797
4798        # album_number grabs the first available integer, if any
4799        self.assertEqual(
4800            audiotools.FlacMetaData([]).album_number, None)
4801
4802        self.assertEqual(
4803            audiotools.FlacMetaData(
4804                [audiotools.flac.Flac_VORBISCOMMENT(
4805                    [u"DISCNUMBER=20"],
4806                    u"vendor")]).album_number,
4807            20)
4808
4809        self.assertEqual(
4810            audiotools.FlacMetaData(
4811                [audiotools.flac.Flac_VORBISCOMMENT(
4812                    [u"DISCNUMBER=20",
4813                     u"DISCNUMBER=5"],
4814                    u"vendor")]).album_number,
4815            20)
4816
4817        self.assertEqual(
4818            audiotools.FlacMetaData(
4819                [audiotools.flac.Flac_VORBISCOMMENT(
4820                    [u"DISCNUMBER=foo 20 bar"],
4821                    u"vendor")]).album_number,
4822            20)
4823
4824        self.assertEqual(
4825            audiotools.FlacMetaData(
4826                [audiotools.flac.Flac_VORBISCOMMENT(
4827                    [u"DISCNUMBER=foo",
4828                     u"DISCNUMBER=20"],
4829                    u"vendor")]).album_number,
4830            20)
4831
4832        self.assertEqual(
4833            audiotools.FlacMetaData(
4834                [audiotools.flac.Flac_VORBISCOMMENT(
4835                    [u"DISCNUMBER=foo",
4836                     u"DISCNUMBER=foo 20 bar"],
4837                    u"vendor")]).album_number,
4838            20)
4839
4840        # album_number is case-insensitive
4841        self.assertEqual(
4842            audiotools.FlacMetaData(
4843                [audiotools.flac.Flac_VORBISCOMMENT(
4844                    [u"dIsCnUmBeR=20"],
4845                    u"vendor")]).album_number,
4846            20)
4847
4848        # track_total grabs the first available TRACKTOTAL integer
4849        # before falling back on slashed fields, if any
4850        self.assertEqual(
4851            audiotools.FlacMetaData([]).track_total, None)
4852
4853        self.assertEqual(
4854            audiotools.FlacMetaData(
4855                [audiotools.flac.Flac_VORBISCOMMENT(
4856                    [u"TRACKTOTAL=15"],
4857                    u"vendor")]).track_total,
4858            15)
4859
4860        self.assertEqual(
4861            audiotools.FlacMetaData(
4862                [audiotools.flac.Flac_VORBISCOMMENT(
4863                    [u"TRACKNUMBER=5/10"],
4864                    u"vendor")]).track_total,
4865            10)
4866
4867        self.assertEqual(
4868            audiotools.FlacMetaData(
4869                [audiotools.flac.Flac_VORBISCOMMENT(
4870                    [u"TRACKNUMBER=5/10",
4871                     u"TRACKTOTAL=15"],
4872                    u"vendor")]).track_total,
4873            15)
4874
4875        self.assertEqual(
4876            audiotools.FlacMetaData(
4877                [audiotools.flac.Flac_VORBISCOMMENT(
4878                    [u"TRACKTOTAL=15",
4879                     u"TRACKNUMBER=5/10"],
4880                    u"vendor")]).track_total,
4881            15)
4882
4883        # track_total is case-insensitive
4884        self.assertEqual(
4885            audiotools.FlacMetaData(
4886                [audiotools.flac.Flac_VORBISCOMMENT(
4887                    [u"tracktotal=15"],
4888                    u"vendor")]).track_total,
4889            15)
4890
4891        # track_total supports aliases
4892        self.assertEqual(
4893            audiotools.FlacMetaData(
4894                [audiotools.flac.Flac_VORBISCOMMENT(
4895                    [u"TOTALTRACKS=15"],
4896                    u"vendor")]).track_total,
4897            15)
4898
4899        # album_total grabs the first available DISCTOTAL integer
4900        # before falling back on slashed fields, if any
4901        self.assertEqual(
4902            audiotools.FlacMetaData([]).album_total, None)
4903
4904        self.assertEqual(
4905            audiotools.FlacMetaData(
4906                [audiotools.flac.Flac_VORBISCOMMENT(
4907                    [u"DISCTOTAL=25"],
4908                    u"vendor")]).album_total,
4909            25)
4910
4911        self.assertEqual(
4912            audiotools.FlacMetaData(
4913                [audiotools.flac.Flac_VORBISCOMMENT(
4914                    [u"DISCNUMBER=10/30"],
4915                    u"vendor")]).album_total,
4916            30)
4917
4918        self.assertEqual(
4919            audiotools.FlacMetaData(
4920                [audiotools.flac.Flac_VORBISCOMMENT(
4921                    [u"DISCNUMBER=10/30",
4922                     u"DISCTOTAL=25"],
4923                    u"vendor")]).album_total,
4924            25)
4925
4926        self.assertEqual(
4927            audiotools.FlacMetaData(
4928                [audiotools.flac.Flac_VORBISCOMMENT(
4929                    [u"DISCTOTAL=25",
4930                     u"DISCNUMBER=10/30"],
4931                    u"vendor")]).album_total,
4932            25)
4933
4934        # album_total is case-insensitive
4935        self.assertEqual(
4936            audiotools.FlacMetaData(
4937                [audiotools.flac.Flac_VORBISCOMMENT(
4938                    [u"disctotal=25"],
4939                    u"vendor")]).album_total,
4940            25)
4941
4942        # album_total supports aliases
4943        self.assertEqual(
4944            audiotools.FlacMetaData(
4945                [audiotools.flac.Flac_VORBISCOMMENT(
4946                    [u"TOTALDISCS=25"],
4947                    u"vendor")]).album_total,
4948            25)
4949
4950        # other fields grab the first available item
4951        self.assertEqual(
4952            audiotools.FlacMetaData(
4953                [audiotools.flac.Flac_VORBISCOMMENT(
4954                    [u"TITLE=first",
4955                     u"TITLE=last"],
4956                    u"vendor")]).track_name,
4957            u"first")
4958
4959    @METADATA_FLAC
4960    def test_setattr(self):
4961        # track_number adds new field if necessary
4962        for metadata in [audiotools.FlacMetaData(
4963                         [audiotools.flac.Flac_VORBISCOMMENT([],
4964                                                             u"vendor")]),
4965                         audiotools.FlacMetaData([])]:
4966
4967            self.assertIsNone(metadata.track_number)
4968            metadata.track_number = 11
4969            self.assertEqual(metadata.get_block(4).comment_strings,
4970                             [u"TRACKNUMBER=11"])
4971            self.assertEqual(metadata.track_number, 11)
4972
4973            metadata = audiotools.FlacMetaData([
4974                audiotools.flac.Flac_VORBISCOMMENT(
4975                    [u"TRACKNUMBER=blah"],
4976                    u"vendor")])
4977            self.assertIsNone(metadata.track_number)
4978            metadata.track_number = 11
4979            self.assertEqual(metadata.get_block(4).comment_strings,
4980                             [u"TRACKNUMBER=blah",
4981                              u"TRACKNUMBER=11"])
4982            self.assertEqual(metadata.track_number, 11)
4983
4984        # track_number updates the first integer field
4985        # and leaves other junk in that field alone
4986        metadata = audiotools.FlacMetaData(
4987            [audiotools.flac.Flac_VORBISCOMMENT(
4988                [u"TRACKNUMBER=10/12"], u"vendor")])
4989        self.assertEqual(metadata.track_number, 10)
4990        metadata.track_number = 11
4991        self.assertEqual(metadata.get_block(4).comment_strings,
4992                         [u"TRACKNUMBER=11/12"])
4993        self.assertEqual(metadata.track_number, 11)
4994
4995        metadata = audiotools.FlacMetaData([
4996            audiotools.flac.Flac_VORBISCOMMENT(
4997                [u"TRACKNUMBER=foo 10 bar"],
4998                u"vendor")])
4999        self.assertEqual(metadata.track_number, 10)
5000        metadata.track_number = 11
5001        self.assertEqual(metadata.get_block(4).comment_strings,
5002                         [u"TRACKNUMBER=foo 11 bar"])
5003        self.assertEqual(metadata.track_number, 11)
5004
5005        metadata = audiotools.FlacMetaData([
5006            audiotools.flac.Flac_VORBISCOMMENT(
5007                [u"TRACKNUMBER=foo 10 bar",
5008                 u"TRACKNUMBER=blah"],
5009                u"vendor")])
5010        self.assertEqual(metadata.track_number, 10)
5011        metadata.track_number = 11
5012        self.assertEqual(metadata.get_block(4).comment_strings,
5013                         [u"TRACKNUMBER=foo 11 bar",
5014                          u"TRACKNUMBER=blah"])
5015        self.assertEqual(metadata.track_number, 11)
5016
5017        # album_number adds new field if necessary
5018        for metadata in [
5019            audiotools.FlacMetaData([
5020                audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]),
5021                audiotools.FlacMetaData([])]:
5022
5023            self.assertIsNone(metadata.album_number)
5024            metadata.album_number = 3
5025            self.assertEqual(metadata.get_block(4).comment_strings,
5026                             [u"DISCNUMBER=3"])
5027            self.assertEqual(metadata.album_number, 3)
5028
5029            metadata = audiotools.FlacMetaData([
5030                audiotools.flac.Flac_VORBISCOMMENT(
5031                    [u"DISCNUMBER=blah"],
5032                    u"vendor")])
5033            self.assertIsNone(metadata.album_number)
5034            metadata.album_number = 3
5035            self.assertEqual(metadata.get_block(4).comment_strings,
5036                             [u"DISCNUMBER=blah",
5037                              u"DISCNUMBER=3"])
5038            self.assertEqual(metadata.album_number, 3)
5039
5040        # album_number updates the first integer field
5041        # and leaves other junk in that field alone
5042        metadata = audiotools.FlacMetaData([
5043            audiotools.flac.Flac_VORBISCOMMENT(
5044                [u"DISCNUMBER=2/4"], u"vendor")])
5045        self.assertEqual(metadata.album_number, 2)
5046        metadata.album_number = 3
5047        self.assertEqual(metadata.get_block(4).comment_strings,
5048                         [u"DISCNUMBER=3/4"])
5049        self.assertEqual(metadata.album_number, 3)
5050
5051        metadata = audiotools.FlacMetaData([
5052            audiotools.flac.Flac_VORBISCOMMENT(
5053                [u"DISCNUMBER=foo 2 bar"],
5054                u"vendor")])
5055        self.assertEqual(metadata.album_number, 2)
5056        metadata.album_number = 3
5057        self.assertEqual(metadata.get_block(4).comment_strings,
5058                         [u"DISCNUMBER=foo 3 bar"])
5059        self.assertEqual(metadata.album_number, 3)
5060
5061        metadata = audiotools.FlacMetaData([
5062            audiotools.flac.Flac_VORBISCOMMENT(
5063                [u"DISCNUMBER=foo 2 bar",
5064                 u"DISCNUMBER=blah"],
5065                u"vendor")])
5066        self.assertEqual(metadata.album_number, 2)
5067        metadata.album_number = 3
5068        self.assertEqual(metadata.get_block(4).comment_strings,
5069                         [u"DISCNUMBER=foo 3 bar",
5070                          u"DISCNUMBER=blah"])
5071        self.assertEqual(metadata.album_number, 3)
5072
5073        # track_total adds new TRACKTOTAL field if necessary
5074        for metadata in [
5075            audiotools.FlacMetaData([
5076                audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]),
5077                audiotools.FlacMetaData([])]:
5078
5079            self.assertIsNone(metadata.track_total)
5080            metadata.track_total = 12
5081            self.assertEqual(metadata.get_block(4).comment_strings,
5082                             [u"TRACKTOTAL=12"])
5083            self.assertEqual(metadata.track_total, 12)
5084
5085            metadata = audiotools.FlacMetaData([
5086                audiotools.flac.Flac_VORBISCOMMENT(
5087                    [u"TRACKTOTAL=blah"],
5088                    u"vendor")])
5089            self.assertIsNone(metadata.track_total)
5090            metadata.track_total = 12
5091            self.assertEqual(metadata.get_block(4).comment_strings,
5092                             [u"TRACKTOTAL=blah",
5093                              u"TRACKTOTAL=12"])
5094            self.assertEqual(metadata.track_total, 12)
5095
5096        # track_total updates first integer TRACKTOTAL field first if possible
5097        # including aliases
5098        metadata = audiotools.FlacMetaData([
5099            audiotools.flac.Flac_VORBISCOMMENT(
5100                [u"TRACKTOTAL=blah",
5101                 u"TRACKTOTAL=2"],
5102                u"vendor")])
5103        self.assertEqual(metadata.track_total, 2)
5104        metadata.track_total = 3
5105        self.assertEqual(metadata.get_block(4).comment_strings,
5106                         [u"TRACKTOTAL=blah",
5107                          u"TRACKTOTAL=3"])
5108        self.assertEqual(metadata.track_total, 3)
5109
5110        metadata = audiotools.FlacMetaData([
5111            audiotools.flac.Flac_VORBISCOMMENT(
5112                [u"TOTALTRACKS=blah",
5113                 u"TOTALTRACKS=2"],
5114                u"vendor")])
5115        self.assertEqual(metadata.track_total, 2)
5116        metadata.track_total = 3
5117        self.assertEqual(metadata.get_block(4).comment_strings,
5118                         [u"TOTALTRACKS=blah",
5119                          u"TOTALTRACKS=3"])
5120        self.assertEqual(metadata.track_total, 3)
5121
5122        # track_total updates slashed TRACKNUMBER field if necessary
5123        metadata = audiotools.FlacMetaData([
5124            audiotools.flac.Flac_VORBISCOMMENT(
5125                [u"TRACKNUMBER=1/4",
5126                 u"TRACKTOTAL=2"],
5127                u"vendor")])
5128        self.assertEqual(metadata.track_total, 2)
5129        metadata.track_total = 3
5130        self.assertEqual(metadata.get_block(4).comment_strings,
5131                         [u"TRACKNUMBER=1/4",
5132                          u"TRACKTOTAL=3"])
5133        self.assertEqual(metadata.track_total, 3)
5134
5135        metadata = audiotools.FlacMetaData([
5136            audiotools.flac.Flac_VORBISCOMMENT(
5137                [u"TRACKNUMBER=1/4"],
5138                u"vendor")])
5139        self.assertEqual(metadata.track_total, 4)
5140        metadata.track_total = 3
5141        self.assertEqual(metadata.get_block(4).comment_strings,
5142                         [u"TRACKNUMBER=1/3"])
5143        self.assertEqual(metadata.track_total, 3)
5144
5145        metadata = audiotools.FlacMetaData([
5146            audiotools.flac.Flac_VORBISCOMMENT(
5147                [u"TRACKNUMBER= foo / 4 bar"],
5148                u"vendor")])
5149        self.assertEqual(metadata.track_total, 4)
5150        metadata.track_total = 3
5151        self.assertEqual(metadata.get_block(4).comment_strings,
5152                         [u"TRACKNUMBER= foo / 3 bar"])
5153        self.assertEqual(metadata.track_total, 3)
5154
5155        # album_total adds new DISCTOTAL field if necessary
5156        for metadata in [audiotools.FlacMetaData(
5157                [audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]),
5158                audiotools.FlacMetaData([])]:
5159
5160            self.assertIsNone(metadata.album_total)
5161            metadata.album_total = 4
5162            self.assertEqual(metadata.get_block(4).comment_strings,
5163                             [u"DISCTOTAL=4"])
5164            self.assertEqual(metadata.album_total, 4)
5165
5166            metadata = audiotools.FlacMetaData([
5167                audiotools.flac.Flac_VORBISCOMMENT(
5168                    [u"DISCTOTAL=blah"],
5169                    u"vendor")])
5170            self.assertIsNone(metadata.album_total)
5171            metadata.album_total = 4
5172            self.assertEqual(metadata.get_block(4).comment_strings,
5173                             [u"DISCTOTAL=blah",
5174                              u"DISCTOTAL=4"])
5175            self.assertEqual(metadata.album_total, 4)
5176
5177        # album_total updates DISCTOTAL field first if possible
5178        # including aliases
5179        metadata = audiotools.FlacMetaData([
5180            audiotools.flac.Flac_VORBISCOMMENT(
5181                [u"DISCTOTAL=blah",
5182                 u"DISCTOTAL=3"],
5183                u"vendor")])
5184        self.assertEqual(metadata.album_total, 3)
5185        metadata.album_total = 4
5186        self.assertEqual(metadata.get_block(4).comment_strings,
5187                         [u"DISCTOTAL=blah",
5188                          u"DISCTOTAL=4"])
5189        self.assertEqual(metadata.album_total, 4)
5190
5191        metadata = audiotools.FlacMetaData([
5192            audiotools.flac.Flac_VORBISCOMMENT(
5193                [u"TOTALDISCS=blah",
5194                 u"TOTALDISCS=3"],
5195                u"vendor")])
5196        self.assertEqual(metadata.album_total, 3)
5197        metadata.album_total = 4
5198        self.assertEqual(metadata.get_block(4).comment_strings,
5199                         [u"TOTALDISCS=blah",
5200                          u"TOTALDISCS=4"])
5201        self.assertEqual(metadata.album_total, 4)
5202
5203        # album_total updates slashed DISCNUMBER field if necessary
5204        metadata = audiotools.FlacMetaData([
5205            audiotools.flac.Flac_VORBISCOMMENT(
5206                [u"DISCNUMBER=2/3",
5207                 u"DISCTOTAL=5"],
5208                u"vendor")])
5209        self.assertEqual(metadata.album_total, 5)
5210        metadata.album_total = 6
5211        self.assertEqual(metadata.get_block(4).comment_strings,
5212                         [u"DISCNUMBER=2/3",
5213                          u"DISCTOTAL=6"])
5214        self.assertEqual(metadata.album_total, 6)
5215
5216        metadata = audiotools.FlacMetaData([
5217            audiotools.flac.Flac_VORBISCOMMENT(
5218                [u"DISCNUMBER=2/3"],
5219                u"vendor")])
5220        self.assertEqual(metadata.album_total, 3)
5221        metadata.album_total = 6
5222        self.assertEqual(metadata.get_block(4).comment_strings,
5223                         [u"DISCNUMBER=2/6"])
5224        self.assertEqual(metadata.album_total, 6)
5225
5226        metadata = audiotools.FlacMetaData([
5227            audiotools.flac.Flac_VORBISCOMMENT(
5228                [u"DISCNUMBER= foo / 3 bar"],
5229                u"vendor")])
5230        self.assertEqual(metadata.album_total, 3)
5231        metadata.album_total = 6
5232        self.assertEqual(metadata.get_block(4).comment_strings,
5233                         [u"DISCNUMBER= foo / 6 bar"])
5234        self.assertEqual(metadata.album_total, 6)
5235
5236        # other fields update the first match
5237        # while leaving the rest alone
5238        metadata = audiotools.FlacMetaData([])
5239        metadata.track_name = u"blah"
5240        self.assertEqual(metadata.track_name, u"blah")
5241        self.assertEqual(metadata.get_block(4).comment_strings,
5242                         [u"TITLE=blah"])
5243
5244        metadata = audiotools.FlacMetaData([
5245            audiotools.flac.Flac_VORBISCOMMENT(
5246                [u"TITLE=foo",
5247                 u"TITLE=bar",
5248                 u"FOO=baz"],
5249                u"vendor")])
5250        metadata.track_name = u"blah"
5251        self.assertEqual(metadata.track_name, u"blah")
5252        self.assertEqual(metadata.get_block(4).comment_strings,
5253                         [u"TITLE=blah",
5254                          u"TITLE=bar",
5255                          u"FOO=baz"])
5256
5257        # setting field to an empty string is okay
5258        for metadata in [
5259                audiotools.FlacMetaData(
5260                    [audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]),
5261                audiotools.FlacMetaData([])]:
5262            metadata.track_name = u""
5263            self.assertEqual(metadata.track_name, u"")
5264            self.assertEqual(metadata.get_block(4).comment_strings,
5265                             [u"TITLE="])
5266
5267    @METADATA_FLAC
5268    def test_delattr(self):
5269        # deleting nonexistent field is okay
5270        for field in audiotools.MetaData.FIELDS:
5271            for metadata in [
5272                    audiotools.FlacMetaData(
5273                        [audiotools.flac.Flac_VORBISCOMMENT([], u"vendor")]),
5274                    audiotools.FlacMetaData([])]:
5275
5276                delattr(metadata, field)
5277                self.assertIsNone(getattr(metadata, field))
5278
5279        # deleting field removes all instances of it
5280        metadata = audiotools.FlacMetaData(
5281            [audiotools.flac.Flac_VORBISCOMMENT(
5282                [u"TITLE=track name"],
5283                u"vendor")])
5284        del(metadata.track_name)
5285        self.assertEqual(metadata.get_block(4).comment_strings,
5286                         [])
5287        self.assertIsNone(metadata.track_name)
5288
5289        metadata = audiotools.FlacMetaData(
5290            [audiotools.flac.Flac_VORBISCOMMENT(
5291                [u"TITLE=track name",
5292                 u"ALBUM=album name"],
5293                u"vendor")])
5294        del(metadata.track_name)
5295        self.assertEqual(metadata.get_block(4).comment_strings,
5296                         [u"ALBUM=album name"])
5297        self.assertIsNone(metadata.track_name)
5298
5299        metadata = audiotools.FlacMetaData(
5300            [audiotools.flac.Flac_VORBISCOMMENT(
5301                [u"TITLE=track name",
5302                 u"TITLE=track name 2",
5303                 u"ALBUM=album name",
5304                 u"TITLE=track name 3"],
5305                u"vendor")])
5306        del(metadata.track_name)
5307        self.assertEqual(metadata.get_block(4).comment_strings,
5308                         [u"ALBUM=album name"])
5309        self.assertIsNone(metadata.track_name)
5310
5311        # setting field to None is the same as deleting field
5312        metadata = audiotools.FlacMetaData([])
5313        metadata.track_name = None
5314        self.assertIsNone(metadata.track_name)
5315
5316        metadata = audiotools.FlacMetaData([
5317            audiotools.flac.Flac_VORBISCOMMENT(
5318                [u"TITLE=track name"],
5319                u"vendor")])
5320        metadata.track_name = None
5321        self.assertEqual(metadata.get_block(4).comment_strings,
5322                         [])
5323        self.assertIsNone(metadata.track_name)
5324
5325        # deleting track_number removes TRACKNUMBER field
5326        metadata = audiotools.FlacMetaData([
5327            audiotools.flac.Flac_VORBISCOMMENT(
5328                [u"TRACKNUMBER=1"],
5329                u"vendor")])
5330        del(metadata.track_number)
5331        self.assertEqual(metadata.get_block(4).comment_strings,
5332                         [])
5333        self.assertIsNone(metadata.track_name)
5334
5335        # deleting slashed TRACKNUMBER converts it to fresh TRACKTOTAL field
5336        metadata = audiotools.FlacMetaData([
5337            audiotools.flac.Flac_VORBISCOMMENT(
5338                [u"TRACKNUMBER=1/3"],
5339                u"vendor")])
5340        del(metadata.track_number)
5341        self.assertEqual(metadata.get_block(4).comment_strings,
5342                         [u"TRACKTOTAL=3"])
5343        self.assertIsNone(metadata.track_number)
5344
5345        metadata = audiotools.FlacMetaData([
5346            audiotools.flac.Flac_VORBISCOMMENT(
5347                [u"TRACKNUMBER=1/3",
5348                 u"TRACKTOTAL=4"],
5349                u"vendor")])
5350        self.assertEqual(metadata.track_total, 4)
5351        del(metadata.track_number)
5352        self.assertEqual(metadata.get_block(4).comment_strings,
5353                         [u"TRACKTOTAL=4"])
5354        self.assertEqual(metadata.track_total, 4)
5355        self.assertIsNone(metadata.track_number)
5356
5357        # deleting track_total removes TRACKTOTAL/TOTALTRACKS fields
5358        metadata = audiotools.FlacMetaData([
5359            audiotools.flac.Flac_VORBISCOMMENT(
5360                [u"TRACKTOTAL=3",
5361                 u"TOTALTRACKS=4"],
5362                u"vendor")])
5363        del(metadata.track_total)
5364        self.assertEqual(metadata.get_block(4).comment_strings,
5365                         [])
5366        self.assertIsNone(metadata.track_total)
5367
5368        # deleting track_total also removes slashed side of TRACKNUMBER fields
5369        metadata = audiotools.FlacMetaData([
5370            audiotools.flac.Flac_VORBISCOMMENT(
5371                [u"TRACKNUMBER=1/3"],
5372                u"vendor")])
5373        del(metadata.track_total)
5374        self.assertIsNone(metadata.track_total)
5375        self.assertEqual(metadata.get_block(4).comment_strings,
5376                         [u"TRACKNUMBER=1"])
5377
5378        metadata = audiotools.FlacMetaData([
5379            audiotools.flac.Flac_VORBISCOMMENT(
5380                [u"TRACKNUMBER=1 / foo 3 baz"],
5381                u"vendor")])
5382        del(metadata.track_total)
5383        self.assertIsNone(metadata.track_total)
5384        self.assertEqual(metadata.get_block(4).comment_strings,
5385                         [u"TRACKNUMBER=1"])
5386
5387        metadata = audiotools.FlacMetaData([
5388            audiotools.flac.Flac_VORBISCOMMENT(
5389                [u"TRACKNUMBER= foo 1 bar / blah 4 baz"], u"vendor")])
5390        del(metadata.track_total)
5391        self.assertIsNone(metadata.track_total)
5392        self.assertEqual(metadata.get_block(4).comment_strings,
5393                         [u"TRACKNUMBER= foo 1 bar"])
5394
5395        # deleting album_number removes DISCNUMBER field
5396        metadata = audiotools.FlacMetaData([
5397            audiotools.flac.Flac_VORBISCOMMENT(
5398                [u"DISCNUMBER=2"],
5399                u"vendor")])
5400        del(metadata.album_number)
5401        self.assertEqual(metadata.get_block(4).comment_strings,
5402                         [])
5403
5404        # deleting slashed DISCNUMBER converts it to fresh DISCTOTAL field
5405        metadata = audiotools.FlacMetaData([
5406            audiotools.flac.Flac_VORBISCOMMENT(
5407                [u"DISCNUMBER=2/4"],
5408                u"vendor")])
5409        del(metadata.album_number)
5410        self.assertEqual(metadata.get_block(4).comment_strings,
5411                         [u"DISCTOTAL=4"])
5412
5413        metadata = audiotools.FlacMetaData([
5414            audiotools.flac.Flac_VORBISCOMMENT(
5415                [u"DISCNUMBER=2/4",
5416                 u"DISCTOTAL=5"],
5417                u"vendor")])
5418        self.assertEqual(metadata.album_total, 5)
5419        del(metadata.album_number)
5420        self.assertEqual(metadata.get_block(4).comment_strings,
5421                         [u"DISCTOTAL=5"])
5422        self.assertEqual(metadata.album_total, 5)
5423
5424        # deleting album_total removes DISCTOTAL/TOTALDISCS fields
5425        metadata = audiotools.FlacMetaData([
5426            audiotools.flac.Flac_VORBISCOMMENT(
5427                [u"DISCTOTAL=4",
5428                 u"TOTALDISCS=5"],
5429                u"vendor")])
5430        del(metadata.album_total)
5431        self.assertEqual(metadata.get_block(4).comment_strings,
5432                         [])
5433        self.assertIsNone(metadata.album_total)
5434
5435        # deleting album_total also removes slashed side of DISCNUMBER fields
5436        metadata = audiotools.FlacMetaData([
5437            audiotools.flac.Flac_VORBISCOMMENT(
5438                [u"DISCNUMBER=2/4"],
5439                u"vendor")])
5440        del(metadata.album_total)
5441        self.assertIsNone(metadata.album_total)
5442        self.assertEqual(metadata.get_block(4).comment_strings,
5443                         [u"DISCNUMBER=2"])
5444
5445        metadata = audiotools.FlacMetaData([
5446            audiotools.flac.Flac_VORBISCOMMENT(
5447                [u"DISCNUMBER=2 / foo 4 baz"],
5448                u"vendor")])
5449        del(metadata.album_total)
5450        self.assertIsNone(metadata.album_total)
5451        self.assertEqual(metadata.get_block(4).comment_strings,
5452                         [u"DISCNUMBER=2"])
5453
5454        metadata = audiotools.FlacMetaData([
5455            audiotools.flac.Flac_VORBISCOMMENT(
5456                [u"DISCNUMBER= foo 2 bar / blah 4 baz"], u"vendor")])
5457        del(metadata.album_total)
5458        self.assertIsNone(metadata.album_total)
5459        self.assertEqual(metadata.get_block(4).comment_strings,
5460                         [u"DISCNUMBER= foo 2 bar"])
5461
5462    @LIB_CUESHEET
5463    @METADATA_FLAC
5464    def test_flac_cuesheet(self):
5465        self.assertTrue(
5466            audiotools.BIN.can_execute(audiotools.BIN["metaflac"]),
5467            "reference binary metaflac(1) required for this test")
5468
5469        from test import EXACT_SILENCE_PCM_Reader
5470        from shutil import copy
5471        from audiotools.cue import read_cuesheet
5472        import subprocess
5473
5474        for (cuesheet_filename,
5475             total_pcm_frames,
5476             sample_rate) in [("metadata_flac_cuesheet-1.cue",
5477                               160107696,
5478                               44100),
5479                              ("metadata_flac_cuesheet-2.cue",
5480                               119882616,
5481                               44100),
5482                              ("metadata_flac_cuesheet-3.cue",
5483                               122513916,
5484                               44100)]:
5485            temp_flac1 = tempfile.NamedTemporaryFile(suffix=".flac")
5486            temp_flac2 = tempfile.NamedTemporaryFile(suffix=".flac")
5487            try:
5488                # build a FLAC full of silence with the total number of frames
5489                flac1 = audiotools.FlacAudio.from_pcm(
5490                    temp_flac1.name,
5491                    EXACT_SILENCE_PCM_Reader(total_pcm_frames),
5492                    total_pcm_frames=total_pcm_frames)
5493
5494                # copy it to another temp file
5495                copy(temp_flac1.name, temp_flac2.name)
5496
5497                # set_cuesheet() to first FLAC file
5498                flac1.set_cuesheet(read_cuesheet(cuesheet_filename))
5499
5500                # import cuesheet to first FLAC file with metaflac
5501                # and get its CUESHEET block
5502                temp_cue = tempfile.NamedTemporaryFile(suffix=".cue")
5503                try:
5504                    with open(cuesheet_filename, "rb") as f:
5505                        temp_cue.write(f.read())
5506                        temp_cue.flush()
5507
5508                    self.assertEqual(
5509                        subprocess.call([audiotools.BIN["metaflac"],
5510                                         "--import-cuesheet-from",
5511                                         temp_cue.name, temp_flac2.name]), 0)
5512                finally:
5513                    temp_cue.close()
5514
5515                flac2 = audiotools.FlacAudio(temp_flac2.name)
5516
5517                # ensure get_cuesheet() data matches
5518                self.assertEqual(flac1.get_cuesheet(),
5519                                 flac2.get_cuesheet())
5520
5521                # ensure CUESHEET blocks match in get_metadata()
5522                self.assertEqual(flac1.get_metadata().get_block(5),
5523                                 flac2.get_metadata().get_block(5))
5524            finally:
5525                temp_flac1.close()
5526                temp_flac2.close()
5527
5528    @METADATA_FLAC
5529    def test_id3(self):
5530        from zlib import decompress
5531
5532        id3v2_tag = decompress(b'x\x9c\xf3t1ff\x00\x02\xd6\xd8\x90' +
5533                               b'\x00WC C\x00\x88=]\x8c\x15BR\x8bK\x14' +
5534                               b'\x1c\x8bJ2\x8bKB<C\x8c\x80\xa2|\xc82' +
5535                               b'\xc1\xf9y\xe9\x0c\xa3`\x14\x0c\r\x00' +
5536                               b'\x00{g\x0c\xcf')
5537        id3v1_tag = decompress(b'x\x9c\x0bqt\xf7t1V\x08I-.Q\x08\xce' +
5538                               b'\xcfKg@\x07pY\xc7\xa2\x92\xcc\xe2\x12' +
5539                               b'\x0cy\xca\xc0\x7f\x00\x1dK\x0b*')
5540
5541        dummy_flac = tempfile.NamedTemporaryFile(suffix=".flac")
5542        dummy_id3flac = tempfile.NamedTemporaryFile(suffix=".flac")
5543        try:
5544            # build test FLAC file with test metadata
5545            flac = audiotools.FlacAudio.from_pcm(
5546                dummy_flac.name,
5547                BLANK_PCM_Reader(2))
5548            metadata = flac.get_metadata()
5549            metadata.track_name = u"Test Name"
5550            metadata.album_name = u"Test Album"
5551            flac.update_metadata(metadata)
5552            self.assertEqual(flac.verify(), True)
5553
5554            # wrap in ID3v2/ID3v1 tags (with different values)
5555            dummy_id3flac.write(id3v2_tag)
5556            with open(dummy_flac.name, "rb") as f:
5557                dummy_id3flac.write(f.read())
5558            dummy_id3flac.write(id3v1_tag)
5559            dummy_id3flac.flush()
5560
5561            # ensure file tests okay
5562            flac2 = audiotools.open(dummy_id3flac.name)
5563            self.assertEqual(flac2.verify(), True)
5564
5565            # ensure start and end of file still match tags
5566            with open(dummy_id3flac.name, "rb") as f:
5567                self.assertEqual(f.read()[0:len(id3v2_tag)], id3v2_tag)
5568            with open(dummy_id3flac.name, "rb") as f:
5569                self.assertEqual(f.read()[-len(id3v1_tag):], id3v1_tag)
5570
5571            # ensure metadata values don't come from ID3v2/ID3v1
5572            metadata = flac2.get_metadata()
5573            self.assertEqual(metadata.track_name, u"Test Name")
5574            self.assertEqual(metadata.album_name, u"Test Album")
5575
5576            # update metadata with new values
5577            # (these are short enough that padding should still be used)
5578            metadata.track_name = u"Test Name2"
5579            metadata.album_name = u"Test Album2"
5580            flac2.update_metadata(metadata)
5581
5582            # ensure start and end of file still match tags
5583            with open(dummy_id3flac.name, "rb") as f:
5584                self.assertEqual(f.read()[0:len(id3v2_tag)], id3v2_tag)
5585            with open(dummy_id3flac.name, "rb") as f:
5586                self.assertEqual(f.read()[-len(id3v1_tag):], id3v1_tag)
5587
5588            # ensure file still tests okay
5589            self.assertEqual(flac2.verify(), True)
5590
5591            # ensure metadata values still don't come from ID3v2/ID3v1
5592            metadata = flac2.get_metadata()
5593            self.assertEqual(metadata.track_name, u"Test Name2")
5594            self.assertEqual(metadata.album_name, u"Test Album2")
5595
5596            # update metadata with large values
5597            # (this should be long enough that padding can't be used)
5598            metadata.comment = u" " * 2 ** 20
5599            flac2.update_metadata(metadata)
5600
5601            # ensure start and end of file still match tags
5602            with open(dummy_id3flac.name, "rb") as f:
5603                self.assertEqual(f.read()[0:len(id3v2_tag)], id3v2_tag)
5604            with open(dummy_id3flac.name, "rb") as f:
5605                self.assertEqual(f.read()[-len(id3v1_tag):], id3v1_tag)
5606
5607            # ensure file still tests okay
5608            self.assertEqual(flac2.verify(), True)
5609
5610            # ensure metadata matches large values
5611            metadata = flac2.get_metadata()
5612            self.assertEqual(metadata.track_name, u"Test Name2")
5613            self.assertEqual(metadata.album_name, u"Test Album2")
5614            self.assertEqual(metadata.comment, u" " * 2 ** 20)
5615        finally:
5616            dummy_flac.close()
5617            dummy_id3flac.close()
5618
5619
5620class M4AMetaDataTest(MetaDataTest):
5621    def setUp(self):
5622        self.metadata_class = audiotools.M4A_META_Atom
5623        self.supported_fields = ["track_name",
5624                                 "track_number",
5625                                 "track_total",
5626                                 "album_name",
5627                                 "artist_name",
5628                                 "composer_name",
5629                                 "copyright",
5630                                 "year",
5631                                 "album_number",
5632                                 "album_total",
5633                                 "comment"]
5634        self.supported_formats = [audiotools.M4AAudio,
5635                                  audiotools.ALACAudio]
5636
5637    def empty_metadata(self):
5638        return self.metadata_class.converted(audiotools.MetaData())
5639
5640    @METADATA_M4A
5641    def test_update(self):
5642        import os
5643
5644        for audio_class in self.supported_formats:
5645            temp_file = tempfile.NamedTemporaryFile(
5646                suffix="." + audio_class.SUFFIX)
5647            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
5648            temp_file_stat = os.stat(temp_file.name)[0]
5649            try:
5650                # update_metadata on file's internal metadata round-trips okay
5651                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
5652                metadata = track.get_metadata()
5653                self.assertEqual(metadata.track_name, u"Foo")
5654                metadata.track_name = u"Bar"
5655                track.update_metadata(metadata)
5656                metadata = track.get_metadata()
5657                self.assertEqual(metadata.track_name, u"Bar")
5658
5659                # update_metadata on unwritable file generates IOError
5660                metadata = track.get_metadata()
5661                os.chmod(temp_file.name, 0)
5662                self.assertRaises(IOError,
5663                                  track.update_metadata,
5664                                  metadata)
5665                os.chmod(temp_file.name, temp_file_stat)
5666
5667                # update_metadata with foreign MetaData generates ValueError
5668                self.assertRaises(ValueError,
5669                                  track.update_metadata,
5670                                  audiotools.MetaData(track_name=u"Foo"))
5671
5672                # update_metadata with None makes no changes
5673                track.update_metadata(None)
5674                metadata = track.get_metadata()
5675                self.assertEqual(metadata.track_name, u"Bar")
5676
5677                # set_metadata can't alter the '\xa9too' field
5678                metadata = track.get_metadata()
5679                old_ilst = metadata.ilst_atom()[b"\xa9too"]
5680                new_ilst = audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
5681                    b'\xa9too',
5682                    [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(
5683                        0, 1, b"Fooz")])
5684                metadata.ilst_atom().replace_child(new_ilst)
5685                self.assertEqual(metadata.ilst_atom()[b"\xa9too"],
5686                                 new_ilst)
5687                track.set_metadata(metadata)
5688                metadata = track.get_metadata()
5689                self.assertEqual(metadata.ilst_atom()[b"\xa9too"], old_ilst)
5690
5691                # update_metadata can alter the '\xa9too' field
5692                metadata = track.get_metadata()
5693                old_ilst = metadata.ilst_atom()[b"\xa9too"]
5694                new_ilst = audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
5695                    b'\xa9too',
5696                    [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(
5697                        0, 1, b"Fooz")])
5698                metadata.ilst_atom().replace_child(new_ilst)
5699                self.assertEqual(metadata.ilst_atom()[b"\xa9too"],
5700                                 new_ilst)
5701                track.update_metadata(metadata)
5702                metadata = track.get_metadata()
5703                self.assertEqual(metadata.ilst_atom()[b"\xa9too"], new_ilst)
5704            finally:
5705                temp_file.close()
5706
5707    @METADATA_M4A
5708    def test_foreign_field(self):
5709        from audiotools.m4a_atoms import M4A_META_Atom
5710        from audiotools.m4a_atoms import M4A_HDLR_Atom
5711        from audiotools.m4a_atoms import M4A_Tree_Atom
5712        from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom
5713        from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom
5714        from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom
5715        from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom
5716        from audiotools.m4a_atoms import M4A_FREE_Atom
5717
5718        metadata = M4A_META_Atom(
5719            0, 0,
5720            [M4A_HDLR_Atom(0, 0, b'\x00\x00\x00\x00',
5721                           b'mdir', b'appl', 0, 0, b'', 0),
5722             M4A_Tree_Atom(
5723                 b'ilst',
5724                 [M4A_ILST_Leaf_Atom(
5725                     b'\xa9nam',
5726                     [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name")]),
5727                  M4A_ILST_Leaf_Atom(
5728                      b'\xa9alb',
5729                      [M4A_ILST_Unicode_Data_Atom(0, 1, b"Album Name")]),
5730                  M4A_ILST_Leaf_Atom(
5731                      b'trkn', [M4A_ILST_TRKN_Data_Atom(1, 3)]),
5732                  M4A_ILST_Leaf_Atom(
5733                      b'disk', [M4A_ILST_DISK_Data_Atom(2, 4)]),
5734                  M4A_ILST_Leaf_Atom(
5735                      b'\xa9foo',
5736                      [M4A_ILST_Unicode_Data_Atom(0, 1, b"Bar")])]),
5737             M4A_FREE_Atom(1024)])
5738
5739        for format in self.supported_formats:
5740            with tempfile.NamedTemporaryFile(
5741                suffix="." + format.SUFFIX) as temp_file:
5742                track = format.from_pcm(temp_file.name,
5743                                        BLANK_PCM_Reader(1))
5744                track.set_metadata(metadata)
5745                metadata2 = track.get_metadata()
5746                self.assertEqual(metadata, metadata2)
5747                self.assertEqual(metadata.__class__, metadata2.__class__)
5748                self.assertEqual(
5749                    track.get_metadata().ilst_atom()[b"\xa9foo"].data,
5750                    b"\x00\x00\x00\x13data\x00\x00\x00\x01\x00\x00\x00\x00Bar")
5751
5752    @METADATA_M4A
5753    def test_field_mapping(self):
5754        mapping = [('track_name', b'\xA9nam', u'a'),
5755                   ('artist_name', b'\xA9ART', u'b'),
5756                   ('year', b'\xA9day', u'c'),
5757                   ('album_name', b'\xA9alb', u'd'),
5758                   ('composer_name', b'\xA9wrt', u'e'),
5759                   ('comment', b'\xA9cmt', u'f'),
5760                   ('copyright', b'cprt', u'g')]
5761
5762        for format in self.supported_formats:
5763            with tempfile.NamedTemporaryFile(
5764                suffix="." + format.SUFFIX) as temp_file:
5765                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
5766
5767                # ensure that setting a class field
5768                # updates its corresponding low-level implementation
5769                for (field, key, value) in mapping:
5770                    track.delete_metadata()
5771                    metadata = self.empty_metadata()
5772                    setattr(metadata, field, value)
5773                    self.assertEqual(getattr(metadata, field), value)
5774                    self.assertEqual(
5775                        metadata[b'ilst'][key][b'data'].data.decode('utf-8'),
5776                        value)
5777                    track.set_metadata(metadata)
5778                    metadata2 = track.get_metadata()
5779                    self.assertEqual(getattr(metadata2, field), value)
5780                    self.assertEqual(
5781                        metadata2[b'ilst'][key][b'data'].data.decode('utf-8'),
5782                        value)
5783
5784                # ensure that updating the low-level implementation
5785                # is reflected in the class field
5786                for (field, key, value) in mapping:
5787                    track.delete_metadata()
5788                    metadata = self.empty_metadata()
5789                    metadata[b'ilst'].add_child(
5790                        audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
5791                            key,
5792                            [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(
5793                                0, 1, value.encode('utf-8'))]))
5794                    self.assertEqual(getattr(metadata, field), value)
5795                    self.assertEqual(
5796                        metadata[b'ilst'][key][b'data'].data.decode('utf-8'),
5797                        value)
5798                    track.set_metadata(metadata)
5799                    metadata2 = track.get_metadata()
5800                    self.assertEqual(getattr(metadata, field), value)
5801                    self.assertEqual(
5802                        metadata[b'ilst'][key][b'data'].data.decode('utf-8'),
5803                        value)
5804
5805                # ensure that setting numerical fields also
5806                # updates the low-level implementation
5807                track.delete_metadata()
5808                metadata = self.empty_metadata()
5809                metadata.track_number = 1
5810                track.set_metadata(metadata)
5811                metadata = track.get_metadata()
5812                self.assertEqual(
5813                    metadata[b'ilst'][b'trkn'][b'data'].track_number,
5814                    1)
5815                self.assertEqual(
5816                    metadata[b'ilst'][b'trkn'][b'data'].track_total,
5817                    0)
5818                metadata.track_total = 2
5819                track.set_metadata(metadata)
5820                metadata = track.get_metadata()
5821                self.assertEqual(
5822                    metadata[b'ilst'][b'trkn'][b'data'].track_number,
5823                    1)
5824                self.assertEqual(
5825                    metadata[b'ilst'][b'trkn'][b'data'].track_total,
5826                    2)
5827                del(metadata.track_number)
5828                track.set_metadata(metadata)
5829                metadata = track.get_metadata()
5830                self.assertEqual(
5831                    metadata[b'ilst'][b'trkn'][b'data'].track_number,
5832                    0)
5833                self.assertEqual(
5834                    metadata[b'ilst'][b'trkn'][b'data'].track_total,
5835                    2)
5836                del(metadata.track_total)
5837                track.set_metadata(metadata)
5838                metadata = track.get_metadata()
5839                self.assertRaises(KeyError,
5840                                  metadata[b'ilst'].__getitem__,
5841                                  b'trkn')
5842
5843                track.delete_metadata()
5844                metadata = self.empty_metadata()
5845                metadata.album_number = 3
5846                track.set_metadata(metadata)
5847                metadata = track.get_metadata()
5848                self.assertEqual(
5849                    metadata[b'ilst'][b'disk'][b'data'].disk_number,
5850                    3)
5851                self.assertEqual(
5852                    metadata[b'ilst'][b'disk'][b'data'].disk_total,
5853                    0)
5854
5855                metadata.album_total = 4
5856                track.set_metadata(metadata)
5857                metadata = track.get_metadata()
5858                self.assertEqual(
5859                    metadata[b'ilst'][b'disk'][b'data'].disk_number,
5860                    3)
5861                self.assertEqual(
5862                    metadata[b'ilst'][b'disk'][b'data'].disk_total,
5863                    4)
5864                del(metadata.album_number)
5865                track.set_metadata(metadata)
5866                metadata = track.get_metadata()
5867                self.assertEqual(
5868                    metadata[b'ilst'][b'disk'][b'data'].disk_number,
5869                    0)
5870                self.assertEqual(
5871                    metadata[b'ilst'][b'disk'][b'data'].disk_total,
5872                    4)
5873                del(metadata.album_total)
5874                track.set_metadata(metadata)
5875                metadata = track.get_metadata()
5876                self.assertRaises(KeyError,
5877                                  metadata[b'ilst'].__getitem__,
5878                                  b'disk')
5879
5880    @METADATA_M4A
5881    def test_getattr(self):
5882        from audiotools.m4a_atoms import M4A_META_Atom
5883        from audiotools.m4a_atoms import M4A_Tree_Atom
5884        from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom
5885        from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom
5886        from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom
5887        from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom
5888
5889        # no ilst atom is okay
5890        for attr in audiotools.MetaData.FIELDS:
5891            metadata = M4A_META_Atom(0, 0, [])
5892            self.assertIsNone(getattr(metadata, attr))
5893
5894        # empty ilst atom is okay
5895        for attr in audiotools.MetaData.FIELDS:
5896            metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
5897            self.assertIsNone(getattr(metadata, attr))
5898
5899        # fields grab the first available atom from ilst atom, if any
5900        metadata = M4A_META_Atom(
5901            0, 0,
5902            [M4A_Tree_Atom(b'ilst',
5903                           [M4A_ILST_Leaf_Atom(b'\xa9nam', [])])])
5904        self.assertIsNone(metadata.track_name)
5905
5906        metadata = M4A_META_Atom(
5907            0, 0,
5908            [M4A_Tree_Atom(b'ilst', [
5909                M4A_ILST_Leaf_Atom(
5910                    b'\xa9nam',
5911                    [M4A_ILST_Unicode_Data_Atom(0, 1,
5912                                                b"Track Name")])])])
5913        self.assertEqual(metadata.track_name, u"Track Name")
5914
5915        metadata = M4A_META_Atom(
5916            0, 0,
5917            [M4A_Tree_Atom(b'ilst', [
5918                M4A_ILST_Leaf_Atom(
5919                    b'\xa9nam',
5920                    [M4A_ILST_Unicode_Data_Atom(0, 1,
5921                                                b"Track Name")]),
5922                M4A_ILST_Leaf_Atom(
5923                    b'\xa9nam',
5924                    [M4A_ILST_Unicode_Data_Atom(0, 1,
5925                                                b"Another Name")])])])
5926        self.assertEqual(metadata.track_name, u"Track Name")
5927
5928        metadata = M4A_META_Atom(
5929            0, 0,
5930            [M4A_Tree_Atom(b'ilst', [
5931                M4A_ILST_Leaf_Atom(
5932                    b'\xa9nam',
5933                    [M4A_ILST_Unicode_Data_Atom(0, 1,
5934                                                b"Track Name"),
5935                     M4A_ILST_Unicode_Data_Atom(0, 1,
5936                                                b"Another Name")])])])
5937        self.assertEqual(metadata.track_name, u"Track Name")
5938
5939        # ensure track_number/_total/album_number/_total fields work
5940        metadata = M4A_META_Atom(
5941            0, 0,
5942            [M4A_Tree_Atom(b'ilst',
5943                           [M4A_ILST_Leaf_Atom(
5944                               b'trkn',
5945                               [M4A_ILST_TRKN_Data_Atom(1, 2)]),
5946                            M4A_ILST_Leaf_Atom(
5947                                b'disk',
5948                                [M4A_ILST_DISK_Data_Atom(3, 4)])])])
5949        self.assertEqual(metadata.track_number, 1)
5950        self.assertEqual(metadata.track_total, 2)
5951        self.assertEqual(metadata.album_number, 3)
5952        self.assertEqual(metadata.album_total, 4)
5953
5954    @METADATA_M4A
5955    def test_setattr(self):
5956        from audiotools.m4a_atoms import M4A_META_Atom
5957        from audiotools.m4a_atoms import M4A_Tree_Atom
5958        from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom
5959        from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom
5960        from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom
5961        from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom
5962
5963        # fields add a new ilst atom, if necessary
5964        metadata = M4A_META_Atom(0, 0, [])
5965        metadata.track_name = u"Track Name"
5966        self.assertEqual(metadata.track_name, u"Track Name")
5967        self.assertEqual(
5968            metadata,
5969            M4A_META_Atom(
5970                0, 0,
5971                [M4A_Tree_Atom(b'ilst', [
5972                    M4A_ILST_Leaf_Atom(
5973                        b'\xa9nam',
5974                        [M4A_ILST_Unicode_Data_Atom(0, 1,
5975                                                    b"Track Name")])])]))
5976
5977        # fields add a new entry to ilst atom, if necessary
5978        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
5979        metadata.track_name = u"Track Name"
5980        self.assertEqual(metadata.track_name, u"Track Name")
5981        self.assertEqual(
5982            metadata,
5983            M4A_META_Atom(
5984                0, 0,
5985                [M4A_Tree_Atom(b'ilst', [
5986                    M4A_ILST_Leaf_Atom(
5987                        b'\xa9nam',
5988                        [M4A_ILST_Unicode_Data_Atom(0, 1,
5989                                                    b"Track Name")])])]))
5990
5991        # fields overwrite first child of ilst atom and leave rest alone
5992        metadata = M4A_META_Atom(
5993            0, 0,
5994            [M4A_Tree_Atom(b'ilst', [
5995                M4A_ILST_Leaf_Atom(
5996                    b'\xa9nam',
5997                    [M4A_ILST_Unicode_Data_Atom(0, 1,
5998                                                b"Old Track Name")])])])
5999        metadata.track_name = u"Track Name"
6000        self.assertEqual(metadata.track_name, u"Track Name")
6001        self.assertEqual(
6002            metadata,
6003            M4A_META_Atom(
6004                0, 0,
6005                [M4A_Tree_Atom(b'ilst', [
6006                    M4A_ILST_Leaf_Atom(
6007                        b'\xa9nam',
6008                        [M4A_ILST_Unicode_Data_Atom(0, 1,
6009                                                    b"Track Name")])])]))
6010
6011        metadata = M4A_META_Atom(
6012            0, 0,
6013            [M4A_Tree_Atom(b'ilst', [
6014                M4A_ILST_Leaf_Atom(
6015                    b'\xa9nam',
6016                    [M4A_ILST_Unicode_Data_Atom(0, 1,
6017                                                b"Old Track Name")]),
6018                M4A_ILST_Leaf_Atom(
6019                    b'\xa9nam',
6020                    [M4A_ILST_Unicode_Data_Atom(0, 1,
6021                                                b"Old Track Name 2")])])])
6022        metadata.track_name = u"Track Name"
6023        self.assertEqual(metadata.track_name, u"Track Name")
6024        self.assertEqual(
6025            metadata,
6026            M4A_META_Atom(
6027                0, 0,
6028                [M4A_Tree_Atom(b'ilst', [
6029                    M4A_ILST_Leaf_Atom(
6030                        b'\xa9nam',
6031                        [M4A_ILST_Unicode_Data_Atom(0, 1,
6032                                                    b"Track Name")]),
6033                    M4A_ILST_Leaf_Atom(
6034                        b'\xa9nam',
6035                        [M4A_ILST_Unicode_Data_Atom(0, 1,
6036                                                    b"Old Track Name 2")])])]))
6037
6038        metadata = M4A_META_Atom(
6039            0, 0,
6040            [M4A_Tree_Atom(b'ilst', [
6041                M4A_ILST_Leaf_Atom(
6042                    b'\xa9nam',
6043                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Old Track Name"),
6044                     M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name 2")])])])
6045        metadata.track_name = u"Track Name"
6046        self.assertEqual(metadata.track_name, u"Track Name")
6047        self.assertEqual(
6048            metadata,
6049            M4A_META_Atom(
6050                0, 0,
6051                [M4A_Tree_Atom(b'ilst', [
6052                    M4A_ILST_Leaf_Atom(
6053                        b'\xa9nam',
6054                        [M4A_ILST_Unicode_Data_Atom(
6055                            0, 1, b"Track Name"),
6056                         M4A_ILST_Unicode_Data_Atom(
6057                             0, 1, b"Track Name 2")])])]))
6058
6059        # setting track_number/_total/album_number/_total
6060        # adds a new field if necessary
6061        metadata = M4A_META_Atom(0, 0, [])
6062        metadata.track_number = 1
6063        self.assertEqual(metadata.track_number, 1)
6064        self.assertEqual(
6065            metadata,
6066            M4A_META_Atom(
6067                0, 0,
6068                [M4A_Tree_Atom(b'ilst',
6069                               [M4A_ILST_Leaf_Atom(
6070                                   b'trkn',
6071                                   [M4A_ILST_TRKN_Data_Atom(1, 0)])])]))
6072
6073        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
6074        metadata.track_number = 1
6075        self.assertEqual(metadata.track_number, 1)
6076        self.assertEqual(
6077            metadata,
6078            M4A_META_Atom(
6079                0, 0,
6080                [M4A_Tree_Atom(b'ilst',
6081                               [M4A_ILST_Leaf_Atom(
6082                                   b'trkn',
6083                                   [M4A_ILST_TRKN_Data_Atom(1, 0)])])]))
6084
6085        metadata = M4A_META_Atom(0, 0, [])
6086        metadata.track_total = 2
6087        self.assertEqual(metadata.track_total, 2)
6088        self.assertEqual(
6089            metadata,
6090            M4A_META_Atom(
6091                0, 0,
6092                [M4A_Tree_Atom(b'ilst',
6093                               [M4A_ILST_Leaf_Atom(
6094                                   b'trkn',
6095                                   [M4A_ILST_TRKN_Data_Atom(0, 2)])])]))
6096
6097        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
6098        metadata.track_total = 2
6099        self.assertEqual(metadata.track_total, 2)
6100        self.assertEqual(
6101            metadata,
6102            M4A_META_Atom(
6103                0, 0,
6104                [M4A_Tree_Atom(b'ilst',
6105                               [M4A_ILST_Leaf_Atom(
6106                                   b'trkn',
6107                                   [M4A_ILST_TRKN_Data_Atom(0, 2)])])]))
6108
6109        metadata = M4A_META_Atom(0, 0, [])
6110        metadata.album_number = 3
6111        self.assertEqual(metadata.album_number, 3)
6112        self.assertEqual(
6113            metadata,
6114            M4A_META_Atom(
6115                0, 0,
6116                [M4A_Tree_Atom(b'ilst',
6117                               [M4A_ILST_Leaf_Atom(
6118                                   b'disk',
6119                                   [M4A_ILST_DISK_Data_Atom(3, 0)])])]))
6120
6121        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
6122        metadata.album_number = 3
6123        self.assertEqual(metadata.album_number, 3)
6124        self.assertEqual(
6125            metadata,
6126            M4A_META_Atom(
6127                0, 0,
6128                [M4A_Tree_Atom(b'ilst',
6129                               [M4A_ILST_Leaf_Atom(
6130                                   b'disk',
6131                                   [M4A_ILST_DISK_Data_Atom(3, 0)])])]))
6132
6133        metadata = M4A_META_Atom(0, 0, [])
6134        metadata.album_total = 4
6135        self.assertEqual(metadata.album_total, 4)
6136        self.assertEqual(
6137            metadata,
6138            M4A_META_Atom(
6139                0, 0,
6140                [M4A_Tree_Atom(b'ilst',
6141                               [M4A_ILST_Leaf_Atom(
6142                                   b'disk',
6143                                   [M4A_ILST_TRKN_Data_Atom(0, 4)])])]))
6144
6145        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
6146        metadata.album_total = 4
6147        self.assertEqual(metadata.album_total, 4)
6148        self.assertEqual(
6149            metadata,
6150            M4A_META_Atom(
6151                0, 0,
6152                [M4A_Tree_Atom(b'ilst',
6153                               [M4A_ILST_Leaf_Atom(
6154                                   b'disk',
6155                                   [M4A_ILST_TRKN_Data_Atom(0, 4)])])]))
6156
6157        # setting track_number/_total/album_number/_total
6158        # overwrites existing field if necessary
6159        metadata = M4A_META_Atom(
6160            0, 0,
6161            [M4A_Tree_Atom(b'ilst',
6162                           [M4A_ILST_Leaf_Atom(
6163                               b'trkn',
6164                               [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6165                            M4A_ILST_Leaf_Atom(
6166                                b'disk',
6167                                [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6168        metadata.track_number = 6
6169        self.assertEqual(metadata.track_number, 6)
6170        self.assertEqual(
6171            metadata,
6172            M4A_META_Atom(
6173                0, 0,
6174                [M4A_Tree_Atom(b'ilst',
6175                               [M4A_ILST_Leaf_Atom(
6176                                   b'trkn',
6177                                   [M4A_ILST_TRKN_Data_Atom(6, 2)]),
6178                                M4A_ILST_Leaf_Atom(
6179                                    b'disk',
6180                                    [M4A_ILST_DISK_Data_Atom(3, 4)])])]))
6181
6182        metadata = M4A_META_Atom(
6183            0, 0,
6184            [M4A_Tree_Atom(b'ilst',
6185                           [M4A_ILST_Leaf_Atom(
6186                               b'trkn',
6187                               [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6188                            M4A_ILST_Leaf_Atom(
6189                                b'disk',
6190                                [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6191        metadata.track_total = 7
6192        self.assertEqual(metadata.track_total, 7)
6193        self.assertEqual(
6194            metadata,
6195            M4A_META_Atom(
6196                0, 0,
6197                [M4A_Tree_Atom(b'ilst',
6198                               [M4A_ILST_Leaf_Atom(
6199                                   b'trkn',
6200                                   [M4A_ILST_TRKN_Data_Atom(1, 7)]),
6201                                M4A_ILST_Leaf_Atom(
6202                                    b'disk',
6203                                    [M4A_ILST_DISK_Data_Atom(3, 4)])])]))
6204
6205        metadata = M4A_META_Atom(
6206            0, 0,
6207            [M4A_Tree_Atom(b'ilst',
6208                           [M4A_ILST_Leaf_Atom(
6209                               b'trkn',
6210                               [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6211                            M4A_ILST_Leaf_Atom(
6212                                b'disk',
6213                                [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6214        metadata.album_number = 8
6215        self.assertEqual(metadata.album_number, 8)
6216        self.assertEqual(
6217            metadata,
6218            M4A_META_Atom(
6219                0, 0,
6220                [M4A_Tree_Atom(b'ilst',
6221                               [M4A_ILST_Leaf_Atom(
6222                                   b'trkn',
6223                                   [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6224                                M4A_ILST_Leaf_Atom(
6225                                    b'disk',
6226                                    [M4A_ILST_DISK_Data_Atom(8, 4)])])]))
6227
6228        metadata = M4A_META_Atom(
6229            0, 0,
6230            [M4A_Tree_Atom(b'ilst',
6231                           [M4A_ILST_Leaf_Atom(
6232                               b'trkn',
6233                               [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6234                            M4A_ILST_Leaf_Atom(
6235                                b'disk',
6236                                [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6237        metadata.album_total = 9
6238        self.assertEqual(metadata.album_total, 9)
6239        self.assertEqual(
6240            metadata,
6241            M4A_META_Atom(
6242                0, 0,
6243                [M4A_Tree_Atom(b'ilst',
6244                               [M4A_ILST_Leaf_Atom(
6245                                   b'trkn',
6246                                   [M4A_ILST_TRKN_Data_Atom(1, 2)]),
6247                                M4A_ILST_Leaf_Atom(
6248                                    b'disk',
6249                                    [M4A_ILST_DISK_Data_Atom(3, 9)])])]))
6250
6251    @METADATA_M4A
6252    def test_delattr(self):
6253        from audiotools.m4a_atoms import M4A_META_Atom
6254        from audiotools.m4a_atoms import M4A_Tree_Atom
6255        from audiotools.m4a_atoms import M4A_ILST_Leaf_Atom
6256        from audiotools.m4a_atoms import M4A_ILST_Unicode_Data_Atom
6257        from audiotools.m4a_atoms import M4A_ILST_TRKN_Data_Atom
6258        from audiotools.m4a_atoms import M4A_ILST_DISK_Data_Atom
6259
6260        # fields remove all matching children from ilst atom
6261        # - no ilst atom
6262        metadata = M4A_META_Atom(0, 0, [])
6263        del(metadata.track_name)
6264        self.assertIsNone(metadata.track_name)
6265        self.assertEqual(metadata, M4A_META_Atom(0, 0, []))
6266
6267        # - empty ilst atom
6268        metadata = M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])])
6269        del(metadata.track_name)
6270        self.assertIsNone(metadata.track_name)
6271        self.assertEqual(metadata,
6272                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6273
6274        # - 1 matching item in ilst atom
6275        metadata = M4A_META_Atom(
6276            0, 0,
6277            [M4A_Tree_Atom(b'ilst', [
6278                M4A_ILST_Leaf_Atom(
6279                    b'\xa9nam',
6280                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name")])])])
6281        del(metadata.track_name)
6282        self.assertIsNone(metadata.track_name)
6283        self.assertEqual(metadata,
6284                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6285
6286        # - 2 maching items in ilst atom
6287        metadata = M4A_META_Atom(
6288            0, 0,
6289            [M4A_Tree_Atom(b'ilst', [
6290                M4A_ILST_Leaf_Atom(
6291                    b'\xa9nam',
6292                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name")]),
6293                M4A_ILST_Leaf_Atom(
6294                    b'\xa9nam',
6295                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name 2")])])])
6296        del(metadata.track_name)
6297        self.assertIsNone(metadata.track_name)
6298        self.assertEqual(metadata,
6299                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6300
6301        # - 2 matching data atoms in ilst child
6302        metadata = M4A_META_Atom(
6303            0, 0,
6304            [M4A_Tree_Atom(b'ilst', [
6305                M4A_ILST_Leaf_Atom(
6306                    b'\xa9nam',
6307                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name"),
6308                     M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name 2")])])])
6309        del(metadata.track_name)
6310        self.assertIsNone(metadata.track_name)
6311        self.assertEqual(metadata,
6312                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6313
6314        # setting item to None is the same as deleting it
6315        metadata = M4A_META_Atom(
6316            0, 0,
6317            [M4A_Tree_Atom(b'ilst', [
6318                M4A_ILST_Leaf_Atom(
6319                    b'\xa9nam',
6320                    [M4A_ILST_Unicode_Data_Atom(0, 1, b"Track Name")])])])
6321        metadata.track_name = None
6322        self.assertIsNone(metadata.track_name)
6323        self.assertEqual(metadata,
6324                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6325
6326        # removing track number removes atom if track total is 0
6327        metadata = M4A_META_Atom(
6328            0, 0,
6329            [M4A_Tree_Atom(b'ilst', [
6330                M4A_ILST_Leaf_Atom(
6331                    b'trkn',
6332                    [M4A_ILST_TRKN_Data_Atom(1, 0)])])])
6333        self.assertEqual(metadata.track_number, 1)
6334        self.assertIsNone(metadata.track_total)
6335        del(metadata.track_number)
6336        self.assertIsNone(metadata.track_number)
6337        self.assertIsNone(metadata.track_total)
6338        self.assertEqual(metadata,
6339                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6340
6341        # removing track number sets value to None if track total is > 0
6342        metadata = M4A_META_Atom(
6343            0, 0,
6344            [M4A_Tree_Atom(b'ilst', [
6345                M4A_ILST_Leaf_Atom(
6346                    b'trkn',
6347                    [M4A_ILST_TRKN_Data_Atom(1, 2)])])])
6348        self.assertEqual(metadata.track_number, 1)
6349        self.assertEqual(metadata.track_total, 2)
6350        del(metadata.track_number)
6351        self.assertIsNone(metadata.track_number)
6352        self.assertEqual(metadata.track_total, 2)
6353        self.assertEqual(
6354            metadata,
6355            M4A_META_Atom(
6356                0, 0,
6357                [M4A_Tree_Atom(b'ilst', [
6358                    M4A_ILST_Leaf_Atom(
6359                        b'trkn',
6360                        [M4A_ILST_TRKN_Data_Atom(0, 2)])])]))
6361
6362        # removing track total removes atom if track number is 0
6363        metadata = M4A_META_Atom(
6364            0, 0,
6365            [M4A_Tree_Atom(b'ilst', [
6366                M4A_ILST_Leaf_Atom(
6367                    b'trkn',
6368                    [M4A_ILST_TRKN_Data_Atom(0, 2)])])])
6369        self.assertIsNone(metadata.track_number)
6370        self.assertEqual(metadata.track_total, 2)
6371        del(metadata.track_total)
6372        self.assertIsNone(metadata.track_number)
6373        self.assertIsNone(metadata.track_total)
6374        self.assertEqual(metadata,
6375                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6376
6377        # removing track total sets value to None if track number is > 0
6378        metadata = M4A_META_Atom(
6379            0, 0,
6380            [M4A_Tree_Atom(b'ilst', [
6381                M4A_ILST_Leaf_Atom(
6382                    b'trkn',
6383                    [M4A_ILST_TRKN_Data_Atom(1, 2)])])])
6384        self.assertEqual(metadata.track_number, 1)
6385        self.assertEqual(metadata.track_total, 2)
6386        del(metadata.track_total)
6387        self.assertEqual(metadata.track_number, 1)
6388        self.assertIsNone(metadata.track_total)
6389        self.assertEqual(
6390            metadata,
6391            M4A_META_Atom(
6392                0, 0,
6393                [M4A_Tree_Atom(b'ilst', [
6394                    M4A_ILST_Leaf_Atom(
6395                        b'trkn',
6396                        [M4A_ILST_TRKN_Data_Atom(1, 0)])])]))
6397
6398        # removing album number removes atom if album total is 0
6399        metadata = M4A_META_Atom(
6400            0, 0,
6401            [M4A_Tree_Atom(b'ilst', [
6402                M4A_ILST_Leaf_Atom(
6403                    b'disk',
6404                    [M4A_ILST_DISK_Data_Atom(3, 0)])])])
6405        self.assertEqual(metadata.album_number, 3)
6406        self.assertIsNone(metadata.album_total)
6407        del(metadata.album_number)
6408        self.assertIsNone(metadata.album_number)
6409        self.assertIsNone(metadata.album_total)
6410        self.assertEqual(metadata,
6411                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6412
6413        # removing album number sets value to None if album total is > 0
6414        metadata = M4A_META_Atom(
6415            0, 0,
6416            [M4A_Tree_Atom(b'ilst', [
6417                M4A_ILST_Leaf_Atom(
6418                    b'disk',
6419                    [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6420        self.assertEqual(metadata.album_number, 3)
6421        self.assertEqual(metadata.album_total, 4)
6422        del(metadata.album_number)
6423        self.assertIsNone(metadata.album_number)
6424        self.assertEqual(metadata.album_total, 4)
6425        self.assertEqual(
6426            metadata,
6427            M4A_META_Atom(
6428                0, 0,
6429                [M4A_Tree_Atom(b'ilst', [
6430                    M4A_ILST_Leaf_Atom(
6431                        b'disk',
6432                        [M4A_ILST_DISK_Data_Atom(0, 4)])])]))
6433
6434        # removing album total removes atom if album number if 0
6435        metadata = M4A_META_Atom(
6436            0, 0,
6437            [M4A_Tree_Atom(b'ilst', [
6438                M4A_ILST_Leaf_Atom(
6439                    b'disk',
6440                    [M4A_ILST_DISK_Data_Atom(0, 4)])])])
6441        self.assertIsNone(metadata.album_number)
6442        self.assertEqual(metadata.album_total, 4)
6443        del(metadata.album_total)
6444        self.assertIsNone(metadata.album_number)
6445        self.assertIsNone(metadata.album_total)
6446        self.assertEqual(metadata,
6447                         M4A_META_Atom(0, 0, [M4A_Tree_Atom(b'ilst', [])]))
6448
6449        # removing album total sets value to None if album number is > 0
6450        metadata = M4A_META_Atom(
6451            0, 0,
6452            [M4A_Tree_Atom(b'ilst', [
6453                M4A_ILST_Leaf_Atom(
6454                    b'disk',
6455                    [M4A_ILST_DISK_Data_Atom(3, 4)])])])
6456        self.assertEqual(metadata.album_number, 3)
6457        self.assertEqual(metadata.album_total, 4)
6458        del(metadata.album_total)
6459        self.assertEqual(metadata.album_number, 3)
6460        self.assertIsNone(metadata.album_total)
6461        self.assertEqual(
6462            metadata,
6463            M4A_META_Atom(
6464                0, 0,
6465                [M4A_Tree_Atom(b'ilst', [
6466                    M4A_ILST_Leaf_Atom(
6467                        b'disk',
6468                        [M4A_ILST_DISK_Data_Atom(3, 0)])])]))
6469
6470    @METADATA_M4A
6471    def test_images(self):
6472        for audio_class in self.supported_formats:
6473            with tempfile.NamedTemporaryFile(
6474                suffix="." + audio_class.SUFFIX) as temp_file:
6475                track = audio_class.from_pcm(temp_file.name,
6476                                             BLANK_PCM_Reader(1))
6477
6478                metadata = self.empty_metadata()
6479                self.assertEqual(metadata.images(), [])
6480
6481                image1 = audiotools.Image.new(TEST_COVER1, u"", 0)
6482
6483                track.set_metadata(metadata)
6484                metadata = track.get_metadata()
6485
6486                # ensure that adding one image works
6487                metadata.add_image(image1)
6488                track.set_metadata(metadata)
6489                metadata = track.get_metadata()
6490                self.assertEqual(metadata.images(), [image1])
6491
6492                # ensure that deleting the first image works
6493                metadata.delete_image(image1)
6494                track.set_metadata(metadata)
6495                metadata = track.get_metadata()
6496                self.assertEqual(metadata.images(), [])
6497
6498    @METADATA_M4A
6499    def test_converted(self):
6500        # build a generic MetaData with everything
6501        image1 = audiotools.Image.new(TEST_COVER1, u"", 0)
6502
6503        metadata_orig = audiotools.MetaData(track_name=u"a",
6504                                            track_number=1,
6505                                            track_total=2,
6506                                            album_name=u"b",
6507                                            artist_name=u"c",
6508                                            performer_name=u"d",
6509                                            composer_name=u"e",
6510                                            conductor_name=u"f",
6511                                            media=u"g",
6512                                            ISRC=u"h",
6513                                            catalog=u"i",
6514                                            copyright=u"j",
6515                                            publisher=u"k",
6516                                            year=u"l",
6517                                            date=u"m",
6518                                            album_number=3,
6519                                            album_total=4,
6520                                            comment="n",
6521                                            images=[image1])
6522
6523        # ensure converted() builds something with our class
6524        metadata_new = self.metadata_class.converted(metadata_orig)
6525        self.assertEqual(metadata_new.__class__, self.metadata_class)
6526
6527        # ensure our fields match
6528        for field in audiotools.MetaData.FIELDS:
6529            if field in self.supported_fields:
6530                self.assertEqual(getattr(metadata_orig, field),
6531                                 getattr(metadata_new, field))
6532            else:
6533                self.assertIsNone(getattr(metadata_new, field))
6534
6535        # ensure images match, if supported
6536        if self.metadata_class.supports_images():
6537            self.assertEqual(metadata_new.images(), [image1])
6538
6539        # check non-MetaData fields
6540        metadata_orig = self.empty_metadata()
6541        metadata_orig[b'ilst'].add_child(
6542            audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
6543                b'test',
6544                [audiotools.m4a_atoms.M4A_Leaf_Atom(b"data", b"foobar")]))
6545        self.assertEqual(
6546            metadata_orig[b'ilst'][b'test'][b'data'].data, b"foobar")
6547        metadata_new = self.metadata_class.converted(metadata_orig)
6548        self.assertEqual(
6549            metadata_orig[b'ilst'][b'test'][b'data'].data, b"foobar")
6550
6551        # ensure that convert() builds a whole new object
6552        metadata_new.track_name = u"Foo"
6553        self.assertEqual(metadata_new.track_name, u"Foo")
6554        metadata_new2 = self.metadata_class.converted(metadata_new)
6555        self.assertEqual(metadata_new2.track_name, u"Foo")
6556        metadata_new2.track_name = u"Bar"
6557        self.assertEqual(metadata_new2.track_name, u"Bar")
6558        self.assertEqual(metadata_new.track_name, u"Foo")
6559
6560    @METADATA_M4A
6561    def test_clean(self):
6562        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
6563                                     CLEAN_REMOVE_LEADING_WHITESPACE,
6564                                     CLEAN_REMOVE_EMPTY_TAG)
6565
6566        # check trailing whitespace
6567        metadata = audiotools.m4a_atoms.M4A_META_Atom(
6568            0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom(b'ilst', [])])
6569        metadata[b'ilst'].add_child(
6570            audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
6571                b"\xa9nam",
6572                [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0,
6573                                                                 1,
6574                                                                 b"Foo ")]))
6575        self.assertEqual(metadata[b'ilst'][b"\xa9nam"][b'data'].data, b"Foo ")
6576        self.assertEqual(metadata.track_name, u'Foo ')
6577        (cleaned, fixes) = metadata.clean()
6578        self.assertEqual(fixes,
6579                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
6580                          {"field": "nam"}])
6581        self.assertEqual(cleaned[b'ilst'][b'\xa9nam'][b'data'].data, b"Foo")
6582        self.assertEqual(cleaned.track_name, u'Foo')
6583
6584        # check leading whitespace
6585        metadata = audiotools.m4a_atoms.M4A_META_Atom(
6586            0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom(b'ilst', [])])
6587        metadata[b'ilst'].add_child(
6588            audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
6589                b"\xa9nam",
6590                [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0,
6591                                                                 1,
6592                                                                 b" Foo")]))
6593        self.assertEqual(metadata[b'ilst'][b"\xa9nam"][b'data'].data, b" Foo")
6594        self.assertEqual(metadata.track_name, u' Foo')
6595        (cleaned, fixes) = metadata.clean()
6596        self.assertEqual(fixes,
6597                         [CLEAN_REMOVE_LEADING_WHITESPACE %
6598                          {"field": "nam"}])
6599        self.assertEqual(cleaned[b'ilst'][b'\xa9nam'][b'data'].data, b"Foo")
6600        self.assertEqual(cleaned.track_name, u'Foo')
6601
6602        # check empty fields
6603        metadata = audiotools.m4a_atoms.M4A_META_Atom(
6604            0, 0, [audiotools.m4a_atoms.M4A_Tree_Atom(b'ilst', [])])
6605        metadata[b'ilst'].add_child(
6606            audiotools.m4a_atoms.M4A_ILST_Leaf_Atom(
6607                b"\xa9nam",
6608                [audiotools.m4a_atoms.M4A_ILST_Unicode_Data_Atom(0, 1, b"")]))
6609        self.assertEqual(metadata[b'ilst'][b"\xa9nam"][b'data'].data, b"")
6610        self.assertEqual(metadata.track_name, u'')
6611        (cleaned, fixes) = metadata.clean()
6612        self.assertEqual(fixes,
6613                         [CLEAN_REMOVE_EMPTY_TAG %
6614                          {"field": "nam"}])
6615        self.assertRaises(KeyError,
6616                          cleaned[b'ilst'].__getitem__,
6617                          b'\xa9nam')
6618        self.assertIsNone(cleaned.track_name)
6619
6620        # numerical fields can't have whitespace
6621        # and images aren't stored with metadata
6622        # so there's no need to check those
6623
6624
6625class VorbisCommentTest(MetaDataTest):
6626    def setUp(self):
6627        self.metadata_class = audiotools.VorbisComment
6628        self.supported_fields = ["track_name",
6629                                 "track_number",
6630                                 "track_total",
6631                                 "album_name",
6632                                 "artist_name",
6633                                 "performer_name",
6634                                 "composer_name",
6635                                 "conductor_name",
6636                                 "media",
6637                                 "ISRC",
6638                                 "catalog",
6639                                 "copyright",
6640                                 "publisher",
6641                                 "year",
6642                                 "album_number",
6643                                 "album_total",
6644                                 "comment"]
6645        self.supported_formats = [audiotools.VorbisAudio]
6646
6647    def empty_metadata(self):
6648        return self.metadata_class.converted(audiotools.MetaData())
6649
6650    @METADATA_VORBIS
6651    def test_update(self):
6652        import os
6653
6654        for audio_class in self.supported_formats:
6655            temp_file = tempfile.NamedTemporaryFile(
6656                suffix="." + audio_class.SUFFIX)
6657            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
6658            temp_file_stat = os.stat(temp_file.name)[0]
6659            try:
6660                # update_metadata on file's internal metadata round-trips okay
6661                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
6662                metadata = track.get_metadata()
6663                self.assertEqual(metadata.track_name, u"Foo")
6664                metadata.track_name = u"Bar"
6665                track.update_metadata(metadata)
6666                metadata = track.get_metadata()
6667                self.assertEqual(metadata.track_name, u"Bar")
6668
6669                # update_metadata on unwritable file generates IOError
6670                metadata = track.get_metadata()
6671                os.chmod(temp_file.name, 0)
6672                self.assertRaises(IOError,
6673                                  track.update_metadata,
6674                                  metadata)
6675                os.chmod(temp_file.name, temp_file_stat)
6676
6677                # update_metadata with foreign MetaData generates ValueError
6678                self.assertRaises(ValueError,
6679                                  track.update_metadata,
6680                                  audiotools.MetaData(track_name=u"Foo"))
6681
6682                # update_metadata with None makes no changes
6683                track.update_metadata(None)
6684                metadata = track.get_metadata()
6685                self.assertEqual(metadata.track_name, u"Bar")
6686
6687                # vendor_string not updated with set_metadata()
6688                # but can be updated with update_metadata()
6689                old_metadata = track.get_metadata()
6690                new_metadata = audiotools.VorbisComment(
6691                    comment_strings=old_metadata.comment_strings[:],
6692                    vendor_string=u"Vendor String")
6693                track.set_metadata(new_metadata)
6694                self.assertEqual(track.get_metadata().vendor_string,
6695                                 old_metadata.vendor_string)
6696                track.update_metadata(new_metadata)
6697                self.assertEqual(track.get_metadata().vendor_string,
6698                                 new_metadata.vendor_string)
6699
6700                # REPLAYGAIN_* tags not updated with set_metadata()
6701                # but can be updated with update_metadata()
6702                old_metadata = track.get_metadata()
6703                new_metadata = audiotools.VorbisComment(
6704                    comment_strings=old_metadata.comment_strings +
6705                    [u"REPLAYGAIN_REFERENCE_LOUDNESS=89.0 dB"],
6706                    vendor_string=old_metadata.vendor_string)
6707                track.set_metadata(new_metadata)
6708                self.assertRaises(
6709                    KeyError,
6710                    track.get_metadata().__getitem__,
6711                    u"REPLAYGAIN_REFERENCE_LOUDNESS")
6712                track.update_metadata(new_metadata)
6713                self.assertEqual(
6714                    track.get_metadata()[u"REPLAYGAIN_REFERENCE_LOUDNESS"],
6715                    [u"89.0 dB"])
6716            finally:
6717                temp_file.close()
6718
6719    @METADATA_VORBIS
6720    def test_foreign_field(self):
6721        metadata = audiotools.VorbisComment([u"TITLE=Track Name",
6722                                             u"ALBUM=Album Name",
6723                                             u"TRACKNUMBER=1",
6724                                             u"TRACKTOTAL=3",
6725                                             u"DISCNUMBER=2",
6726                                             u"DISCTOTAL=4",
6727                                             u"FOO=Bar"], u"")
6728        for format in self.supported_formats:
6729            temp_file = tempfile.NamedTemporaryFile(
6730                suffix="." + format.SUFFIX)
6731            try:
6732                track = format.from_pcm(temp_file.name,
6733                                        BLANK_PCM_Reader(1))
6734                track.set_metadata(metadata)
6735                metadata2 = track.get_metadata()
6736                self.assertEqual(metadata.comment_strings,
6737                                 metadata2.comment_strings)
6738                self.assertEqual(metadata.__class__, metadata2.__class__)
6739                self.assertEqual(metadata2[u"FOO"], [u"Bar"])
6740            finally:
6741                temp_file.close()
6742
6743    @METADATA_VORBIS
6744    def test_field_mapping(self):
6745        mapping = [('track_name', u'TITLE', u'a'),
6746                   ('track_number', u'TRACKNUMBER', 1),
6747                   ('track_total', u'TRACKTOTAL', 2),
6748                   ('album_name', u'ALBUM', u'b'),
6749                   ('artist_name', u'ARTIST', u'c'),
6750                   ('performer_name', u'PERFORMER', u'd'),
6751                   ('composer_name', u'COMPOSER', u'e'),
6752                   ('conductor_name', u'CONDUCTOR', u'f'),
6753                   ('media', u'SOURCE MEDIUM', u'g'),
6754                   ('ISRC', u'ISRC', u'h'),
6755                   ('catalog', u'CATALOG', u'i'),
6756                   ('copyright', u'COPYRIGHT', u'j'),
6757                   ('year', u'DATE', u'k'),
6758                   ('album_number', u'DISCNUMBER', 3),
6759                   ('album_total', u'DISCTOTAL', 4),
6760                   ('comment', u'COMMENT', u'l')]
6761
6762        for format in self.supported_formats:
6763            temp_file = tempfile.NamedTemporaryFile(suffix="." + format.SUFFIX)
6764            try:
6765                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
6766
6767                # ensure that setting a class field
6768                # updates its corresponding low-level implementation
6769                for (field, key, value) in mapping:
6770                    track.delete_metadata()
6771                    metadata = self.empty_metadata()
6772                    setattr(metadata, field, value)
6773                    self.assertEqual(getattr(metadata, field), value)
6774                    self.assertEqual(
6775                        metadata[key][0],
6776                        u"%s" % (value))
6777                    track.set_metadata(metadata)
6778                    metadata2 = track.get_metadata()
6779                    self.assertEqual(getattr(metadata2, field), value)
6780                    self.assertEqual(
6781                        metadata2[key][0],
6782                        u"%s" % (value))
6783
6784                # ensure that updating the low-level implementation
6785                # is reflected in the class field
6786                for (field, key, value) in mapping:
6787                    track.delete_metadata()
6788                    metadata = self.empty_metadata()
6789                    metadata[key] = [u"%s" % (value)]
6790                    self.assertEqual(getattr(metadata, field), value)
6791                    self.assertEqual(
6792                        metadata[key][0],
6793                        u"%s" % (value))
6794                    track.set_metadata(metadata)
6795                    metadata2 = track.get_metadata()
6796                    self.assertEqual(getattr(metadata2, field), value)
6797                    self.assertEqual(
6798                        metadata2[key][0],
6799                        u"%s" % (value))
6800            finally:
6801                temp_file.close()
6802
6803    @METADATA_VORBIS
6804    def test_getitem(self):
6805        # getitem with no matches raises KeyError
6806        self.assertRaises(KeyError,
6807                          audiotools.VorbisComment([u"FOO=kelp"],
6808                                                   u"vendor").__getitem__,
6809                          u"BAR")
6810
6811        # getitem with 1 match returns list of length 1
6812        self.assertEqual(
6813            audiotools.VorbisComment([u"FOO=kelp",
6814                                      u"BAR=spam"], u"vendor")[u"FOO"],
6815            [u"kelp"])
6816
6817        # getitem with multiple matches returns multiple items, in order
6818        self.assertEqual(
6819            audiotools.VorbisComment([u"FOO=1",
6820                                      u"BAR=spam",
6821                                      u"FOO=2",
6822                                      u"FOO=3"], u"vendor")[u"FOO"],
6823            [u"1", u"2", u"3"])
6824
6825        # getitem with aliases returns all matching items, in order
6826        self.assertEqual(
6827            audiotools.VorbisComment([u"TRACKTOTAL=1",
6828                                      u"TOTALTRACKS=2",
6829                                      u"TRACKTOTAL=3"],
6830                                     u"vendor")[u"TRACKTOTAL"],
6831            [u"1", u"2", u"3"])
6832
6833        self.assertEqual(
6834            audiotools.VorbisComment([u"TRACKTOTAL=1",
6835                                      u"TOTALTRACKS=2",
6836                                      u"TRACKTOTAL=3"],
6837                                     u"vendor")[u"TOTALTRACKS"],
6838            [u"1", u"2", u"3"])
6839
6840        # getitem is case-insensitive
6841        self.assertEqual(
6842            audiotools.VorbisComment([u"FOO=kelp"], u"vendor")[u"FOO"],
6843            [u"kelp"])
6844
6845        self.assertEqual(
6846            audiotools.VorbisComment([u"FOO=kelp"], u"vendor")[u"foo"],
6847            [u"kelp"])
6848
6849        self.assertEqual(
6850            audiotools.VorbisComment([u"foo=kelp"], u"vendor")[u"FOO"],
6851            [u"kelp"])
6852
6853        self.assertEqual(
6854            audiotools.VorbisComment([u"foo=kelp"], u"vendor")[u"foo"],
6855            [u"kelp"])
6856
6857    @METADATA_VORBIS
6858    def test_setitem(self):
6859        # setitem replaces all keys with new values
6860        metadata = audiotools.VorbisComment([], u"vendor")
6861        metadata[u"FOO"] = [u"bar"]
6862        self.assertEqual(metadata[u"FOO"], [u"bar"])
6863
6864        metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor")
6865        metadata[u"FOO"] = [u"bar"]
6866        self.assertEqual(metadata[u"FOO"], [u"bar"])
6867
6868        metadata = audiotools.VorbisComment([u"FOO=1",
6869                                             u"FOO=2"], u"vendor")
6870        metadata[u"FOO"] = [u"bar"]
6871        self.assertEqual(metadata[u"FOO"], [u"bar"])
6872
6873        metadata = audiotools.VorbisComment([], u"vendor")
6874        metadata[u"FOO"] = [u"bar", u"baz"]
6875        self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"])
6876
6877        metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor")
6878        metadata[u"FOO"] = [u"bar", u"baz"]
6879        self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"])
6880
6881        metadata = audiotools.VorbisComment([u"FOO=1",
6882                                             u"FOO=2"], u"vendor")
6883        metadata[u"FOO"] = [u"bar", u"baz"]
6884        self.assertEqual(metadata[u"FOO"], [u"bar", u"baz"])
6885
6886        # setitem leaves other items alone
6887        metadata = audiotools.VorbisComment([u"BAR=bar"],
6888                                            u"vendor")
6889        metadata[u"FOO"] = [u"foo"]
6890        self.assertEqual(metadata.comment_strings,
6891                         [u"BAR=bar", u"FOO=foo"])
6892
6893        metadata = audiotools.VorbisComment([u"FOO=ack",
6894                                             u"BAR=bar"],
6895                                            u"vendor")
6896        metadata[u"FOO"] = [u"foo"]
6897        self.assertEqual(metadata.comment_strings,
6898                         [u"FOO=foo", u"BAR=bar"])
6899
6900        metadata = audiotools.VorbisComment([u"FOO=ack",
6901                                             u"BAR=bar"],
6902                                            u"vendor")
6903        metadata[u"FOO"] = [u"foo", u"fud"]
6904        self.assertEqual(metadata.comment_strings,
6905                         [u"FOO=foo", u"BAR=bar", u"FOO=fud"])
6906
6907        # setitem handles aliases automatically
6908        metadata = audiotools.VorbisComment([u"TRACKTOTAL=1",
6909                                             u"TOTALTRACKS=2",
6910                                             u"TRACKTOTAL=3"],
6911                                            u"vendor")
6912        metadata[u"TRACKTOTAL"] = [u"4", u"5", u"6"]
6913        self.assertEqual(metadata.comment_strings,
6914                         [u"TRACKTOTAL=4",
6915                          u"TOTALTRACKS=5",
6916                          u"TRACKTOTAL=6"])
6917
6918        metadata = audiotools.VorbisComment([u"TRACKTOTAL=1",
6919                                             u"TOTALTRACKS=2",
6920                                             u"TRACKTOTAL=3"],
6921                                            u"vendor")
6922        metadata[u"TOTALTRACKS"] = [u"4", u"5", u"6"]
6923        self.assertEqual(metadata.comment_strings,
6924                         [u"TRACKTOTAL=4",
6925                          u"TOTALTRACKS=5",
6926                          u"TRACKTOTAL=6"])
6927
6928        # setitem is case-preserving
6929        metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor")
6930        metadata[u"FOO"] = [u"bar"]
6931        self.assertEqual(metadata.comment_strings,
6932                         [u"FOO=bar"])
6933
6934        metadata = audiotools.VorbisComment([u"FOO=1"], u"vendor")
6935        metadata[u"foo"] = [u"bar"]
6936        self.assertEqual(metadata.comment_strings,
6937                         [u"FOO=bar"])
6938
6939        metadata = audiotools.VorbisComment([u"foo=1"], u"vendor")
6940        metadata[u"FOO"] = [u"bar"]
6941        self.assertEqual(metadata.comment_strings,
6942                         [u"foo=bar"])
6943
6944        metadata = audiotools.VorbisComment([u"foo=1"], u"vendor")
6945        metadata[u"foo"] = [u"bar"]
6946        self.assertEqual(metadata.comment_strings,
6947                         [u"foo=bar"])
6948
6949    @METADATA_VORBIS
6950    def test_getattr(self):
6951        # track_number grabs the first available integer
6952        self.assertEqual(
6953            audiotools.VorbisComment([u"TRACKNUMBER=10"],
6954                                     u"vendor").track_number,
6955            10)
6956
6957        self.assertEqual(
6958            audiotools.VorbisComment([u"TRACKNUMBER=10",
6959                                      u"TRACKNUMBER=5"],
6960                                     u"vendor").track_number,
6961            10)
6962
6963        self.assertEqual(
6964            audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar"],
6965                                     u"vendor").track_number,
6966            10)
6967
6968        self.assertEqual(
6969            audiotools.VorbisComment([u"TRACKNUMBER=foo",
6970                                      u"TRACKNUMBER=10"],
6971                                     u"vendor").track_number,
6972            10)
6973
6974        self.assertEqual(
6975            audiotools.VorbisComment([u"TRACKNUMBER=foo",
6976                                      u"TRACKNUMBER=foo 10 bar"],
6977                                     u"vendor").track_number,
6978            10)
6979
6980        # track_number is case-insensitive
6981        self.assertEqual(
6982            audiotools.VorbisComment([u"tRaCkNuMbEr=10"],
6983                                     u"vendor").track_number,
6984            10)
6985
6986        # album_number grabs the first available integer
6987        self.assertEqual(
6988            audiotools.VorbisComment([u"DISCNUMBER=20"],
6989                                     u"vendor").album_number,
6990            20)
6991
6992        self.assertEqual(
6993            audiotools.VorbisComment([u"DISCNUMBER=20",
6994                                      u"DISCNUMBER=5"],
6995                                     u"vendor").album_number,
6996            20)
6997
6998        self.assertEqual(
6999            audiotools.VorbisComment([u"DISCNUMBER=foo 20 bar"],
7000                                     u"vendor").album_number,
7001            20)
7002
7003        self.assertEqual(
7004            audiotools.VorbisComment([u"DISCNUMBER=foo",
7005                                      u"DISCNUMBER=20"],
7006                                     u"vendor").album_number,
7007            20)
7008
7009        self.assertEqual(
7010            audiotools.VorbisComment([u"DISCNUMBER=foo",
7011                                      u"DISCNUMBER=foo 20 bar"],
7012                                     u"vendor").album_number,
7013            20)
7014
7015        # album_number is case-insensitive
7016        self.assertEqual(
7017            audiotools.VorbisComment([u"dIsCnUmBeR=20"],
7018                                     u"vendor").album_number,
7019            20)
7020
7021        # track_total grabs the first available TRACKTOTAL integer
7022        # before falling back on slashed fields
7023        self.assertEqual(
7024            audiotools.VorbisComment([u"TRACKTOTAL=15"],
7025                                     u"vendor").track_total,
7026            15)
7027
7028        self.assertEqual(
7029            audiotools.VorbisComment([u"TRACKNUMBER=5/10"],
7030                                     u"vendor").track_total,
7031            10)
7032
7033        self.assertEqual(
7034            audiotools.VorbisComment([u"TRACKTOTAL=foo/10"],
7035                                     u"vendor").track_total,
7036            10)
7037
7038        self.assertEqual(
7039            audiotools.VorbisComment([u"TRACKNUMBER=5/10",
7040                                      u"TRACKTOTAL=15"],
7041                                     u"vendor").track_total,
7042            15)
7043
7044        self.assertEqual(
7045            audiotools.VorbisComment([u"TRACKTOTAL=15",
7046                                      u"TRACKNUMBER=5/10"],
7047                                     u"vendor").track_total,
7048            15)
7049
7050        # track_total is case-insensitive
7051        self.assertEqual(
7052            audiotools.VorbisComment([u"tracktotal=15"],
7053                                     u"vendor").track_total,
7054            15)
7055
7056        # track_total supports aliases
7057        self.assertEqual(
7058            audiotools.VorbisComment([u"TOTALTRACKS=15"],
7059                                     u"vendor").track_total,
7060            15)
7061
7062        # album_total grabs the first available DISCTOTAL integer
7063        # before falling back on slashed fields
7064        self.assertEqual(
7065            audiotools.VorbisComment([u"DISCTOTAL=25"],
7066                                     u"vendor").album_total,
7067            25)
7068
7069        self.assertEqual(
7070            audiotools.VorbisComment([u"DISCNUMBER=10/30"],
7071                                     u"vendor").album_total,
7072            30)
7073
7074        self.assertEqual(
7075            audiotools.VorbisComment([u"DISCNUMBER=foo/30"],
7076                                     u"vendor").album_total,
7077            30)
7078
7079        self.assertEqual(
7080            audiotools.VorbisComment([u"DISCNUMBER=10/30",
7081                                      u"DISCTOTAL=25"],
7082                                     u"vendor").album_total,
7083            25)
7084
7085        self.assertEqual(
7086            audiotools.VorbisComment([u"DISCTOTAL=25",
7087                                      u"DISCNUMBER=10/30"],
7088                                     u"vendor").album_total,
7089            25)
7090
7091        # album_total is case-insensitive
7092        self.assertEqual(
7093            audiotools.VorbisComment([u"disctotal=25"],
7094                                     u"vendor").album_total,
7095            25)
7096
7097        # album_total supports aliases
7098        self.assertEqual(
7099            audiotools.VorbisComment([u"TOTALDISCS=25"],
7100                                     u"vendor").album_total,
7101            25)
7102
7103        # other fields grab the first available item
7104        self.assertEqual(
7105            audiotools.VorbisComment([u"TITLE=first",
7106                                      u"TITLE=last"],
7107                                     u"vendor").track_name,
7108            u"first")
7109
7110    @METADATA_VORBIS
7111    def test_setattr(self):
7112        # track_number adds new field if necessary
7113        metadata = audiotools.VorbisComment([], u"vendor")
7114        self.assertIsNone(metadata.track_number)
7115        metadata.track_number = 11
7116        self.assertEqual(metadata.comment_strings,
7117                         [u"TRACKNUMBER=11"])
7118        self.assertEqual(metadata.track_number, 11)
7119
7120        metadata = audiotools.VorbisComment([u"TRACKNUMBER=blah"],
7121                                            u"vendor")
7122        self.assertIsNone(metadata.track_number)
7123        metadata.track_number = 11
7124        self.assertEqual(metadata.comment_strings,
7125                         [u"TRACKNUMBER=blah",
7126                          u"TRACKNUMBER=11"])
7127        self.assertEqual(metadata.track_number, 11)
7128
7129        # track_number updates the first integer field
7130        # and leaves other junk in that field alone
7131        metadata = audiotools.VorbisComment([u"TRACKNUMBER=10/12"], u"vendor")
7132        self.assertEqual(metadata.track_number, 10)
7133        metadata.track_number = 11
7134        self.assertEqual(metadata.comment_strings,
7135                         [u"TRACKNUMBER=11/12"])
7136        self.assertEqual(metadata.track_number, 11)
7137
7138        metadata = audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar"],
7139                                            u"vendor")
7140        self.assertEqual(metadata.track_number, 10)
7141        metadata.track_number = 11
7142        self.assertEqual(metadata.comment_strings,
7143                         [u"TRACKNUMBER=foo 11 bar"])
7144        self.assertEqual(metadata.track_number, 11)
7145
7146        metadata = audiotools.VorbisComment([u"TRACKNUMBER=foo 10 bar",
7147                                             u"TRACKNUMBER=blah"],
7148                                            u"vendor")
7149        self.assertEqual(metadata.track_number, 10)
7150        metadata.track_number = 11
7151        self.assertEqual(metadata.comment_strings,
7152                         [u"TRACKNUMBER=foo 11 bar",
7153                          u"TRACKNUMBER=blah"])
7154        self.assertEqual(metadata.track_number, 11)
7155
7156        # album_number adds new field if necessary
7157        metadata = audiotools.VorbisComment([], u"vendor")
7158        self.assertIsNone(metadata.album_number)
7159        metadata.album_number = 3
7160        self.assertEqual(metadata.comment_strings,
7161                         [u"DISCNUMBER=3"])
7162        self.assertEqual(metadata.album_number, 3)
7163
7164        metadata = audiotools.VorbisComment([u"DISCNUMBER=blah"],
7165                                            u"vendor")
7166        self.assertIsNone(metadata.album_number)
7167        metadata.album_number = 3
7168        self.assertEqual(metadata.comment_strings,
7169                         [u"DISCNUMBER=blah",
7170                          u"DISCNUMBER=3"])
7171        self.assertEqual(metadata.album_number, 3)
7172
7173        # album_number updates the first integer field
7174        # and leaves other junk in that field alone
7175        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"], u"vendor")
7176        self.assertEqual(metadata.album_number, 2)
7177        metadata.album_number = 3
7178        self.assertEqual(metadata.comment_strings,
7179                         [u"DISCNUMBER=3/4"])
7180        self.assertEqual(metadata.album_number, 3)
7181
7182        metadata = audiotools.VorbisComment([u"DISCNUMBER=foo 2 bar"],
7183                                            u"vendor")
7184        self.assertEqual(metadata.album_number, 2)
7185        metadata.album_number = 3
7186        self.assertEqual(metadata.comment_strings,
7187                         [u"DISCNUMBER=foo 3 bar"])
7188        self.assertEqual(metadata.album_number, 3)
7189
7190        metadata = audiotools.VorbisComment([u"DISCNUMBER=foo 2 bar",
7191                                             u"DISCNUMBER=blah"],
7192                                            u"vendor")
7193        self.assertEqual(metadata.album_number, 2)
7194        metadata.album_number = 3
7195        self.assertEqual(metadata.comment_strings,
7196                         [u"DISCNUMBER=foo 3 bar",
7197                          u"DISCNUMBER=blah"])
7198        self.assertEqual(metadata.album_number, 3)
7199
7200        # track_total adds new TRACKTOTAL field if necessary
7201        metadata = audiotools.VorbisComment([], u"vendor")
7202        self.assertIsNone(metadata.track_total)
7203        metadata.track_total = 12
7204        self.assertEqual(metadata.comment_strings,
7205                         [u"TRACKTOTAL=12"])
7206        self.assertEqual(metadata.track_total, 12)
7207
7208        metadata = audiotools.VorbisComment([u"TRACKTOTAL=blah"],
7209                                            u"vendor")
7210        self.assertIsNone(metadata.track_total)
7211        metadata.track_total = 12
7212        self.assertEqual(metadata.comment_strings,
7213                         [u"TRACKTOTAL=blah",
7214                          u"TRACKTOTAL=12"])
7215        self.assertEqual(metadata.track_total, 12)
7216
7217        # track_total updates first integer TRACKTOTAL field first if possible
7218        # including aliases
7219        metadata = audiotools.VorbisComment([u"TRACKTOTAL=blah",
7220                                             u"TRACKTOTAL=2"], u"vendor")
7221        self.assertEqual(metadata.track_total, 2)
7222        metadata.track_total = 3
7223        self.assertEqual(metadata.comment_strings,
7224                         [u"TRACKTOTAL=blah",
7225                          u"TRACKTOTAL=3"])
7226        self.assertEqual(metadata.track_total, 3)
7227
7228        metadata = audiotools.VorbisComment([u"TOTALTRACKS=blah",
7229                                             u"TOTALTRACKS=2"], u"vendor")
7230        self.assertEqual(metadata.track_total, 2)
7231        metadata.track_total = 3
7232        self.assertEqual(metadata.comment_strings,
7233                         [u"TOTALTRACKS=blah",
7234                          u"TOTALTRACKS=3"])
7235        self.assertEqual(metadata.track_total, 3)
7236
7237        # track_total updates slashed TRACKNUMBER field if necessary
7238        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/4",
7239                                             u"TRACKTOTAL=2"], u"vendor")
7240        self.assertEqual(metadata.track_total, 2)
7241        metadata.track_total = 3
7242        self.assertEqual(metadata.comment_strings,
7243                         [u"TRACKNUMBER=1/4",
7244                          u"TRACKTOTAL=3"])
7245        self.assertEqual(metadata.track_total, 3)
7246
7247        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/4"], u"vendor")
7248        self.assertEqual(metadata.track_total, 4)
7249        metadata.track_total = 3
7250        self.assertEqual(metadata.comment_strings,
7251                         [u"TRACKNUMBER=1/3"])
7252        self.assertEqual(metadata.track_total, 3)
7253
7254        metadata = audiotools.VorbisComment([u"TRACKNUMBER= foo / 4 bar"],
7255                                            u"vendor")
7256        self.assertEqual(metadata.track_total, 4)
7257        metadata.track_total = 3
7258        self.assertEqual(metadata.comment_strings,
7259                         [u"TRACKNUMBER= foo / 3 bar"])
7260        self.assertEqual(metadata.track_total, 3)
7261
7262        # album_total adds new DISCTOTAL field if necessary
7263        metadata = audiotools.VorbisComment([], u"vendor")
7264        self.assertIsNone(metadata.album_total)
7265        metadata.album_total = 4
7266        self.assertEqual(metadata.comment_strings,
7267                         [u"DISCTOTAL=4"])
7268        self.assertEqual(metadata.album_total, 4)
7269
7270        metadata = audiotools.VorbisComment([u"DISCTOTAL=blah"],
7271                                            u"vendor")
7272        self.assertIsNone(metadata.album_total)
7273        metadata.album_total = 4
7274        self.assertEqual(metadata.comment_strings,
7275                         [u"DISCTOTAL=blah",
7276                          u"DISCTOTAL=4"])
7277        self.assertEqual(metadata.album_total, 4)
7278
7279        # album_total updates DISCTOTAL field first if possible
7280        # including aliases
7281        metadata = audiotools.VorbisComment([u"DISCTOTAL=blah",
7282                                             u"DISCTOTAL=3"], u"vendor")
7283        self.assertEqual(metadata.album_total, 3)
7284        metadata.album_total = 4
7285        self.assertEqual(metadata.comment_strings,
7286                         [u"DISCTOTAL=blah",
7287                          u"DISCTOTAL=4"])
7288        self.assertEqual(metadata.album_total, 4)
7289
7290        metadata = audiotools.VorbisComment([u"TOTALDISCS=blah",
7291                                             u"TOTALDISCS=3"], u"vendor")
7292        self.assertEqual(metadata.album_total, 3)
7293        metadata.album_total = 4
7294        self.assertEqual(metadata.comment_strings,
7295                         [u"TOTALDISCS=blah",
7296                          u"TOTALDISCS=4"])
7297        self.assertEqual(metadata.album_total, 4)
7298
7299        # album_total updates slashed DISCNUMBER field if necessary
7300        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/3",
7301                                             u"DISCTOTAL=5"], u"vendor")
7302        self.assertEqual(metadata.album_total, 5)
7303        metadata.album_total = 6
7304        self.assertEqual(metadata.comment_strings,
7305                         [u"DISCNUMBER=2/3",
7306                          u"DISCTOTAL=6"])
7307        self.assertEqual(metadata.album_total, 6)
7308
7309        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/3"], u"vendor")
7310        self.assertEqual(metadata.album_total, 3)
7311        metadata.album_total = 6
7312        self.assertEqual(metadata.comment_strings,
7313                         [u"DISCNUMBER=2/6"])
7314        self.assertEqual(metadata.album_total, 6)
7315
7316        metadata = audiotools.VorbisComment([u"DISCNUMBER= foo / 3 bar"],
7317                                            u"vendor")
7318        self.assertEqual(metadata.album_total, 3)
7319        metadata.album_total = 6
7320        self.assertEqual(metadata.comment_strings,
7321                         [u"DISCNUMBER= foo / 6 bar"])
7322        self.assertEqual(metadata.album_total, 6)
7323
7324        # other fields update the first match
7325        # while leaving the rest alone
7326        metadata = audiotools.VorbisComment([u"TITLE=foo",
7327                                             u"TITLE=bar",
7328                                             u"FOO=baz"],
7329                                            u"vendor")
7330        metadata.track_name = u"blah"
7331        self.assertEqual(metadata.track_name, u"blah")
7332        self.assertEqual(metadata.comment_strings,
7333                         [u"TITLE=blah",
7334                          u"TITLE=bar",
7335                          u"FOO=baz"])
7336
7337        # setting field to an empty string is okay
7338        metadata = audiotools.VorbisComment([], u"vendor")
7339        metadata.track_name = u""
7340        self.assertEqual(metadata.track_name, u"")
7341        self.assertEqual(metadata.comment_strings,
7342                         [u"TITLE="])
7343
7344    @METADATA_VORBIS
7345    def test_delattr(self):
7346        # deleting nonexistent field is okay
7347        for field in audiotools.MetaData.FIELDS:
7348            metadata = audiotools.VorbisComment([],
7349                                                u"vendor")
7350            delattr(metadata, field)
7351            self.assertIsNone(getattr(metadata, field))
7352
7353        # deleting field removes all instances of it
7354        metadata = audiotools.VorbisComment([],
7355                                            u"vendor")
7356        del(metadata.track_name)
7357        self.assertEqual(metadata.comment_strings,
7358                         [])
7359        self.assertIsNone(metadata.track_name)
7360
7361        metadata = audiotools.VorbisComment([u"TITLE=track name"],
7362                                            u"vendor")
7363        del(metadata.track_name)
7364        self.assertEqual(metadata.comment_strings,
7365                         [])
7366        self.assertIsNone(metadata.track_name)
7367
7368        metadata = audiotools.VorbisComment([u"TITLE=track name",
7369                                             u"ALBUM=album name"],
7370                                            u"vendor")
7371        del(metadata.track_name)
7372        self.assertEqual(metadata.comment_strings,
7373                         [u"ALBUM=album name"])
7374        self.assertIsNone(metadata.track_name)
7375
7376        metadata = audiotools.VorbisComment([u"TITLE=track name",
7377                                             u"TITLE=track name 2",
7378                                             u"ALBUM=album name",
7379                                             u"TITLE=track name 3"],
7380                                            u"vendor")
7381        del(metadata.track_name)
7382        self.assertEqual(metadata.comment_strings,
7383                         [u"ALBUM=album name"])
7384        self.assertIsNone(metadata.track_name)
7385
7386        # setting field to None is the same as deleting field
7387        metadata = audiotools.VorbisComment([u"TITLE=track name"],
7388                                            u"vendor")
7389        metadata.track_name = None
7390        self.assertEqual(metadata.comment_strings,
7391                         [])
7392        self.assertIsNone(metadata.track_name)
7393
7394        metadata = audiotools.VorbisComment([u"TITLE=track name"],
7395                                            u"vendor")
7396        metadata.track_name = None
7397        self.assertEqual(metadata.comment_strings,
7398                         [])
7399        self.assertIsNone(metadata.track_name)
7400
7401        # deleting track_number removes TRACKNUMBER field
7402        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1"],
7403                                            u"vendor")
7404        del(metadata.track_number)
7405        self.assertEqual(metadata.comment_strings,
7406                         [])
7407        self.assertIsNone(metadata.track_number)
7408
7409        # deleting slashed TRACKNUMBER converts it to fresh TRACKTOTAL field
7410        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3"],
7411                                            u"vendor")
7412        del(metadata.track_number)
7413        self.assertEqual(metadata.comment_strings,
7414                         [u"TRACKTOTAL=3"])
7415        self.assertIsNone(metadata.track_number)
7416
7417        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3",
7418                                             u"TRACKTOTAL=4"],
7419                                            u"vendor")
7420        self.assertEqual(metadata.track_total, 4)
7421        del(metadata.track_number)
7422        self.assertEqual(metadata.comment_strings,
7423                         [u"TRACKTOTAL=4"])
7424        self.assertEqual(metadata.track_total, 4)
7425        self.assertIsNone(metadata.track_number)
7426
7427        # deleting track_total removes TRACKTOTAL/TOTALTRACKS fields
7428        metadata = audiotools.VorbisComment([u"TRACKTOTAL=3",
7429                                             u"TOTALTRACKS=4"],
7430                                            u"vendor")
7431        del(metadata.track_total)
7432        self.assertEqual(metadata.comment_strings,
7433                         [])
7434        self.assertIsNone(metadata.track_total)
7435
7436        # deleting track_total also removes slashed side of TRACKNUMBER fields
7437        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1/3"],
7438                                            u"vendor")
7439        del(metadata.track_total)
7440        self.assertIsNone(metadata.track_total)
7441        self.assertEqual(metadata.comment_strings,
7442                         [u"TRACKNUMBER=1"])
7443
7444        metadata = audiotools.VorbisComment([u"TRACKNUMBER=1 / foo 3 baz"],
7445                                            u"vendor")
7446        del(metadata.track_total)
7447        self.assertIsNone(metadata.track_total)
7448        self.assertEqual(metadata.comment_strings,
7449                         [u"TRACKNUMBER=1"])
7450
7451        metadata = audiotools.VorbisComment(
7452            [u"TRACKNUMBER= foo 1 bar / blah 4 baz"], u"vendor")
7453        del(metadata.track_total)
7454        self.assertIsNone(metadata.track_total)
7455        self.assertEqual(metadata.comment_strings,
7456                         [u"TRACKNUMBER= foo 1 bar"])
7457
7458        # deleting album_number removes DISCNUMBER field
7459        metadata = audiotools.VorbisComment([u"DISCNUMBER=2"],
7460                                            u"vendor")
7461        del(metadata.album_number)
7462        self.assertEqual(metadata.comment_strings,
7463                         [])
7464
7465        # deleting slashed DISCNUMBER converts it to fresh DISCTOTAL field
7466        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"],
7467                                            u"vendor")
7468        del(metadata.album_number)
7469        self.assertEqual(metadata.comment_strings,
7470                         [u"DISCTOTAL=4"])
7471
7472        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4",
7473                                             u"DISCTOTAL=5"],
7474                                            u"vendor")
7475        self.assertEqual(metadata.album_total, 5)
7476        del(metadata.album_number)
7477        self.assertEqual(metadata.comment_strings,
7478                         [u"DISCTOTAL=5"])
7479        self.assertEqual(metadata.album_total, 5)
7480
7481        # deleting album_total removes DISCTOTAL/TOTALDISCS fields
7482        metadata = audiotools.VorbisComment([u"DISCTOTAL=4",
7483                                             u"TOTALDISCS=5"],
7484                                            u"vendor")
7485        del(metadata.album_total)
7486        self.assertEqual(metadata.comment_strings,
7487                         [])
7488        self.assertIsNone(metadata.album_total)
7489
7490        # deleting album_total also removes slashed side of DISCNUMBER fields
7491        metadata = audiotools.VorbisComment([u"DISCNUMBER=2/4"],
7492                                            u"vendor")
7493        del(metadata.album_total)
7494        self.assertIsNone(metadata.album_total)
7495        self.assertEqual(metadata.comment_strings,
7496                         [u"DISCNUMBER=2"])
7497
7498        metadata = audiotools.VorbisComment([u"DISCNUMBER=2 / foo 4 baz"],
7499                                            u"vendor")
7500        del(metadata.album_total)
7501        self.assertIsNone(metadata.album_total)
7502        self.assertEqual(metadata.comment_strings,
7503                         [u"DISCNUMBER=2"])
7504
7505        metadata = audiotools.VorbisComment(
7506            [u"DISCNUMBER= foo 2 bar / blah 4 baz"], u"vendor")
7507        del(metadata.album_total)
7508        self.assertIsNone(metadata.album_total)
7509        self.assertEqual(metadata.comment_strings,
7510                         [u"DISCNUMBER= foo 2 bar"])
7511
7512    @METADATA_VORBIS
7513    def test_supports_images(self):
7514        self.assertEqual(self.metadata_class.supports_images(), False)
7515
7516    @METADATA_VORBIS
7517    def test_lowercase(self):
7518        for audio_format in self.supported_formats:
7519            temp_file = tempfile.NamedTemporaryFile(
7520                suffix="." + audio_format.SUFFIX)
7521            try:
7522                track = audio_format.from_pcm(temp_file.name,
7523                                              BLANK_PCM_Reader(1))
7524
7525                lc_metadata = audiotools.VorbisComment(
7526                    [u"title=track name",
7527                     u"tracknumber=1",
7528                     u"tracktotal=3",
7529                     u"album=album name",
7530                     u"artist=artist name",
7531                     u"performer=performer name",
7532                     u"composer=composer name",
7533                     u"conductor=conductor name",
7534                     u"source medium=media",
7535                     u"isrc=isrc",
7536                     u"catalog=catalog",
7537                     u"copyright=copyright",
7538                     u"publisher=publisher",
7539                     u"date=2009",
7540                     u"discnumber=2",
7541                     u"disctotal=4",
7542                     u"comment=some comment"],
7543                    u"vendor string")
7544
7545                metadata = audiotools.MetaData(
7546                    track_name=u"track name",
7547                    track_number=1,
7548                    track_total=3,
7549                    album_name=u"album name",
7550                    artist_name=u"artist name",
7551                    performer_name=u"performer name",
7552                    composer_name=u"composer name",
7553                    conductor_name=u"conductor name",
7554                    media=u"media",
7555                    ISRC=u"isrc",
7556                    catalog=u"catalog",
7557                    copyright=u"copyright",
7558                    publisher=u"publisher",
7559                    year=u"2009",
7560                    album_number=2,
7561                    album_total=4,
7562                    comment=u"some comment")
7563
7564                track.set_metadata(lc_metadata)
7565                track = audiotools.open(track.filename)
7566                self.assertEqual(metadata, lc_metadata)
7567
7568                track = audio_format.from_pcm(temp_file.name,
7569                                              BLANK_PCM_Reader(1))
7570                track.set_metadata(audiotools.MetaData(
7571                    track_name=u"Track Name",
7572                    track_number=1))
7573                metadata = track.get_metadata()
7574                self.assertEqual(metadata[u"TITLE"], [u"Track Name"])
7575                self.assertEqual(metadata[u"TRACKNUMBER"], [u"1"])
7576                self.assertEqual(metadata.track_name, u"Track Name")
7577                self.assertEqual(metadata.track_number, 1)
7578
7579                metadata[u"title"] = [u"New Track Name"]
7580                metadata[u"tracknumber"] = [u"2"]
7581                track.set_metadata(metadata)
7582                metadata = track.get_metadata()
7583                self.assertEqual(metadata[u"TITLE"], [u"New Track Name"])
7584                self.assertEqual(metadata[u"TRACKNUMBER"], [u"2"])
7585                self.assertEqual(metadata.track_name, u"New Track Name")
7586                self.assertEqual(metadata.track_number, 2)
7587
7588                metadata.track_name = u"New Track Name 2"
7589                metadata.track_number = 3
7590                track.set_metadata(metadata)
7591                metadata = track.get_metadata()
7592                self.assertEqual(metadata[u"TITLE"], [u"New Track Name 2"])
7593                self.assertEqual(metadata[u"TRACKNUMBER"], [u"3"])
7594                self.assertEqual(metadata.track_name, u"New Track Name 2")
7595                self.assertEqual(metadata.track_number, 3)
7596            finally:
7597                temp_file.close()
7598
7599    @METADATA_VORBIS
7600    def test_totals(self):
7601        metadata = self.empty_metadata()
7602        metadata[u"TRACKNUMBER"] = [u"2/4"]
7603        self.assertEqual(metadata.track_number, 2)
7604        self.assertEqual(metadata.track_total, 4)
7605
7606        metadata = self.empty_metadata()
7607        metadata[u"TRACKNUMBER"] = [u"02/4"]
7608        self.assertEqual(metadata.track_number, 2)
7609        self.assertEqual(metadata.track_total, 4)
7610
7611        metadata = self.empty_metadata()
7612        metadata[u"TRACKNUMBER"] = [u"2/04"]
7613        self.assertEqual(metadata.track_number, 2)
7614        self.assertEqual(metadata.track_total, 4)
7615
7616        metadata = self.empty_metadata()
7617        metadata[u"TRACKNUMBER"] = [u"02/04"]
7618        self.assertEqual(metadata.track_number, 2)
7619        self.assertEqual(metadata.track_total, 4)
7620
7621        metadata = self.empty_metadata()
7622        metadata[u"TRACKNUMBER"] = [u"foo 2 bar /4"]
7623        self.assertEqual(metadata.track_number, 2)
7624        self.assertEqual(metadata.track_total, 4)
7625
7626        metadata = self.empty_metadata()
7627        metadata[u"TRACKNUMBER"] = [u"2/ foo 4 bar"]
7628        self.assertEqual(metadata.track_number, 2)
7629        self.assertEqual(metadata.track_total, 4)
7630
7631        metadata = self.empty_metadata()
7632        metadata[u"TRACKNUMBER"] = [u"foo 2 bar / kelp 4 spam"]
7633        self.assertEqual(metadata.track_number, 2)
7634        self.assertEqual(metadata.track_total, 4)
7635
7636        metadata = self.empty_metadata()
7637        metadata[u"DISCNUMBER"] = [u"1/3"]
7638        self.assertEqual(metadata.album_number, 1)
7639        self.assertEqual(metadata.album_total, 3)
7640
7641        metadata = self.empty_metadata()
7642        metadata[u"DISCNUMBER"] = [u"01/3"]
7643        self.assertEqual(metadata.album_number, 1)
7644        self.assertEqual(metadata.album_total, 3)
7645
7646        metadata = self.empty_metadata()
7647        metadata[u"DISCNUMBER"] = [u"1/03"]
7648        self.assertEqual(metadata.album_number, 1)
7649        self.assertEqual(metadata.album_total, 3)
7650
7651        metadata = self.empty_metadata()
7652        metadata[u"DISCNUMBER"] = [u"01/03"]
7653        self.assertEqual(metadata.album_number, 1)
7654        self.assertEqual(metadata.album_total, 3)
7655
7656        metadata = self.empty_metadata()
7657        metadata[u"DISCNUMBER"] = [u"foo 1 bar /3"]
7658        self.assertEqual(metadata.album_number, 1)
7659        self.assertEqual(metadata.album_total, 3)
7660
7661        metadata = self.empty_metadata()
7662        metadata[u"DISCNUMBER"] = [u"1/ foo 3 bar"]
7663        self.assertEqual(metadata.album_number, 1)
7664        self.assertEqual(metadata.album_total, 3)
7665
7666        metadata = self.empty_metadata()
7667        metadata[u"DISCNUMBER"] = [u"foo 1 bar / kelp 3 spam"]
7668        self.assertEqual(metadata.album_number, 1)
7669        self.assertEqual(metadata.album_total, 3)
7670
7671    @METADATA_VORBIS
7672    def test_clean(self):
7673        from audiotools.text import (CLEAN_REMOVE_TRAILING_WHITESPACE,
7674                                     CLEAN_REMOVE_LEADING_WHITESPACE,
7675                                     CLEAN_REMOVE_LEADING_ZEROES,
7676                                     CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES,
7677                                     CLEAN_REMOVE_EMPTY_TAG)
7678
7679        # check trailing whitespace
7680        metadata = audiotools.VorbisComment([u"TITLE=Foo "], u"vendor")
7681        (cleaned, results) = metadata.clean()
7682        self.assertEqual(cleaned,
7683                         audiotools.VorbisComment([u"TITLE=Foo"], u"vendor"))
7684        self.assertEqual(results,
7685                         [CLEAN_REMOVE_TRAILING_WHITESPACE %
7686                          {"field": u"TITLE"}])
7687
7688        # check leading whitespace
7689        metadata = audiotools.VorbisComment([u"TITLE= Foo"], u"vendor")
7690        (cleaned, results) = metadata.clean()
7691        self.assertEqual(cleaned,
7692                         audiotools.VorbisComment([u"TITLE=Foo"], u"vendor"))
7693        self.assertEqual(results,
7694                         [CLEAN_REMOVE_LEADING_WHITESPACE %
7695                          {"field": u"TITLE"}])
7696
7697        # check leading zeroes
7698        metadata = audiotools.VorbisComment([u"TRACKNUMBER=001"], u"vendor")
7699        (cleaned, results) = metadata.clean()
7700        self.assertEqual(cleaned,
7701                         audiotools.VorbisComment([u"TRACKNUMBER=1"],
7702                                                  u"vendor"))
7703        self.assertEqual(results,
7704                         [CLEAN_REMOVE_LEADING_ZEROES %
7705                          {"field": u"TRACKNUMBER"}])
7706
7707        # check leading space/zeroes in slashed field
7708        for field in [u"TRACKNUMBER=01/2",
7709                      u"TRACKNUMBER=1/02",
7710                      u"TRACKNUMBER=01/02",
7711                      u"TRACKNUMBER=1/ 2",
7712                      u"TRACKNUMBER=1/ 02"]:
7713            metadata = audiotools.VorbisComment([field], u"vendor")
7714            (cleaned, results) = metadata.clean()
7715            self.assertEqual(cleaned,
7716                             audiotools.VorbisComment([u"TRACKNUMBER=1/2"],
7717                                                      u"vendor"))
7718            self.assertEqual(results,
7719                             [CLEAN_REMOVE_LEADING_WHITESPACE_ZEROES %
7720                              {"field": u"TRACKNUMBER"}])
7721
7722        # check empty fields
7723        metadata = audiotools.VorbisComment([u"TITLE="], u"vendor")
7724        (cleaned, results) = metadata.clean()
7725        self.assertEqual(cleaned,
7726                         audiotools.VorbisComment([], u"vendor"))
7727        self.assertEqual(results,
7728                         [CLEAN_REMOVE_EMPTY_TAG %
7729                          {"field": u"TITLE"}])
7730
7731        metadata = audiotools.VorbisComment([u"TITLE=    "], u"vendor")
7732        (cleaned, results) = metadata.clean()
7733        self.assertEqual(cleaned,
7734                         audiotools.VorbisComment([], u"vendor"))
7735        self.assertEqual(results,
7736                         [CLEAN_REMOVE_EMPTY_TAG %
7737                          {"field": u"TITLE"}])
7738
7739    @METADATA_VORBIS
7740    def test_aliases(self):
7741        for (key, map_to) in audiotools.VorbisComment.ALIASES.items():
7742            attr = [attr for (attr, item) in
7743                    audiotools.VorbisComment.ATTRIBUTE_MAP.items()
7744                    if item in map_to][0]
7745
7746            if attr in audiotools.VorbisComment.INTEGER_FIELDS:
7747                old_raw_value = u"1"
7748                old_attr_value = 1
7749                new_raw_value = u"2"
7750                new_attr_value = 2
7751            else:
7752                old_raw_value = old_attr_value = u"Foo"
7753                new_raw_value = new_attr_value = u"Bar"
7754
7755            metadata = audiotools.VorbisComment([], u"")
7756
7757            # ensure setting aliased field shows up in attribute
7758            metadata[key] = [old_raw_value]
7759            self.assertEqual(getattr(metadata, attr), old_attr_value)
7760
7761            # ensure updating attribute reflects in aliased field
7762            setattr(metadata, attr, new_attr_value)
7763            self.assertEqual(getattr(metadata, attr), new_attr_value)
7764            self.assertEqual(metadata[key], [new_raw_value])
7765
7766            self.assertEqual(metadata.keys(), [key])
7767
7768            # ensure updating the metadata with an aliased key
7769            # doesn't change the aliased key field
7770            for new_key in map_to:
7771                if new_key != key:
7772                    metadata[new_key] = [old_raw_value]
7773                    self.assertEqual(metadata.keys(), [key])
7774
7775    @METADATA_VORBIS
7776    def test_replay_gain(self):
7777        import test_streams
7778
7779        for input_class in [audiotools.FlacAudio,
7780                            audiotools.OggFlacAudio,
7781                            audiotools.VorbisAudio]:
7782            temp1 = tempfile.NamedTemporaryFile(
7783                suffix="." + input_class.SUFFIX)
7784            try:
7785                track1 = input_class.from_pcm(
7786                    temp1.name,
7787                    test_streams.Sine16_Stereo(44100, 44100,
7788                                               441.0, 0.50,
7789                                               4410.0, 0.49, 1.0))
7790                self.assertIsNone(track1.get_replay_gain(),
7791                                  "ReplayGain present for class %s" %
7792                                  (input_class.NAME))
7793                track1.set_metadata(audiotools.MetaData(track_name=u"Foo"))
7794                audiotools.add_replay_gain([track1])
7795                self.assertEqual(track1.get_metadata().track_name, u"Foo")
7796                self.assertIsNotNone(track1.get_replay_gain(),
7797                                     "ReplayGain not present for class %s" %
7798                                     (input_class.NAME))
7799
7800                for output_class in [audiotools.VorbisAudio]:
7801                    temp2 = tempfile.NamedTemporaryFile(
7802                        suffix="." + input_class.SUFFIX)
7803                    try:
7804                        track2 = output_class.from_pcm(
7805                            temp2.name,
7806                            test_streams.Sine16_Stereo(66150, 44100,
7807                                                       8820.0, 0.70,
7808                                                       4410.0, 0.29, 1.0))
7809
7810                        # ensure that ReplayGain doesn't get ported
7811                        # via set_metadata()
7812                        self.assertIsNone(
7813                            track2.get_replay_gain(),
7814                            "ReplayGain present for class %s" %
7815                            (output_class.NAME))
7816                        track2.set_metadata(track1.get_metadata())
7817                        self.assertEqual(track2.get_metadata().track_name,
7818                                         u"Foo")
7819                        self.assertIsNone(
7820                            track2.get_replay_gain(),
7821                            "ReplayGain present for class %s from %s" %
7822                            (output_class.NAME,
7823                             input_class.NAME))
7824
7825                        # and if ReplayGain is already set,
7826                        # ensure set_metadata() doesn't remove it
7827                        audiotools.add_replay_gain([track2])
7828                        old_replay_gain = track2.get_replay_gain()
7829                        self.assertIsNotNone(old_replay_gain)
7830                        track2.set_metadata(audiotools.MetaData(
7831                            track_name=u"Bar"))
7832                        self.assertEqual(track2.get_metadata().track_name,
7833                                         u"Bar")
7834                        self.assertEqual(track2.get_replay_gain(),
7835                                         old_replay_gain)
7836                    finally:
7837                        temp2.close()
7838            finally:
7839                temp1.close()
7840
7841
7842class OpusTagsTest(MetaDataTest):
7843    def setUp(self):
7844        self.metadata_class = audiotools.VorbisComment
7845        self.supported_fields = ["track_name",
7846                                 "track_number",
7847                                 "track_total",
7848                                 "album_name",
7849                                 "artist_name",
7850                                 "performer_name",
7851                                 "composer_name",
7852                                 "conductor_name",
7853                                 "media",
7854                                 "ISRC",
7855                                 "catalog",
7856                                 "copyright",
7857                                 "publisher",
7858                                 "year",
7859                                 "album_number",
7860                                 "album_total",
7861                                 "comment"]
7862        self.supported_formats = [audiotools.OpusAudio]
7863
7864    def empty_metadata(self):
7865        return self.metadata_class.converted(audiotools.MetaData())
7866
7867    @METADATA_OPUS
7868    def test_update(self):
7869        import os
7870
7871        for audio_class in self.supported_formats:
7872            temp_file = tempfile.NamedTemporaryFile(
7873                suffix="." + audio_class.SUFFIX)
7874            track = audio_class.from_pcm(temp_file.name, BLANK_PCM_Reader(10))
7875            temp_file_stat = os.stat(temp_file.name)[0]
7876            try:
7877                # update_metadata on file's internal metadata round-trips okay
7878                track.set_metadata(audiotools.MetaData(track_name=u"Foo"))
7879                metadata = track.get_metadata()
7880                self.assertEqual(metadata.track_name, u"Foo")
7881                metadata.track_name = u"Bar"
7882                track.update_metadata(metadata)
7883                metadata = track.get_metadata()
7884                self.assertEqual(metadata.track_name, u"Bar")
7885
7886                # update_metadata on unwritable file generates IOError
7887                metadata = track.get_metadata()
7888                os.chmod(temp_file.name, 0)
7889                self.assertRaises(IOError,
7890                                  track.update_metadata,
7891                                  metadata)
7892                os.chmod(temp_file.name, temp_file_stat)
7893
7894                # update_metadata with foreign MetaData generates ValueError
7895                self.assertRaises(ValueError,
7896                                  track.update_metadata,
7897                                  audiotools.MetaData(track_name=u"Foo"))
7898
7899                # update_metadata with None makes no changes
7900                track.update_metadata(None)
7901                metadata = track.get_metadata()
7902                self.assertEqual(metadata.track_name, u"Bar")
7903
7904                # vendor_string not updated with set_metadata()
7905                # but can be updated with update_metadata()
7906                old_metadata = track.get_metadata()
7907                new_metadata = audiotools.VorbisComment(
7908                    comment_strings=old_metadata.comment_strings[:],
7909                    vendor_string=u"Vendor String")
7910                track.set_metadata(new_metadata)
7911                self.assertEqual(track.get_metadata().vendor_string,
7912                                 old_metadata.vendor_string)
7913                track.update_metadata(new_metadata)
7914                self.assertEqual(track.get_metadata().vendor_string,
7915                                 new_metadata.vendor_string)
7916
7917                # REPLAYGAIN_* tags not updated with set_metadata()
7918                # but can be updated with update_metadata()
7919                old_metadata = track.get_metadata()
7920                new_metadata = audiotools.VorbisComment(
7921                    comment_strings=old_metadata.comment_strings +
7922                    [u"REPLAYGAIN_REFERENCE_LOUDNESS=89.0 dB"],
7923                    vendor_string=old_metadata.vendor_string)
7924                track.set_metadata(new_metadata)
7925                self.assertRaises(
7926                    KeyError,
7927                    track.get_metadata().__getitem__,
7928                    u"REPLAYGAIN_REFERENCE_LOUDNESS")
7929                track.update_metadata(new_metadata)
7930                self.assertEqual(
7931                    track.get_metadata()[u"REPLAYGAIN_REFERENCE_LOUDNESS"],
7932                    [u"89.0 dB"])
7933            finally:
7934                temp_file.close()
7935
7936    @METADATA_OPUS
7937    def test_foreign_field(self):
7938        metadata = audiotools.VorbisComment([u"TITLE=Track Name",
7939                                             u"ALBUM=Album Name",
7940                                             u"TRACKNUMBER=1",
7941                                             u"TRACKTOTAL=3",
7942                                             u"DISCNUMBER=2",
7943                                             u"DISCTOTAL=4",
7944                                             u"FOO=Bar"], u"")
7945        for format in self.supported_formats:
7946            temp_file = tempfile.NamedTemporaryFile(
7947                suffix="." + format.SUFFIX)
7948            try:
7949                track = format.from_pcm(temp_file.name,
7950                                        BLANK_PCM_Reader(1))
7951                track.set_metadata(metadata)
7952                metadata2 = track.get_metadata()
7953                self.assertTrue(
7954                    set(metadata.comment_strings).issubset(
7955                        set(metadata2.comment_strings)))
7956                self.assertEqual(metadata.__class__, metadata2.__class__)
7957                self.assertEqual(metadata2[u"FOO"], [u"Bar"])
7958            finally:
7959                temp_file.close()
7960
7961    @METADATA_OPUS
7962    def test_field_mapping(self):
7963        mapping = [('track_name', u'TITLE', u'a'),
7964                   ('track_number', u'TRACKNUMBER', 1),
7965                   ('track_total', u'TRACKTOTAL', 2),
7966                   ('album_name', u'ALBUM', u'b'),
7967                   ('artist_name', u'ARTIST', u'c'),
7968                   ('performer_name', u'PERFORMER', u'd'),
7969                   ('composer_name', u'COMPOSER', u'e'),
7970                   ('conductor_name', u'CONDUCTOR', u'f'),
7971                   ('media', u'SOURCE MEDIUM', u'g'),
7972                   ('ISRC', u'ISRC', u'h'),
7973                   ('catalog', u'CATALOG', u'i'),
7974                   ('copyright', u'COPYRIGHT', u'j'),
7975                   ('year', u'DATE', u'k'),
7976                   ('album_number', u'DISCNUMBER', 3),
7977                   ('album_total', u'DISCTOTAL', 4),
7978                   ('comment', u'COMMENT', u'l')]
7979
7980        for format in self.supported_formats:
7981            temp_file = tempfile.NamedTemporaryFile(suffix="." + format.SUFFIX)
7982            try:
7983                track = format.from_pcm(temp_file.name, BLANK_PCM_Reader(1))
7984
7985                # ensure that setting a class field
7986                # updates its corresponding low-level implementation
7987                for (field, key, value) in mapping:
7988                    track.delete_metadata()
7989                    metadata = self.empty_metadata()
7990                    setattr(metadata, field, value)
7991                    self.assertEqual(getattr(metadata, field), value)
7992                    self.assertEqual(
7993                        metadata[key][0],
7994                        u"%s" % (value))
7995                    track.set_metadata(metadata)
7996                    metadata2 = track.get_metadata()
7997                    self.assertEqual(getattr(metadata2, field), value)
7998                    self.assertEqual(
7999                        metadata2[key][0],
8000                        u"%s" % (value))
8001
8002                # ensure that updating the low-level implementation
8003                # is reflected in the class field
8004                for (field, key, value) in mapping:
8005                    track.delete_metadata()
8006                    metadata = self.empty_metadata()
8007                    metadata[key] = [u"%s" % (value)]
8008                    self.assertEqual(getattr(metadata, field), value)
8009                    self.assertEqual(
8010                        metadata[key][0],
8011                        u"%s" % (value))
8012                    track.set_metadata(metadata)
8013                    metadata2 = track.get_metadata()
8014                    self.assertEqual(getattr(metadata2, field), value)
8015                    self.assertEqual(
8016                        metadata2[key][0],
8017                        u"%s" % (value))
8018            finally:
8019                temp_file.close()
8020
8021    @METADATA_OPUS
8022    def test_supports_images(self):
8023        self.assertEqual(self.metadata_class.supports_images(), False)
8024
8025
8026class TrueAudioTest(unittest.TestCase):
8027    # True Audio supports APEv2, ID3v2 and ID3v1
8028    # which makes the format much more complicated
8029    # than if it supported only a single format.
8030
8031    def __base_metadatas__(self):
8032        base_metadata = audiotools.MetaData(
8033            track_name=u"Track Name",
8034            album_name=u"Album Name",
8035            artist_name=u"Artist Name",
8036            track_number=1)
8037
8038        yield audiotools.ApeTag.converted(base_metadata)
8039        yield audiotools.ID3v22Comment.converted(base_metadata)
8040        yield audiotools.ID3v23Comment.converted(base_metadata)
8041        yield audiotools.ID3v24Comment.converted(base_metadata)
8042        yield audiotools.ID3CommentPair.converted(base_metadata)
8043
8044    @METADATA_TTA
8045    def test_update(self):
8046        import os
8047
8048        for base_metadata in self.__base_metadatas__():
8049            temp_file = tempfile.NamedTemporaryFile(
8050                suffix="." + audiotools.TrueAudio.SUFFIX)
8051            track = audiotools.TrueAudio.from_pcm(temp_file.name,
8052                                                  BLANK_PCM_Reader(10))
8053            temp_file_stat = os.stat(temp_file.name)[0]
8054            try:
8055                # update_metadata on file's internal metadata round-trips okay
8056                track.update_metadata(base_metadata)
8057                metadata = track.get_metadata()
8058                self.assertEqual(metadata.track_name, u"Track Name")
8059                metadata.track_name = u"Bar"
8060                track.update_metadata(metadata)
8061                metadata = track.get_metadata()
8062                self.assertIs(metadata.__class__, base_metadata.__class__)
8063                self.assertEqual(metadata.track_name, u"Bar")
8064
8065                # update_metadata on unwritable file generates IOError
8066                os.chmod(temp_file.name, 0)
8067                self.assertRaises(IOError,
8068                                  track.update_metadata,
8069                                  base_metadata)
8070                os.chmod(temp_file.name, temp_file_stat)
8071
8072                # update_metadata with foreign MetaData generates ValueError
8073                self.assertRaises(ValueError,
8074                                  track.update_metadata,
8075                                  audiotools.MetaData(track_name=u"Foo"))
8076
8077                # # update_metadata with None makes no changes
8078                track.update_metadata(None)
8079                metadata = track.get_metadata()
8080                self.assertEqual(metadata.track_name, u"Bar")
8081
8082                if isinstance(base_metadata, audiotools.ApeTag):
8083                    # replaygain strings not updated with set_metadata()
8084                    # but can be updated with update_metadata()
8085                    self.assertRaises(KeyError,
8086                                      track.get_metadata().__getitem__,
8087                                      b"replaygain_track_gain")
8088                    metadata[b"replaygain_track_gain"] = \
8089                        audiotools.ape.ApeTagItem.string(
8090                            b"replaygain_track_gain", u"???")
8091                    track.set_metadata(metadata)
8092                    self.assertRaises(KeyError,
8093                                      track.get_metadata().__getitem__,
8094                                      b"replaygain_track_gain")
8095                    track.update_metadata(metadata)
8096                    self.assertEqual(
8097                        track.get_metadata()[b"replaygain_track_gain"],
8098                        audiotools.ape.ApeTagItem.string(
8099                            b"replaygain_track_gain", u"???"))
8100
8101                    # cuesheet not updated with set_metadata()
8102                    # but can be updated with update_metadata()
8103                    metadata[b"Cuesheet"] = \
8104                        audiotools.ape.ApeTagItem.string(
8105                            b"Cuesheet", u"???")
8106                    track.set_metadata(metadata)
8107                    self.assertRaises(KeyError,
8108                                      track.get_metadata().__getitem__,
8109                                      b"Cuesheet")
8110                    track.update_metadata(metadata)
8111                    self.assertEqual(
8112                        track.get_metadata()[b"Cuesheet"],
8113                        audiotools.ape.ApeTagItem.string(b"Cuesheet", u"???"))
8114            finally:
8115                temp_file.close()
8116
8117    @METADATA_TTA
8118    def test_delete(self):
8119        # delete metadata clears out ID3v?, ID3v1, ApeTag and ID3CommentPairs
8120
8121        for metadata in self.__base_metadatas__():
8122            temp_file = tempfile.NamedTemporaryFile(
8123                suffix="." + audiotools.TrueAudio.SUFFIX)
8124            try:
8125                track = audiotools.TrueAudio.from_pcm(temp_file.name,
8126                                                      BLANK_PCM_Reader(1))
8127
8128                self.assertIsNone(track.get_metadata())
8129                track.update_metadata(metadata)
8130                self.assertIsNotNone(track.get_metadata())
8131                track.delete_metadata()
8132                self.assertIsNone(track.get_metadata())
8133            finally:
8134                temp_file.close()
8135
8136    @METADATA_TTA
8137    def test_images(self):
8138        # images work like either WavPack or MP3
8139        # depending on which metadata is in place
8140
8141        for metadata in self.__base_metadatas__():
8142            temp_file = tempfile.NamedTemporaryFile(
8143                suffix="." + audiotools.TrueAudio.SUFFIX)
8144            try:
8145                track = audiotools.TrueAudio.from_pcm(temp_file.name,
8146                                                      BLANK_PCM_Reader(1))
8147
8148                self.assertEqual(metadata.images(), [])
8149
8150                image1 = audiotools.Image.new(TEST_COVER1,
8151                                              u"Text 1", 0)
8152                image2 = audiotools.Image.new(TEST_COVER2,
8153                                              u"Text 2", 1)
8154
8155                track.set_metadata(metadata)
8156                metadata = track.get_metadata()
8157
8158                # ensure that adding one image works
8159                metadata.add_image(image1)
8160                track.set_metadata(metadata)
8161                metadata = track.get_metadata()
8162                self.assertEqual(metadata.images(), [image1])
8163
8164                # ensure that adding a second image works
8165                metadata.add_image(image2)
8166                track.set_metadata(metadata)
8167                metadata = track.get_metadata()
8168                self.assertEqual(metadata.images(), [image1,
8169                                                     image2])
8170
8171                # ensure that deleting the first image works
8172                metadata.delete_image(image1)
8173                track.set_metadata(metadata)
8174                metadata = track.get_metadata()
8175                self.assertEqual(metadata.images(), [image2])
8176
8177                metadata.delete_image(image2)
8178                track.set_metadata(metadata)
8179                metadata = track.get_metadata()
8180                self.assertEqual(metadata.images(), [])
8181
8182            finally:
8183                temp_file.close()
8184
8185    @METADATA_TTA
8186    def test_replay_gain(self):
8187        # adding ReplayGain converts internal MetaData to APEv2
8188        # but otherwise works like WavPack
8189
8190        import test_streams
8191        for metadata in self.__base_metadatas__():
8192            temp1 = tempfile.NamedTemporaryFile(
8193                suffix="." + audiotools.TrueAudio.SUFFIX)
8194            try:
8195                track1 = audiotools.TrueAudio.from_pcm(
8196                    temp1.name,
8197                    test_streams.Sine16_Stereo(44100, 44100,
8198                                               441.0, 0.50,
8199                                               4410.0, 0.49, 1.0))
8200                self.assertIsNone(
8201                    track1.get_replay_gain(),
8202                    "ReplayGain present for class %s" %
8203                    (audiotools.TrueAudio.NAME))
8204                track1.update_metadata(metadata)
8205                audiotools.add_replay_gain([track1])
8206                self.assertIsInstance(track1.get_metadata(), audiotools.ApeTag)
8207                self.assertEqual(track1.get_metadata().track_name,
8208                                 u"Track Name")
8209                self.assertIsNotNone(
8210                    track1.get_replay_gain(),
8211                    "ReplayGain not present for class %s" %
8212                    (audiotools.TrueAudio.NAME))
8213
8214                temp2 = tempfile.NamedTemporaryFile(
8215                    suffix="." + audiotools.TrueAudio.SUFFIX)
8216                try:
8217                    track2 = audiotools.TrueAudio.from_pcm(
8218                        temp2.name,
8219                        test_streams.Sine16_Stereo(66150, 44100,
8220                                                   8820.0, 0.70,
8221                                                   4410.0, 0.29, 1.0))
8222
8223                    # ensure that ReplayGain doesn't get ported
8224                    # via set_metadata()
8225                    self.assertIsNone(
8226                        track2.get_replay_gain(),
8227                        "ReplayGain present for class %s" %
8228                        (audiotools.TrueAudio.NAME))
8229                    track2.set_metadata(track1.get_metadata())
8230                    self.assertEqual(track2.get_metadata().track_name,
8231                                     u"Track Name")
8232                    self.assertIsNone(
8233                        track2.get_replay_gain(),
8234                        "ReplayGain present for class %s from %s" %
8235                        (audiotools.TrueAudio.NAME,
8236                         audiotools.TrueAudio.NAME))
8237
8238                    # and if ReplayGain is already set,
8239                    # ensure set_metadata() doesn't remove it
8240                    audiotools.add_replay_gain([track2])
8241                    old_replay_gain = track2.get_replay_gain()
8242                    self.assertIsNotNone(old_replay_gain)
8243                    track2.set_metadata(
8244                        audiotools.MetaData(track_name=u"Bar"))
8245                    self.assertEqual(track2.get_metadata().track_name,
8246                                     u"Bar")
8247                    self.assertEqual(track2.get_replay_gain(),
8248                                     old_replay_gain)
8249
8250                finally:
8251                    temp2.close()
8252            finally:
8253                temp1.close()
8254