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