1# This program is free software; you can redistribute it and/or modify 2# it under the terms of the GNU General Public License as published by 3# the Free Software Foundation; either version 2 of the License, or 4# (at your option) any later version. 5 6from tests import TestCase, get_data_path 7 8import os 9import io 10from contextlib import contextmanager 11from senf import fsnative, fsn2text, bytes2fsn, mkstemp, mkdtemp 12 13from quodlibet import config 14from quodlibet.formats import AudioFile, types as format_types, AudioFileError 15from quodlibet.formats._audio import NUMERIC_ZERO_DEFAULT 16from quodlibet.formats import decode_value, MusicFile, FILESYSTEM_TAGS 17from quodlibet.util.tags import _TAGS as TAGS 18from quodlibet.util.path import normalize_path, mkdir, get_home_dir, unquote, \ 19 escape_filename, RootPathFile 20from quodlibet.util.environment import is_windows 21 22from .helper import temp_filename 23 24 25bar_1_1 = AudioFile({ 26 "~filename": fsnative(u"/fakepath/1"), 27 "title": "A song", 28 "discnumber": "1/2", "tracknumber": "1/3", 29 "artist": "Foo", "album": "Bar"}) 30bar_1_2 = AudioFile({ 31 "~filename": fsnative(u"/fakepath/2"), 32 "title": "Perhaps another", 33 "titlesort": "Titles don't sort", 34 "discnumber": "1", "tracknumber": "2/3", 35 "artist": "Lali-ho!", "album": "Bar", 36 "date": "2004-12-12", "originaldate": "2005-01-01", 37 "~#filesize": 1024 ** 2, "~#bitrate": 128}) 38bar_2_1 = AudioFile({ 39 "~filename": fsnative(u"does not/exist"), 40 "title": "more songs", 41 "discnumber": "2/2", "tracknumber": "1", 42 "artist": "Foo\nI have two artists", 43 "artistsort": "Foosort\n\nThird artist", 44 "album": "Bar", 45 "lyricist": "Foo", "composer": "Foo", "performer": "I have two artists"}) 46bar_va = AudioFile({ 47 "~filename": "/fakepath/3", 48 "title": "latest", 49 "artist": "Foo\nI have two artists", 50 "album": "Bar", 51 "language": "de\neng", 52 "albumartist": "Various Artists", 53 "performer": "Jay-Z"}) 54 55num_call = AudioFile({"custom": "0.3"}) 56 57 58class TAudioFile(TestCase): 59 60 def setUp(self): 61 config.RATINGS = config.HardCodedRatingsPrefs() 62 fd, filename = mkstemp() 63 os.close(fd) 64 self.quux = AudioFile({ 65 "~filename": normalize_path(filename, True), 66 "album": u"Quuxly" 67 }) 68 69 def tearDown(self): 70 try: 71 os.unlink(self.quux["~filename"]) 72 except EnvironmentError: 73 pass 74 75 def test_format_type(self): 76 for t in format_types: 77 i = AudioFile.__new__(t) 78 assert isinstance(i("~format"), str) 79 80 def test_tag_strs(self): 81 for t in format_types: 82 i = AudioFile.__new__(t) 83 i["~filename"] = fsnative(u"foo") 84 for tag in TAGS.values(): 85 name = tag.name 86 # brute force 87 variants = [ 88 name, "~" + name, name + "sort", "~" + name + "sort", 89 name + ":role", "~" + name + ":role", 90 "~" + name + "sort:role", name + "sort:role", 91 ] 92 for name in variants: 93 if name in FILESYSTEM_TAGS: 94 assert isinstance(i(name, fsnative()), fsnative) 95 else: 96 assert isinstance(i(name), str) 97 98 def test_sort(self): 99 l = [self.quux, bar_1_2, bar_2_1, bar_1_1] 100 l.sort() 101 self.assertEqual(l, [bar_1_1, bar_1_2, bar_2_1, self.quux]) 102 self.assertEqual(self.quux, self.quux) 103 self.assertEqual(bar_1_1, bar_1_1) 104 self.assertNotEqual(bar_2_1, bar_1_2) 105 106 def test_realkeys(self): 107 self.failIf("artist" in self.quux.realkeys()) 108 self.failIf("~filename" in self.quux.realkeys()) 109 self.failUnless("album" in self.quux.realkeys()) 110 111 def test_iterrealitems(self): 112 af = AudioFile({ 113 "~filename": fsnative(u"foo"), 114 "album": u"Quuxly" 115 }) 116 assert list(af.iterrealitems()) == [('album', u'Quuxly')] 117 118 def test_language(self): 119 self.assertEqual(bar_va("~language"), "German\nEnglish") 120 self.assertEqual(bar_va.list("~language"), ['German', 'English']) 121 self.assertEqual(bar_1_1("~language", default="foo"), "foo") 122 self.assertEqual(bar_1_1.list("~language"), []) 123 124 def test_trackdisc(self): 125 self.failUnlessEqual(bar_1_1("~#track"), 1) 126 self.failUnlessEqual(bar_1_1("~#disc"), 1) 127 self.failUnlessEqual(bar_1_1("~#tracks"), 3) 128 self.failUnlessEqual(bar_1_1("~#discs"), 2) 129 self.failIf(bar_1_2("~#discs")) 130 self.failIf(bar_2_1("~#tracks")) 131 132 def test_setitem_keys(self): 133 af = AudioFile() 134 af[u"foo"] = u"bar" 135 assert "foo" in af 136 assert isinstance(list(af.keys())[0], str) 137 af.clear() 138 af[u"öäü"] = u"bar" 139 assert u"öäü" in af 140 assert isinstance(list(af.keys())[0], str) 141 142 with self.assertRaises(TypeError): 143 af[42] = u"foo" 144 145 with self.assertRaises(TypeError): 146 af[b"foo"] = u"bar" 147 148 def test_call(self): 149 # real keys should lookup the same 150 for key in bar_1_1.realkeys(): 151 self.failUnlessEqual(bar_1_1[key], bar_1_1(key)) 152 153 # fake/generated key checks 154 af = AudioFile() 155 self.failIf(af("not a key")) 156 self.failUnlessEqual(af("not a key", "foo"), "foo") 157 self.failUnlessEqual(af("artist"), "") 158 159 assert self.quux("~basename") 160 assert self.quux("~dirname") == os.path.dirname(self.quux("~filename")) 161 assert self.quux("title") == \ 162 "%s [Unknown]" % fsn2text(self.quux("~basename")) 163 164 self.failUnlessEqual(bar_1_1("~#disc"), 1) 165 self.failUnlessEqual(bar_1_2("~#disc"), 1) 166 self.failUnlessEqual(bar_2_1("~#disc"), 2) 167 self.failUnlessEqual(bar_1_1("~#track"), 1) 168 self.failUnlessEqual(bar_1_2("~#track"), 2) 169 self.failUnlessEqual(bar_2_1("~#track"), 1) 170 171 def test_year(self): 172 self.failUnlessEqual(bar_1_2("~year"), "2004") 173 self.failUnlessEqual(bar_1_2("~#year"), 2004) 174 self.failUnlessEqual(bar_1_1("~#year", 1999), 1999) 175 176 def test_filesize(self): 177 self.failUnlessEqual(bar_1_2("~filesize"), "1.00 MB") 178 self.failUnlessEqual(bar_1_2("~#filesize"), 1024 ** 2) 179 assert isinstance(bar_1_2("~filesize"), str) 180 181 def test_bitrate(self): 182 self.assertEqual(bar_1_2("~#bitrate"), 128) 183 self.assertEqual(bar_1_2("~bitrate"), "128 kbps") 184 185 def test_originalyear(self): 186 self.failUnlessEqual(bar_1_2("~originalyear"), "2005") 187 self.failUnlessEqual(bar_1_2("~#originalyear"), 2005) 188 self.failUnlessEqual(bar_1_1("~#originalyear", 1999), 1999) 189 190 def test_call_people(self): 191 af = AudioFile() 192 self.failUnlessEqual(af("~people"), "") 193 self.failUnlessEqual(bar_1_1("~people"), "Foo") 194 self.failUnlessEqual(bar_1_2("~people"), "Lali-ho!") 195 self.failUnlessEqual(bar_2_1("~people"), "Foo\nI have two artists") 196 # See Issue 1034 197 self.failUnlessEqual(bar_va("~people"), 198 "Foo\nI have two artists\nVarious Artists\nJay-Z") 199 200 def test_call_multiple(self): 201 for song in [self.quux, bar_1_1, bar_2_1]: 202 self.failUnlessEqual(song("~~people"), song("~people")) 203 self.failUnlessEqual(song("~title~people"), song("title")) 204 self.failUnlessEqual( 205 song("~title~~people"), song("~title~artist")) 206 207 def test_tied_filename_numeric(self): 208 self.assertEqual( 209 bar_1_2("~~filename~~#originalyear"), u'/fakepath/2 - 2005') 210 211 def test_call_numeric(self): 212 self.failUnlessAlmostEqual(num_call("~#custom"), 0.3) 213 self.failUnlessEqual(num_call("~#blah~foo", 0), 0) 214 215 def test_list(self): 216 for key in bar_1_1.realkeys(): 217 self.failUnlessEqual(bar_1_1.list(key), [bar_1_1(key)]) 218 219 af = AudioFile({"~filename": fsnative(u"foo")}) 220 self.failUnlessEqual(af.list("artist"), []) 221 self.failUnlessEqual(af.list("title"), [af("title")]) 222 self.failUnlessEqual(af.list("not a key"), []) 223 224 self.failUnlessEqual(len(bar_2_1.list("artist")), 2) 225 self.failUnlessEqual(bar_2_1.list("artist"), 226 bar_2_1["artist"].split("\n")) 227 228 def test_list_tied_tags(self): 229 expected = ["%s - %s" % (bar_1_1("artist"), bar_1_1("title"))] 230 self.failUnlessEqual(bar_1_1.list("~artist~title"), expected) 231 232 def test_list_multiple_tied_tags(self): 233 expected = ["%s - %s" % (bar_2_1.comma("artist"), bar_2_1("title"))] 234 self.failUnlessEqual(bar_2_1.list("~artist~title"), expected) 235 236 def test_list_sort(self): 237 self.failUnlessEqual(bar_1_1.list_sort("title"), 238 [("A song", "A song")]) 239 self.failUnlessEqual(bar_1_1.list_sort("artist"), 240 [("Foo", "Foo")]) 241 242 af = AudioFile({"~filename": fsnative(u"foo")}) 243 self.failUnlessEqual(af.list_sort("artist"), []) 244 self.failUnlessEqual(af.list_sort("title"), 245 [(af("title"), af("title"))]) 246 self.failUnlessEqual(af.list_sort("not a key"), []) 247 248 self.failUnlessEqual(bar_1_2.list_sort("title"), 249 [("Perhaps another", "Perhaps another")]) 250 self.failUnlessEqual(bar_2_1.list_sort("artist"), 251 [("Foo", "Foosort"), 252 ("I have two artists", "I have two artists")]) 253 self.failUnlessEqual(bar_2_1.list_sort("~#track"), 254 [('1', '1')]) 255 256 def test_list_sort_empty_sort(self): 257 # we don't want to care about empty sort values, make sure we ignore 258 # them 259 s = AudioFile({"artist": "x\ny\nz", "artistsort": "c\n\nd"}) 260 self.assertEqual( 261 s.list_sort("artist"), [("x", "c"), ("y", "y"), ("z", "d")]) 262 263 def test_list_sort_noexist(self): 264 self.failUnlessEqual(bar_1_1.list_sort("nopenopenope"), []) 265 266 def test_list_separate_noexist(self): 267 self.failUnlessEqual(bar_1_1.list_separate("nopenopenope"), []) 268 269 def test_list_sort_length_diff(self): 270 s = AudioFile({"artist": "a\nb", "artistsort": "c"}) 271 self.assertEqual(s.list_sort("artist"), [("a", "c"), ("b", "b")]) 272 273 s = AudioFile({"artist": "a\nb", "artistsort": "c\nd\ne"}) 274 self.assertEqual(s.list_sort("artist"), [("a", "c"), ("b", "d")]) 275 276 s = AudioFile({"artistsort": "c\nd\ne"}) 277 self.assertEqual(s.list_sort("artist"), []) 278 279 s = AudioFile({"artist": "a\nb"}) 280 self.assertEqual(s.list_sort("artist"), [("a", "a"), ("b", "b")]) 281 282 s = AudioFile({}) 283 self.assertEqual(s.list_sort("artist"), []) 284 285 def test_list_separate(self): 286 self.failUnlessEqual(bar_1_1.list_separate("title"), 287 [("A song", "A song")]) 288 self.failUnlessEqual(bar_1_1.list_separate("artist"), 289 [("Foo", "Foo")]) 290 291 self.failUnlessEqual(bar_2_1.list_separate("~artist~album"), 292 [('Foo', 'Foosort'), 293 ('I have two artists', 'I have two artists'), 294 ('Bar', 'Bar')]) 295 296 self.failUnlessEqual(bar_2_1.list_separate("~artist~~#track"), 297 [('Foo', 'Foosort'), 298 ('I have two artists', 'I have two artists'), 299 ('1', '1')]) 300 301 def test_list_list_separate_types(self): 302 res = bar_2_1.list_separate("~~#track~artist~~filename") 303 self.assertEqual(res, [(u'1', u'1'), (u'Foo', u'Foosort'), 304 (u'I have two artists', u'I have two artists'), 305 (u'does not/exist', u'does not/exist')]) 306 307 def test_list_numeric(self): 308 self.assertEqual(bar_1_2.list('~#bitrate'), [128]) 309 310 def test_comma(self): 311 for key in bar_1_1.realkeys(): 312 self.failUnlessEqual(bar_1_1.comma(key), bar_1_1(key)) 313 self.failUnless(", " in bar_2_1.comma("artist")) 314 315 def test_comma_filename(self): 316 self.assertTrue(isinstance(bar_1_1.comma("~filename"), str)) 317 318 def test_comma_mountpoint(self): 319 assert not bar_1_1("~mountpoint") 320 assert isinstance(bar_1_1.comma("~mountpoint"), str) 321 assert bar_1_1.comma("~mountpoint") == u"" 322 323 def test_exist(self): 324 self.failIf(bar_2_1.exists()) 325 self.failUnless(self.quux.exists()) 326 327 def test_valid(self): 328 self.failIf(bar_2_1.valid()) 329 330 quux = self.quux 331 quux["~#mtime"] = 0 332 self.failIf(quux.valid()) 333 quux["~#mtime"] = os.path.getmtime(quux["~filename"]) 334 self.failUnless(quux.valid()) 335 os.utime(quux["~filename"], (quux["~#mtime"], quux["~#mtime"] - 1)) 336 self.failIf(quux.valid()) 337 quux["~#mtime"] = os.path.getmtime(quux["~filename"]) 338 self.failUnless(quux.valid()) 339 340 os.utime(quux["~filename"], (quux["~#mtime"], quux["~#mtime"] - 1)) 341 quux.sanitize() 342 self.failUnless(quux.valid()) 343 344 def test_can_change(self): 345 af = AudioFile() 346 self.failIf(af.can_change("~foobar")) 347 self.failIf(af.can_change("=foobar")) 348 self.failIf(af.can_change("foo=bar")) 349 self.failIf(af.can_change("")) 350 self.failUnless(af.can_change("foo bar")) 351 352 def test_is_writable(self): 353 self.assertTrue(self.quux.is_writable()) 354 os.chmod(self.quux["~filename"], 0o444) 355 self.assertFalse(self.quux.is_writable()) 356 os.chmod(self.quux["~filename"], 0o644) 357 self.assertTrue(self.quux.is_writable()) 358 359 def test_can_multiple_values(self): 360 af = AudioFile() 361 self.assertEqual(af.can_multiple_values(), True) 362 self.assertTrue(af.can_multiple_values("artist")) 363 364 def test_rename(self): 365 old_fn = self.quux["~filename"] 366 367 fd, new_fn = mkstemp() 368 os.close(fd) 369 os.unlink(new_fn) 370 371 assert self.quux.exists() 372 self.quux.rename(new_fn) 373 assert not os.path.exists(old_fn) 374 assert self.quux.exists() 375 self.quux.rename(old_fn) 376 assert not os.path.exists(new_fn) 377 assert self.quux.exists() 378 379 def test_rename_other_dir(self): 380 old_fn = self.quux["~filename"] 381 new_dir = mkdtemp() 382 self.quux.rename(os.path.join(new_dir, "foo")) 383 assert not os.path.exists(old_fn) 384 assert self.quux.exists() 385 self.quux.rename(old_fn) 386 assert self.quux.exists() 387 os.rmdir(new_dir) 388 389 def test_rename_to_existing(self): 390 self.quux.rename(self.quux("~filename")) 391 if os.name != "nt": 392 self.failUnlessRaises( 393 ValueError, self.quux.rename, fsnative(u"/dev/null")) 394 395 with temp_filename() as new_file: 396 with self.assertRaises(ValueError): 397 self.quux.rename(new_file) 398 399 def test_lyric_filename(self): 400 song = AudioFile() 401 song["~filename"] = fsnative(u"filename") 402 self.assertTrue(isinstance(song.lyric_filename, fsnative)) 403 song["title"] = u"Title" 404 song["artist"] = u"Artist" 405 self.assertTrue(isinstance(song.lyric_filename, fsnative)) 406 song["lyricist"] = u"Lyricist" 407 self.assertTrue(isinstance(song.lyric_filename, fsnative)) 408 409 def lyric_filename_search_test_song(self, pathfile): 410 s = AudioFile() 411 s.sanitize(pathfile) 412 s['artist'] = "SpongeBob SquarePants" 413 s['title'] = "Theme Tune" 414 return s 415 416 @contextmanager 417 def lyric_filename_test_setup(self, no_config=False): 418 419 with temp_filename() as filename: 420 s = self.lyric_filename_search_test_song(filename) 421 root = os.path.dirname(filename) 422 423 if not no_config: 424 config.set("memory", "lyric_filenames", 425 "<artist>.-.<title>,<artist> - <title>.lyrics_mod") 426 config.set("memory", "lyric_rootpaths", root) 427 428 s.root = root 429 yield s 430 431 if not no_config: 432 self.lyric_filename_search_clean_config() 433 434 def lyric_filename_search_clean_config(self): 435 """reset config to ensure other tests aren't affected""" 436 config.remove_option("memory", "lyric_rootpaths") 437 config.remove_option("memory", "lyric_filenames") 438 439 def test_lyric_filename_search_builtin_default(self): 440 """test built-in default""" 441 with self.lyric_filename_test_setup(no_config=True) as ts: 442 fp = os.path.join(ts.root, ts["artist"], ts["title"] + ".lyric") 443 p = os.path.dirname(fp) 444 mkdir(p) 445 with io.open(fp, "w", encoding='utf-8') as f: 446 f.write(u"") 447 search = unquote(ts.lyric_filename) 448 os.remove(fp) 449 os.rmdir(p) 450 self.assertEqual(search, fp) 451 452 def test_lyric_filename_search_builtin_default_local_path(self): 453 """test built-in default local path""" 454 with self.lyric_filename_test_setup(no_config=True) as ts: 455 fp = os.path.join(ts.root, ts["artist"] + " - " + 456 ts["title"] + ".lyric") 457 with io.open(fp, "w", encoding='utf-8') as f: 458 f.write(u"") 459 search = ts.lyric_filename 460 os.remove(fp) 461 if is_windows(): 462 fp = fp.lower() # account for 'os.path.normcase' santisatation 463 search = search.lower() # compensate for the above 464 self.assertEqual(search, fp) 465 466 def test_lyric_filename_search_file_not_found(self): 467 """test default file not found fallback""" 468 with self.lyric_filename_test_setup() as ts: 469 fp = os.path.join(ts.root, ts["artist"] + ".-." + ts["title"]) 470 search = unquote(ts.lyric_filename) 471 self.assertEqual(search, fp) 472 473 def test_lyric_filename_search_custom_path(self): 474 """test custom lyrics file location / naming""" 475 with self.lyric_filename_test_setup() as ts: 476 fp = os.path.join(ts.root, ts["artist"] + " - " + 477 ts["title"] + ".lyric") 478 with io.open(fp, "w", encoding='utf-8') as f: 479 f.write(u"") 480 search = ts.lyric_filename 481 os.remove(fp) 482 self.assertEqual(search, fp) 483 484 def test_lyric_filename_search_order_priority(self): 485 """test custom lyrics order priority""" 486 with self.lyric_filename_test_setup() as ts: 487 root2 = os.path.join(get_home_dir(), ".lyrics") # built-in default 488 fp2 = os.path.join(root2, ts["artist"] + " - " + 489 ts["title"] + ".lyric") 490 p2 = os.path.dirname(fp2) 491 mkdir(p2) 492 with io.open(fp2, "w", encoding='utf-8') as f: 493 f.write(u"") 494 fp = os.path.join(ts.root, ts["artist"] + " - " + 495 ts["title"] + ".lyric") 496 with io.open(fp, "w", encoding='utf-8') as f: 497 f.write(u"") 498 mkdir(p2) 499 search = ts.lyric_filename 500 os.remove(fp2) 501 os.rmdir(p2) 502 os.remove(fp) 503 self.assertEqual(search, fp) 504 505 def test_lyric_filename_search_modified_extension_fallback(self): 506 """test modified extension fallback search""" 507 with self.lyric_filename_test_setup() as ts: 508 fp = os.path.join(ts.root, 509 ts["artist"] + " - " + ts["title"] + ".txt") 510 with io.open(fp, "w", encoding='utf-8') as f: 511 f.write(u"") 512 search = ts.lyric_filename 513 os.remove(fp) 514 self.assertEqual(search, fp) 515 516 def test_lyric_filename_search_special_characters(self): 517 """test '<' and/or '>' in name (not parsed (transparent to test))""" 518 with self.lyric_filename_test_setup(no_config=True) as ts: 519 520 path_variants = ['<oldskool>'] \ 521 if is_windows() else [r'\<artist\>', r'\<artist>', 522 r'<artist\>'] 523 524 for path_variant in path_variants: 525 ts['artist'] = path_variant + " SpongeBob SquarePants" 526 parts = [ts.root, 527 ts["artist"] + " - " + ts["title"] + ".lyric"] 528 rpf = RootPathFile(ts.root, os.path.sep.join(parts)) 529 if not rpf.valid: 530 rpf = RootPathFile(rpf.root, rpf.pathfile_escaped) 531 self.assertTrue(rpf.valid, 532 "even escaped target file is not valid") 533 with io.open(rpf.pathfile, "w", encoding='utf-8') as f: 534 f.write(u"") 535 search = ts.lyric_filename 536 os.remove(rpf.pathfile) 537 fp = rpf.pathfile 538 if is_windows(): 539 # account for 'os.path.normcase' santisatation 540 fp = fp.lower() 541 search = search.lower() # compensate for the above 542 self.assertEqual(search, fp) 543 544 def test_lyric_filename_search_special_characters_across_path(self): 545 """test '<' and/or '>' in name across path separator (not parsed 546 (transparent to test))""" 547 with self.lyric_filename_test_setup(no_config=True) as ts: 548 # test '<' and '>' in name across path 549 # (not parsed (transparent to test)) 550 ts['artist'] = "a < b" 551 ts['title'] = "b > a" 552 parts = [ts.root, ts["artist"], ts["title"] + ".lyric"] 553 rpf = RootPathFile(ts.root, os.path.sep.join(parts)) 554 rootp = ts.root 555 rmdirs = [] 556 # ensure valid dir existence 557 for p in rpf.end.split(os.path.sep)[:-1]: 558 rootp = os.path.sep.join([ts.root, p]) 559 if not RootPathFile(ts.root, rootp).valid: 560 rootp = os.path.sep.join([ts.root, escape_filename(p)]) 561 self.assertTrue(RootPathFile(ts.root, rootp).valid, 562 "even escaped target dir part is not valid!") 563 if not os.path.exists(rootp): 564 mkdir(rootp) 565 rmdirs.append(rootp) 566 567 if not rpf.valid: 568 rpf = RootPathFile(rpf.root, rpf.pathfile_escaped) 569 570 with io.open(rpf.pathfile, "w", encoding='utf-8') as f: 571 f.write(u"") 572 # search for lyric file 573 search = ts.lyric_filename 574 # clean up test lyric file / path 575 os.remove(rpf.pathfile) 576 for p in rmdirs: 577 os.rmdir(p) 578 # test whether the 'found' file is the test lyric file 579 fp = rpf.pathfile 580 if is_windows(): 581 fp = fp.lower() # account for 'os.path.normcase' santisatation 582 search = search.lower() # compensate for the above 583 self.assertEqual(search, fp) 584 585 def test_lyrics_from_file(self): 586 with temp_filename() as filename: 587 af = AudioFile(artist='Motörhead', title='this: again') 588 af.sanitize(filename) 589 lyrics = "blah!\nblasé \n" 590 lyrics_dir = os.path.dirname(af.lyric_filename) 591 mkdir(lyrics_dir) 592 with io.open(af.lyric_filename, "w", encoding='utf-8') as lf: 593 lf.write(str(lyrics)) 594 self.failUnlessEqual(af("~lyrics").splitlines(), 595 lyrics.splitlines()) 596 os.remove(af.lyric_filename) 597 os.rmdir(lyrics_dir) 598 599 def test_unsynced_lyrics(self): 600 song = AudioFile() 601 song["unsyncedlyrics"] = "lala" 602 assert song("~lyrics") == "lala" 603 assert song("unsyncedlyrics") == "lala" 604 assert song("lyrics") != "lala" 605 606 def test_mountpoint(self): 607 song = AudioFile() 608 song["~filename"] = fsnative(u"filename") 609 song.sanitize() 610 assert isinstance(song["~mountpoint"], fsnative) 611 assert isinstance(song.comma("~mointpoint"), str) 612 613 def test_sanitize(self): 614 q = AudioFile(self.quux) 615 b = AudioFile(bar_1_1) 616 q.sanitize() 617 b.pop('~filename') 618 self.failUnlessRaises(ValueError, b.sanitize) 619 n = AudioFile({"artist": u"foo\0bar", "title": u"baz\0", 620 "~filename": fsnative(u"whatever")}) 621 n.sanitize() 622 self.failUnlessEqual(n["artist"], "foo\nbar") 623 self.failUnlessEqual(n["title"], "baz") 624 625 def test_performers(self): 626 q = AudioFile([("performer:vocals", "A"), ("performer:guitar", "B"), 627 ("performer", "C")]) 628 self.failUnlessEqual(set(q.list("~performers")), {"A", "B", "C"}) 629 self.failUnlessEqual(set(q.list("~performers:roles")), 630 {"A (Vocals)", "B (Guitar)", "C"}) 631 632 def test_performers_multi_value(self): 633 q = AudioFile([ 634 ("performer:vocals", "X\nA\nY"), 635 ("performer:guitar", "Y\nB\nA"), 636 ("performer", "C\nB\nA"), 637 ]) 638 639 self.failUnlessEqual( 640 set(q.list("~performer")), {"A", "B", "C", "X", "Y"}) 641 642 self.failUnlessEqual( 643 set(q.list("~performer:roles")), { 644 "A (Guitar, Vocals)", 645 "C", 646 "B (Guitar)", 647 "X (Vocals)", 648 "Y (Guitar, Vocals)", 649 }) 650 651 def test_people(self): 652 q = AudioFile([("performer:vocals", "A"), ("performer:guitar", "B"), 653 ("performer", "C"), ("arranger", "A"), 654 ("albumartist", "B"), ("artist", "C")]) 655 self.failUnlessEqual(q.list("~people"), ["C", "B", "A"]) 656 self.failUnlessEqual(q.list("~people:roles"), 657 ["C (Performance)", "B (Guitar)", "A (Arrangement, Vocals)"]) 658 659 def test_people_mix(self): 660 q = AudioFile([ 661 ("performer:arrangement", "A"), 662 ("arranger", "A"), 663 ("performer", "A"), 664 ("performer:foo", "A"), 665 ]) 666 self.failUnlessEqual(q.list("~people"), ["A"]) 667 self.failUnlessEqual(q.list("~people:roles"), 668 ["A (Arrangement, Arrangement, Foo, Performance)"]) 669 670 def test_people_multi_value(self): 671 q = AudioFile([ 672 ("arranger", "A\nX"), 673 ("performer", "A\nY"), 674 ("performer:foo", "A\nX"), 675 ]) 676 677 self.failUnlessEqual(q.list("~people"), ["A", "Y", "X"]) 678 self.failUnlessEqual(q.list("~people:roles"), 679 ["A (Arrangement, Foo, Performance)", "Y (Performance)", 680 "X (Arrangement, Foo)"]) 681 682 def test_people_individuals(self): 683 q = AudioFile({"artist": "A\nX", "albumartist": "Various Artists"}) 684 self.failUnlessEqual(q.list("~people:real"), ["A", "X"]) 685 686 lonely = AudioFile({"artist": "various artists", "title": "blah"}) 687 self.failUnlessEqual(lonely.list("~people:real"), 688 ["various artists"]) 689 690 lots = AudioFile({"artist": "Various Artists", "albumartist": "V.A."}) 691 self.failUnlessEqual(lots.list("~people:real"), 692 ["Various Artists"]) 693 694 def test_peoplesort(self): 695 q = AudioFile([("performer:vocals", "The A"), 696 ("performersort:vocals", "A, The"), 697 ("performer:guitar", "The B"), 698 ("performersort:guitar", "B, The"), 699 ("performer", "The C"), 700 ("performersort", "C, The"), 701 ("albumartist", "The B"), 702 ("albumartistsort", "B, The")]) 703 self.failUnlessEqual(q.list("~peoplesort"), 704 ["B, The", "C, The", "A, The"]) 705 self.failUnlessEqual(q.list("~peoplesort:roles"), 706 ["B, The (Guitar)", "C, The (Performance)", "A, The (Vocals)"]) 707 708 def test_to_dump(self): 709 dump = bar_1_1.to_dump() 710 num = len(set(bar_1_1.keys()) | NUMERIC_ZERO_DEFAULT) 711 self.failUnlessEqual(dump.count(b"\n"), num + 2) 712 for key, value in bar_1_1.items(): 713 self.failUnless(key.encode("utf-8") in dump) 714 self.failUnless(value.encode("utf-8") in dump) 715 for key in NUMERIC_ZERO_DEFAULT: 716 self.failUnless(key.encode("utf-8") in dump) 717 718 n = AudioFile() 719 n.from_dump(dump) 720 self.failUnless( 721 set(dump.split(b"\n")) == set(n.to_dump().split(b"\n"))) 722 723 def test_to_dump_unicode(self): 724 b = AudioFile(bar_1_1) 725 b[u"öäü"] = u"öäü" 726 dump = b.to_dump() 727 n = AudioFile() 728 n.from_dump(dump) 729 self.assertEqual(n[u"öäü"], u"öäü") 730 731 def test_add(self): 732 song = AudioFile() 733 self.failIf("foo" in song) 734 song.add("foo", "bar") 735 self.failUnlessEqual(song["foo"], "bar") 736 song.add("foo", "another") 737 self.failUnlessEqual(song.list("foo"), ["bar", "another"]) 738 739 def test_remove(self): 740 song = AudioFile() 741 song.add("foo", "bar") 742 song.add("foo", "another") 743 song.add("foo", "one more") 744 song.remove("foo", "another") 745 self.failUnlessEqual(song.list("foo"), ["bar", "one more"]) 746 song.remove("foo", "bar") 747 self.failUnlessEqual(song.list("foo"), ["one more"]) 748 song.remove("foo", "one more") 749 self.failIf("foo" in song) 750 751 def test_remove_unknown(self): 752 song = AudioFile() 753 song.add("foo", "bar") 754 song.remove("foo", "not in list") 755 song.remove("nope") 756 self.failUnlessEqual(song.list("foo"), ["bar"]) 757 758 def test_remove_all(self): 759 song = AudioFile() 760 song.add("foo", "bar") 761 song.add("foo", "another") 762 song.add("foo", "one more") 763 song.remove("foo") 764 self.assertFalse("foo" in song) 765 766 def test_remove_empty(self): 767 song = AudioFile() 768 song.add("foo", u"") 769 song.remove("foo", u"") 770 self.assertFalse("foo" in song) 771 772 def test_change(self): 773 song = AudioFile() 774 song.add("foo", "bar") 775 song.add("foo", "another") 776 song.change("foo", "bar", "one more") 777 self.failUnlessEqual(song.list("foo"), ["one more", "another"]) 778 song.change("foo", "does not exist", "finally") 779 self.failUnlessEqual(song["foo"], "finally") 780 song.change("foo", "finally", "we're done") 781 self.failUnlessEqual(song["foo"], "we're done") 782 783 def test_bookmarks_none(self): 784 self.failUnlessEqual([], AudioFile().bookmarks) 785 786 def test_bookmarks_simple(self): 787 af = AudioFile({"~bookmark": "1:20 Mark 1"}) 788 self.failUnlessEqual([(80, "Mark 1")], af.bookmarks) 789 790 def test_bookmarks_two(self): 791 af = AudioFile({"~bookmark": "1:40 Mark 2\n1:20 Mark 1"}) 792 self.failUnlessEqual([(80, "Mark 1"), (100, "Mark 2")], af.bookmarks) 793 794 def test_bookmark_invalid(self): 795 af = AudioFile({"~bookmark": ("Not Valid\n1:40 Mark 2\n" 796 "-20 Not Valid 2\n1:20 Mark 1")}) 797 self.failUnlessEqual( 798 [(80, "Mark 1"), (100, "Mark 2"), (-1, "Not Valid"), 799 (-1, "-20 Not Valid 2")], af.bookmarks) 800 801 def test_set_bookmarks_none(self): 802 af = AudioFile({"bookmark": "foo"}) 803 af.bookmarks = [] 804 self.failUnlessEqual([], AudioFile().bookmarks) 805 self.failIf("~bookmark" in af) 806 807 def test_set_bookmarks_simple(self): 808 af = AudioFile() 809 af.bookmarks = [(120, "A mark"), (140, "Mark twain")] 810 self.failUnlessEqual(af["~bookmark"], "2:00 A mark\n2:20 Mark twain") 811 812 def test_set_bookmarks_invalid_value(self): 813 self.failUnlessRaises( 814 ValueError, setattr, AudioFile(), 'bookmarks', "huh?") 815 816 def test_set_bookmarks_invalid_time(self): 817 self.failUnlessRaises( 818 TypeError, setattr, AudioFile(), 'bookmarks', [("notint", "!")]) 819 820 def test_set_bookmarks_unrealistic_time(self): 821 self.failUnlessRaises( 822 ValueError, setattr, AudioFile(), 'bookmarks', [(-1, "!")]) 823 824 def test_has_rating(self): 825 song = AudioFile() 826 self.assertFalse(song.has_rating) 827 song["~#rating"] = 0.5 828 self.assertTrue(song.has_rating) 829 song.remove_rating() 830 self.assertFalse(song.has_rating) 831 832 def test_remove_rating(self): 833 song = AudioFile() 834 self.assertFalse(song.has_rating) 835 song.remove_rating() 836 self.assertFalse(song.has_rating) 837 song["~#rating"] = 0.5 838 self.assertTrue(song.has_rating) 839 song.remove_rating() 840 self.assertFalse(song.has_rating) 841 842 def test_album_key(self): 843 album_key_tests = [ 844 ({}, ((), (), '')), 845 ({'album': 'foo'}, (('foo',), (), '')), 846 ({'labelid': 'foo'}, ((), (), 'foo')), 847 ({'musicbrainz_albumid': 'foo'}, ((), (), 'foo')), 848 ({'album': 'foo', 'labelid': 'bar'}, (('foo',), (), 'bar')), 849 ({'album': 'foo', 'labelid': 'bar', 'musicbrainz_albumid': 'quux'}, 850 (('foo',), (), 'bar')), 851 ({'albumartist': 'a'}, ((), ('a',), '')), 852 ] 853 for tags, expected in album_key_tests: 854 afile = AudioFile(**tags) 855 afile.sanitize(fsnative(u'/dir/fn')) 856 self.failUnlessEqual(afile.album_key, expected) 857 858 def test_eq_ne(self): 859 self.failIf(AudioFile({"a": "b"}) == AudioFile({"a": "b"})) 860 self.failUnless(AudioFile({"a": "b"}) != AudioFile({"a": "b"})) 861 862 def test_invalid_fs_encoding(self): 863 # issue 798 864 a = AudioFile() 865 if os.name != "nt": 866 a["~filename"] = "/\xf6\xe4\xfc/\xf6\xe4\xfc.ogg" # latin 1 encoded 867 a.sort_by_func("~filename")(a) 868 a.sort_by_func("~basename")(a) 869 else: 870 # windows 871 a["~filename"] = \ 872 b"/\xf6\xe4\xfc/\xf6\xe4\xfc.ogg".decode("latin-1") 873 a.sort_by_func("~filename")(a) 874 a.sort_by_func("~basename")(a) 875 a.sort_by_func("~dirname")(a) 876 877 def test_sort_key_defaults(self): 878 AF = AudioFile 879 assert AF().sort_key == AF({"tracknumber": "0"}).sort_key 880 assert AF().sort_key != AF({"tracknumber": "1/1"}).sort_key 881 assert AF().sort_key < AF({"tracknumber": "2/2"}).sort_key 882 883 assert AF().sort_key == AF({"discnumber": "0"}).sort_key 884 assert AF().sort_key != AF({"discnumber": "1/1"}).sort_key 885 assert AF().sort_key < AF({"discnumber": "2/2"}).sort_key 886 887 def test_sort_cache(self): 888 copy = AudioFile(bar_1_1) 889 890 sort_1 = tuple(copy.sort_key) 891 copy["title"] = copy["title"] + "something" 892 sort_2 = tuple(copy.sort_key) 893 self.failIfEqual(sort_1, sort_2) 894 895 album_sort_1 = tuple(copy.album_key) 896 copy["album"] = copy["album"] + "something" 897 sort_3 = tuple(copy.sort_key) 898 self.failIfEqual(sort_2, sort_3) 899 900 album_sort_2 = tuple(copy.album_key) 901 self.failIfEqual(album_sort_1, album_sort_2) 902 903 def test_cache_attributes(self): 904 x = AudioFile() 905 x.multisong = not x.multisong 906 x["a"] = "b" # clears cache 907 # attribute should be unchanged 908 self.failIfEqual(AudioFile().multisong, x.multisong) 909 910 def test_sort_func(self): 911 tags = [lambda s: s("foo"), "artistsort", "albumsort", 912 "~filename", "~format", "discnumber", "~#track"] 913 914 for tag in tags: 915 f = AudioFile.sort_by_func(tag) 916 f(bar_1_1) 917 f(bar_1_2) 918 f(bar_2_1) 919 920 def test_sort_func_custom_numeric(self): 921 func = AudioFile.sort_by_func("~#year") 922 923 files = [AudioFile({"year": "nope"}), AudioFile({"date": "2038"})] 924 assert sorted(files, key=func) == files 925 926 def test_uri(self): 927 # On windows where we have unicode paths (windows encoding is utf-16) 928 # we need to encode to utf-8 first, then escape. 929 # On linux we take the byte stream and escape it. 930 # see g_filename_to_uri 931 932 if os.name == "nt": 933 f = AudioFile({"~filename": u"/\xf6\xe4.mp3", "title": "win"}) 934 self.failUnlessEqual(f("~uri"), "file:///%C3%B6%C3%A4.mp3") 935 else: 936 f = AudioFile({ 937 "~filename": bytes2fsn(b"/\x87\x12.mp3", None), 938 "title": "linux", 939 }) 940 self.failUnlessEqual(f("~uri"), "file:///%87%12.mp3") 941 942 def test_reload(self): 943 audio = MusicFile(get_data_path('silence-44-s.mp3')) 944 audio["title"] = u"foo" 945 audio.reload() 946 self.assertNotEqual(audio.get("title"), u"foo") 947 948 def test_reload_fail(self): 949 audio = MusicFile(get_data_path('silence-44-s.mp3')) 950 audio["title"] = u"foo" 951 audio.sanitize(fsnative(u"/dev/null")) 952 self.assertRaises(AudioFileError, audio.reload) 953 self.assertEqual(audio["title"], u"foo") 954 955 956class TAudioFormats(TestCase): 957 958 def setUp(self): 959 with temp_filename() as filename: 960 self.filename = filename 961 962 def test_load_non_exist(self): 963 for t in format_types: 964 if not t.is_file: 965 continue 966 self.assertRaises(AudioFileError, t, self.filename) 967 968 def test_write_non_existing(self): 969 for t in format_types: 970 if not t.is_file: 971 continue 972 instance = AudioFile.__new__(t) 973 instance.sanitize(self.filename) 974 try: 975 instance.write() 976 except AudioFileError: 977 pass 978 979 def test_reload_non_existing(self): 980 for t in format_types: 981 if not t.is_file: 982 continue 983 instance = AudioFile.__new__(t) 984 instance.sanitize(self.filename) 985 try: 986 instance.reload() 987 except AudioFileError: 988 pass 989 990 991class Tdecode_value(TestCase): 992 993 def test_main(self): 994 self.assertEqual(decode_value("~#foo", 0.25), u"0.25") 995 self.assertEqual(decode_value("~#foo", 4), u"4") 996 self.assertEqual(decode_value("~#foo", "bar"), u"bar") 997 self.assertTrue(isinstance(decode_value("~#foo", "bar"), str)) 998 path = fsnative(u"/foobar") 999 self.assertEqual(decode_value("~filename", path), fsn2text(path)) 1000 1001 def test_path(self): 1002 try: 1003 path = bytes2fsn(b"\xff\xff", "utf-8") 1004 except ValueError: 1005 return 1006 1007 assert decode_value("~filename", path) == fsn2text(path) 1008 1009 1010class Treplay_gain(TestCase): 1011 1012 # -6dB is approximately equal to half magnitude 1013 minus_6db = 0.501187234 1014 1015 def setUp(self): 1016 self.rg_data = {"replaygain_album_gain": "-1.00 dB", 1017 "replaygain_album_peak": "1.1", 1018 "replaygain_track_gain": "+1.0000001 dB", 1019 "replaygain_track_peak": "0.9"} 1020 self.song = AudioFile(self.rg_data) 1021 self.no_rg_song = AudioFile() 1022 1023 def test_large(self): 1024 rg_data = {"replaygain_track_gain": "9999999 dB"} 1025 song = AudioFile(rg_data) 1026 assert song.replay_gain(["track"], 0, 0) == 1.0 1027 assert song.replay_gain([], 0, 99999999999) == 1.0 1028 1029 def test_no_rg_song(self): 1030 scale = self.no_rg_song.replay_gain(["track"], 0, -6.0) 1031 self.failUnlessAlmostEqual(scale, self.minus_6db) 1032 1033 scale = self.no_rg_song.replay_gain(["track"], +10, +10) 1034 self.failUnlessEqual(scale, 1.0) 1035 1036 scale = self.no_rg_song.replay_gain(["track"], -16.0, +10) 1037 self.failUnlessAlmostEqual(scale, self.minus_6db) 1038 1039 def test_nogain(self): 1040 self.failUnlessEqual(self.song.replay_gain(["none", "track"]), 1) 1041 1042 def test_fallback_track(self): 1043 del(self.song["replaygain_track_gain"]) 1044 self.failUnlessAlmostEqual( 1045 self.song.replay_gain(["track"], 0, -6.0), self.minus_6db) 1046 1047 def test_fallback_album(self): 1048 del(self.song["replaygain_album_gain"]) 1049 self.failUnlessAlmostEqual( 1050 self.song.replay_gain(["album"], 0, -6.0), self.minus_6db) 1051 1052 def test_fallback_and_preamp(self): 1053 del(self.song["replaygain_track_gain"]) 1054 self.failUnlessEqual(self.song.replay_gain(["track"], 9, -9), 1) 1055 1056 def test_preamp_track(self): 1057 self.failUnlessAlmostEqual( 1058 self.song.replay_gain(["track"], -7.0, 0), self.minus_6db) 1059 1060 def test_preamp_album(self): 1061 self.failUnlessAlmostEqual( 1062 self.song.replay_gain(["album"], -5.0, 0), self.minus_6db) 1063 1064 def test_preamp_clip(self): 1065 # Make sure excess pre-amp won't clip a track (with peak data) 1066 self.failUnlessAlmostEqual( 1067 self.song.replay_gain(["track"], 12.0, 0), 1.0 / 0.9) 1068 1069 def test_trackgain(self): 1070 self.failUnless(self.song.replay_gain(["track"]) > 1) 1071 1072 def test_albumgain(self): 1073 self.failUnless(self.song.replay_gain(["album"]) < 1) 1074 1075 def test_invalid(self): 1076 self.song["replaygain_album_gain"] = "fdsodgbdf" 1077 self.failUnlessEqual(self.song.replay_gain(["album"]), 1) 1078 1079 def test_track_fallback(self): 1080 radio_rg = self.song.replay_gain(["track"]) 1081 del(self.song["replaygain_album_gain"]) 1082 del(self.song["replaygain_album_peak"]) 1083 # verify defaulting to track when album is present 1084 self.failUnlessAlmostEqual( 1085 self.song.replay_gain(["album", "track"]), radio_rg) 1086 1087 def test_numeric_rg_tags(self): 1088 """Tests fully-numeric (ie no "db") RG tags. See Issue 865""" 1089 self.failUnless(self.song("replaygain_album_gain"), "-1.00 db") 1090 for key, exp in self.rg_data.items(): 1091 # Hack the nasties off and produce the "real" expected value 1092 exp = float(exp.split(" ")[0]) 1093 # Compare as floats. Seems fairer. 1094 album_rg = self.song("~#%s" % key) 1095 try: 1096 val = float(album_rg) 1097 except ValueError: 1098 self.fail("Invalid %s returned: %s" % (key, album_rg)) 1099 self.failUnlessAlmostEqual( 1100 val, exp, places=5, 1101 msg="%s should be %s not %s" % (key, exp, val)) 1102