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
6import shutil
7import os
8from collections import defaultdict
9
10from senf import fsnative
11
12from quodlibet import config
13
14from tests import TestCase, mkdtemp
15from quodlibet.formats import AudioFile as Fakesong
16from quodlibet.formats._audio import NUMERIC_ZERO_DEFAULT, PEOPLE
17from quodlibet.util.collection import Album, Playlist, avg, bayesian_average, \
18    FileBackedPlaylist
19from quodlibet.library.libraries import FileLibrary
20from quodlibet.util import format_rating
21
22config.RATINGS = config.HardCodedRatingsPrefs()
23
24NUMERIC_SONGS = [
25    Fakesong({"~filename": fsnative(u"fake1-\xf0.mp3"),
26              "~#length": 4, "~#added": 5, "~#lastplayed": 1,
27              "~#bitrate": 200, "date": "100", "~#rating": 0.1,
28              "originaldate": "2004-01-01", "~#filesize": 101}),
29    Fakesong({"~filename": fsnative(u"fake2.mp3"),
30              "~#length": 7, "~#added": 7, "~#lastplayed": 88,
31              "~#bitrate": 220, "date": "99", "~#rating": 0.3,
32              "originaldate": "2002-01-01", "~#filesize": 202}),
33    Fakesong({"~filename": fsnative(u"fake3.mp3"),
34              "~#length": 1, "~#added": 3, "~#lastplayed": 43,
35              "~#bitrate": 60, "date": "33", "~#rating": 0.5,
36              "tracknumber": "4/6", "discnumber": "1/2"})
37]
38AMAZING_SONG = Fakesong({"~#length": 123, "~#rating": 1.0})
39
40
41class TAlbum(TestCase):
42    def setUp(self):
43        config.init()
44
45    def test_people_sort(s):
46        songs = [
47            Fakesong({"albumartist": "aa", "artist": "b\na"}),
48            Fakesong({"albumartist": "aa", "artist": "a\na"})
49        ]
50
51        album = Album(songs[0])
52        album.songs = set(songs)
53
54        s.failUnlessEqual(album.comma("~people"), "aa, a, b")
55
56    def test_peoplesort_sort(s):
57        songs = [
58            Fakesong({"albumartistsort": "aa", "artist": "b\na"}),
59            Fakesong({"albumartist": "aa", "artistsort": "a\na"})
60        ]
61
62        album = Album(songs[0])
63        album.songs = set(songs)
64
65        s.failUnlessEqual(album.comma("~peoplesort"), "aa, a, b")
66
67    def test_tied_tags(s):
68        songs = [
69            Fakesong({"artist": "a", "title": "c"}),
70            Fakesong({"artist": "a", "dummy": "d\ne"})
71        ]
72
73        album = Album(songs[0])
74        album.songs = set(songs)
75
76        s.failUnlessEqual(album.comma("~artist~dummy"), "a - d, e")
77
78    def test_tied_num_tags(s):
79        songs = [
80            Fakesong({"~#length": 5, "title": "c", "~#rating": 0.4}),
81            Fakesong({"~#length": 7, "dummy": "d\ne", "~#rating": 0.6}),
82            Fakesong({"~#length": 0, "dummy2": 5, "~#rating": 0.5})
83        ]
84
85        album = Album(songs[0])
86        album.songs = set(songs)
87
88        s.failUnlessEqual(album.comma("~foo~~s~~~"), "")
89        s.failUnlessEqual(album.comma("~#length~dummy"), "12 - d, e")
90        s.failUnlessEqual(album.comma("~#rating~dummy"), "0.50 - d, e")
91        s.failUnlessEqual(album.comma("~#length:sum~dummy"), "12 - d, e")
92        s.failUnlessEqual(album.comma("~#dummy2"), 5)
93        s.failUnlessEqual(album.comma("~#dummy3"), "")
94
95    def test_internal_tags(s):
96        songs = [
97            Fakesong({"~#length": 5, "discnumber": "1", "date": "2038"}),
98            Fakesong({"~#length": 7, "dummy": "d\ne", "discnumber": "2"})
99        ]
100
101        album = Album(songs[0])
102        album.songs = set(songs)
103
104        s.failIfEqual(album.comma("~long-length"), "")
105        s.failIfEqual(album.comma("~tracks"), "")
106        s.failIfEqual(album.comma("~discs"), "")
107        s.failUnlessEqual(album.comma("~foo"), "")
108
109        s.failUnlessEqual(album.comma(""), "")
110        s.failUnlessEqual(album.comma("~"), "")
111        s.failUnlessEqual(album.get("~#"), "")
112
113    def test_numeric_ops(s):
114        songs = NUMERIC_SONGS
115        album = Album(songs[0])
116        album.songs = set(songs)
117
118        s.failUnlessEqual(album.get("~#length"), 12)
119        s.failUnlessEqual(album.get("~#length:sum"), 12)
120        s.failUnlessEqual(album.get("~#length:max"), 7)
121        s.failUnlessEqual(album.get("~#length:min"), 1)
122        s.failUnlessEqual(album.get("~#length:avg"), 4)
123        s.failUnlessEqual(album.get("~#length:foo"), 0)
124
125        s.failUnlessEqual(album.get("~#added"), 7)
126        s.failUnlessEqual(album.get("~#lastplayed"), 88)
127        s.failUnlessEqual(album.get("~#bitrate"), 200)
128        s.failUnlessEqual(album.get("~#year"), 33)
129        s.failUnlessEqual(album.get("~#rating"), 0.3)
130        s.failUnlessEqual(album.get("~#originalyear"), 2002)
131
132    def test_numeric_comma(self):
133        songs = [Fakesong({
134            "~#added": 1,
135            "~#rating": 0.5,
136            "~#bitrate": 42,
137            "~#length": 1,
138        })]
139
140        album = Album(songs[0])
141        album.songs = set(songs)
142
143        self.assertEqual(album.comma("~#added"), 1)
144        self.assertEqual(album.comma("~#rating"), 0.5)
145        self.assertEqual(album.comma("~#bitrate"), 42)
146
147    def test_numeric_funcs_text(self):
148        songs = NUMERIC_SONGS
149        album = Album(songs[0])
150        album.songs = set(songs)
151
152        self.assertEqual(album("~length:sum"), "0:12")
153        self.assertEqual(album("~length:min"), "0:01")
154        self.assertEqual(album("~long-length:min"), "1 second")
155        self.assertEqual(album("~tracks:min"), "6 tracks")
156        self.assertEqual(album("~discs:min"), "2 discs")
157        self.assertEqual(album("~rating:min"), format_rating(0.1))
158        self.assertEqual(album("~filesize:min"), "0 B")
159
160    def test_single_rating(s):
161        songs = [Fakesong({"~#rating": 0.75})]
162        album = Album(songs[0])
163        album.songs = set(songs)
164        # One song should average to its own rating
165        s.failUnlessEqual(album.get("~#rating:avg"), songs[0]("~#rating"))
166        # BAV should now be default for rating
167        s.failUnlessEqual(album.get("~#rating:bav"), album.get("~#rating:avg"))
168
169    def test_multiple_ratings(s):
170        r1, r2 = 1.0, 0.5
171        songs = [Fakesong({"~#rating": r1}), Fakesong({"~#rating": r2})]
172        album = Album(songs[0])
173        album.songs = set(songs)
174        # Standard averaging still available
175        s.failUnlessEqual(album("~#rating:avg"), avg([r1, r2]))
176
177        # C = 0.0 => emulate arithmetic mean
178        config.set("settings", "bayesian_rating_factor", 0.0)
179        s.failUnlessEqual(album("~#rating:bav"), album("~#rating:avg"))
180
181    def test_bayesian_multiple_ratings(s):
182        # separated from above to avoid caching
183        c, r1, r2 = 5, 1.0, 0.5
184        songs = [Fakesong({"~#rating": r1}), Fakesong({"~#rating": r2})]
185        album = Album(songs[0])
186        album.songs = set(songs)
187
188        config.set("settings", "bayesian_rating_factor", float(c))
189        s.failUnlessEqual(
190            config.getfloat("settings", "bayesian_rating_factor"), float(c))
191        expected = avg(c * [config.RATINGS.default] + [r1, r2])
192        s.failUnlessEqual(album("~#rating:bav"), expected)
193        s.failUnlessEqual(album("~#rating"), expected)
194
195    def test_bayesian_average(s):
196        bav = bayesian_average
197        l = [1, 2, 3, 4]
198        a = avg(l)
199        # c=0 => this becomes a mean regardless of m
200        s.failUnlessEqual(bav(l, 0, 0), a)
201        s.failUnlessEqual(bav(l, 0, 999), a)
202        # c=1, m = a (i.e. just adding another mean score) => no effect
203        s.failUnlessEqual(bav(l, 1, a), a)
204        # Harder ones
205        s.failUnlessEqual(bav(l, 5, 2), 20.0 / 9)
206        expected = 40.0 / 14
207        s.failUnlessEqual(bav(l, 10, 3), expected)
208        # Also check another iterable
209        s.failUnlessEqual(bav(tuple(l), 10, 3), expected)
210
211    def test_defaults(s):
212        failUnlessEq = s.failUnlessEqual
213        song = Fakesong({})
214        album = Album(song)
215
216        failUnlessEq(album("foo", "x"), "x")
217
218        album.songs.add(song)
219
220        failUnlessEq(album("~#length", "x"), song("~#length", "x"))
221        failUnlessEq(album("~#bitrate", "x"), song("~#bitrate", "x"))
222        failUnlessEq(album("~#rating", "x"), song("~#rating", "x"))
223        failUnlessEq(album("~#playcount", "x"), song("~#playcount", "x"))
224        failUnlessEq(album("~#mtime", "x"), song("~#mtime", "x"))
225        failUnlessEq(album("~#year", "x"), song("~#year", "x"))
226
227        failUnlessEq(album("~#foo", "x"), song("~#foo", "x"))
228        failUnlessEq(album("foo", "x"), song("foo", "x"))
229        failUnlessEq(album("~foo", "x"), song("~foo", "x"))
230
231        failUnlessEq(album("~people", "x"), song("~people", "x"))
232        failUnlessEq(album("~peoplesort", "x"), song("~peoplesort", "x"))
233        failUnlessEq(album("~performer", "x"), song("~performer", "x"))
234        failUnlessEq(album("~performersort", "x"), song("~performersort", "x"))
235
236        failUnlessEq(album("~rating", "x"), song("~rating", "x"))
237
238        for p in PEOPLE:
239            failUnlessEq(album(p, "x"), song(p, "x"))
240
241        for p in NUMERIC_ZERO_DEFAULT:
242            failUnlessEq(album(p, "x"), song(p, "x"))
243
244    def test_methods(s):
245        songs = [
246            Fakesong({"b": "bb4\nbb1\nbb1",
247                      "c": "cc1\ncc3\ncc3",
248                      "#d": 0.1}),
249            Fakesong({"b": "bb1\nbb1\nbb4",
250                      "c": "cc3\ncc1\ncc3",
251                      "#d": 0.2})
252        ]
253
254        album = Album(songs[0])
255        album.songs = set(songs)
256
257        s.failUnlessEqual(album.list("c"), ["cc3", "cc1"])
258        s.failUnlessEqual(album.list("~c~b"), ["cc3", "cc1", "bb1", "bb4"])
259        s.failUnlessEqual(album.list("#d"), ["0.1", "0.2"])
260
261        s.failUnlessEqual(album.comma("c"), "cc3, cc1")
262        s.failUnlessEqual(album.comma("~c~b"), "cc3, cc1 - bb1, bb4")
263
264    def tearDown(self):
265        config.quit()
266
267
268class MockPlaylistResource(object):
269    def __init__(self, pl):
270        self.pl = pl
271
272    def __enter__(self):
273        return self.pl
274
275    def __exit__(self, *exc_info):
276        self.pl.delete()
277
278
279class TPlaylist(TestCase):
280    TWO_SONGS = [
281        Fakesong({"~#length": 5, "discnumber": "1", "date": "2038"}),
282        Fakesong({"~#length": 7, "dummy": "d\ne", "discnumber": "2"})
283    ]
284
285    class FakeLib(object):
286
287        def __init__(self):
288            self.reset()
289
290        def emit(self, name, songs):
291            self.emitted[name].extend(songs)
292
293        def masked(self, songs):
294            return False
295
296        def reset(self):
297            self.emitted = defaultdict(list)
298
299        @property
300        def changed(self):
301            return self.emitted.get('changed', [])
302
303    FAKE_LIB = FakeLib()
304
305    def setUp(self):
306        self.FAKE_LIB.reset()
307
308    def pl(self, name, lib=None):
309        return Playlist(name, lib)
310
311    def wrap(self, name, lib=FAKE_LIB):
312        return MockPlaylistResource(self.pl(name, lib))
313
314    def test_equality(s):
315        pl = s.pl("playlist")
316        pl2 = s.pl("playlist")
317        pl3 = s.pl("playlist")
318        s.failUnlessEqual(pl, pl2)
319        # Debatable
320        s.failUnlessEqual(pl, pl3)
321        pl4 = s.pl("foobar")
322        s.failIfEqual(pl, pl4)
323        pl.delete()
324        pl2.delete()
325        pl3.delete()
326        pl4.delete()
327
328    def test_index(s):
329        with s.wrap("playlist") as pl:
330            songs = s.TWO_SONGS
331            pl.extend(songs)
332            # Just a sanity check...
333            s.failUnlessEqual(songs.index(songs[1]), 1)
334            # And now the happy paths..
335            s.failUnlessEqual(pl.index(songs[0]), 0)
336            s.failUnlessEqual(pl.index(songs[1]), 1)
337            # ValueError is what we want here
338            try:
339                pl.index(Fakesong({}))
340                s.fail()
341            except ValueError:
342                pass
343
344    def test_name_tag(s):
345        with s.wrap("a playlist") as pl:
346            s.failUnlessEqual(pl("~name"), "a playlist")
347            s.failUnlessEqual(pl.get("~name"), "a playlist")
348
349    def test_internal_tags(s):
350        with s.wrap("playlist") as pl:
351            pl.extend(s.TWO_SONGS)
352
353            s.failIfEqual(pl.comma("~long-length"), "")
354            s.failIfEqual(pl.comma("~tracks"), "")
355            s.failIfEqual(pl.comma("~discs"), "")
356            s.failUnlessEqual(pl.comma("~foo"), "")
357
358            s.failUnlessEqual(pl.comma(""), "")
359            s.failUnlessEqual(pl.comma("~"), "")
360            s.failUnlessEqual(pl.get("~#"), "")
361
362    def test_numeric_ops(s):
363        songs = NUMERIC_SONGS
364        with s.wrap("playlist") as pl:
365            pl.extend(songs)
366
367            s.failUnlessEqual(pl.get("~#length"), 12)
368            s.failUnlessEqual(pl.get("~#length:sum"), 12)
369            s.failUnlessEqual(pl.get("~#length:max"), 7)
370            s.failUnlessEqual(pl.get("~#length:min"), 1)
371            s.failUnlessEqual(pl.get("~#length:avg"), 4)
372            s.failUnlessEqual(pl.get("~#length:foo"), 0)
373
374            s.failUnlessEqual(pl.get("~#rating:avg"), avg([0.1, 0.3, 0.5]))
375
376            s.failUnlessEqual(pl.get("~#filesize"), 303)
377
378            s.failUnlessEqual(pl.get("~#added"), 7)
379            s.failUnlessEqual(pl.get("~#lastplayed"), 88)
380            s.failUnlessEqual(pl.get("~#bitrate"), 200)
381            s.failUnlessEqual(pl.get("~#year"), 33)
382            s.failUnlessEqual(pl.get("~#rating"), 0.3)
383            s.failUnlessEqual(pl.get("~#originalyear"), 2002)
384
385    def test_updating_aggregates_extend(s):
386        with s.wrap("playlist") as pl:
387            pl.extend(NUMERIC_SONGS)
388            old_length = pl.get("~#length")
389            old_size = pl.get("~#filesize")
390
391            # Double the playlist
392            pl.extend(NUMERIC_SONGS)
393
394            new_length = pl.get("~#length")
395            new_size = pl.get("~#filesize")
396            s.failUnless(new_length > old_length,
397                         msg="Ooops, %d <= %d" % (new_length, old_length))
398
399            s.failUnless(new_size > old_size,
400                         msg="Ooops, %d <= %d" % (new_size, old_size))
401
402    def test_updating_aggregates_append(s):
403        with s.wrap("playlist") as pl:
404            pl.extend(NUMERIC_SONGS)
405            old_rating = pl.get("~#rating")
406
407            pl.append(AMAZING_SONG)
408
409            new_rating = pl.get("~#filesize")
410            s.failUnless(new_rating > old_rating)
411
412    def test_updating_aggregates_clear(s):
413        with s.wrap("playlist") as pl:
414            pl.extend(NUMERIC_SONGS)
415            s.failUnless(pl.get("~#length"))
416
417            pl.clear()
418            s.failIf(pl.get("~#length"))
419
420    def test_updating_aggregates_remove_songs(s):
421        with s.wrap("playlist") as pl:
422            pl.extend(NUMERIC_SONGS)
423            s.failUnless(pl.get("~#length"))
424
425            pl.remove_songs(NUMERIC_SONGS)
426            s.failIf(pl.get("~#length"))
427
428    def test_listlike(s):
429        with s.wrap("playlist") as pl:
430            pl.extend(NUMERIC_SONGS)
431            s.failUnlessEqual(NUMERIC_SONGS[0], pl[0])
432            s.failUnlessEqual(NUMERIC_SONGS[1:2], pl[1:2])
433            s.failUnless(NUMERIC_SONGS[1] in pl)
434
435    def test_extend_signals(s):
436        with s.wrap("playlist") as pl:
437            pl.extend(NUMERIC_SONGS)
438            s.failUnlessEqual(s.FAKE_LIB.changed, NUMERIC_SONGS)
439
440    def test_append_signals(s):
441        with s.wrap("playlist") as pl:
442            song = NUMERIC_SONGS[0]
443            pl.append(song)
444            s.failUnlessEqual(s.FAKE_LIB.changed, [song])
445
446    def test_clear_signals(s):
447        with s.wrap("playlist") as pl:
448            pl.extend(NUMERIC_SONGS)
449            pl.clear()
450            s.failUnlessEqual(s.FAKE_LIB.changed, NUMERIC_SONGS * 2)
451
452    def test_make(self):
453        with self.wrap("Does not exist") as pl:
454            self.failIf(len(pl))
455            self.failUnlessEqual(pl.name, "Does not exist")
456
457    def test_rename_working(self):
458        with self.wrap("Foobar") as pl:
459            assert pl.name == "Foobar"
460            pl.rename("Foo Quuxly")
461            assert pl.name == "Foo Quuxly"
462            # Rename should not fire signals
463            self.failIf(self.FAKE_LIB.changed)
464
465    def test_rename_nothing(self):
466        with self.wrap("Foobar") as pl:
467            self.failUnlessRaises(ValueError, pl.rename, "")
468
469    def test_no_op_rename(self):
470        with self.wrap("playlist") as pl:
471            pl.rename("playlist")
472            self.failUnlessEqual(pl.name, "playlist")
473
474    def test_playlists_featuring(s):
475        with s.wrap("playlist") as pl:
476            pl.extend(NUMERIC_SONGS)
477            playlists = Playlist.playlists_featuring(NUMERIC_SONGS[0])
478            s.failUnlessEqual(set(playlists), {pl})
479            # Now add a second one, check that instance tracking works
480            with s.wrap("playlist2") as pl2:
481                pl2.append(NUMERIC_SONGS[0])
482                playlists = Playlist.playlists_featuring(NUMERIC_SONGS[0])
483                s.failUnlessEqual(set(playlists), {pl, pl2})
484
485    def test_playlists_tag(self):
486        # Arguably belongs in _audio
487        songs = NUMERIC_SONGS
488        pl_name = "playlist 123!"
489        with self.wrap(pl_name) as pl:
490            pl.extend(songs)
491            for song in songs:
492                self.assertEquals(pl_name, song("~playlists"))
493
494    def test_duplicates_single_item(self):
495        with self.wrap("playlist") as pl:
496            pl.append(self.TWO_SONGS[0])
497            self.failIf(pl.has_duplicates)
498            pl.append(self.TWO_SONGS[0])
499            self.failUnless(pl.has_duplicates)
500
501    def test_duplicates(self):
502        with self.wrap("playlist") as pl:
503            pl.extend(self.TWO_SONGS)
504            pl.extend(self.TWO_SONGS)
505            self.failUnlessEqual(len(pl), 4)
506            self.failUnless(pl.has_duplicates,
507                            ("Playlist has un-detected duplicates: %s "
508                             % "\n".join([str(s) for s in pl._list])))
509
510    def test_remove_leaving_duplicates(self):
511        with self.wrap("playlist") as pl:
512            pl.extend(self.TWO_SONGS)
513            [first, second] = self.TWO_SONGS
514            pl.extend(NUMERIC_SONGS + self.TWO_SONGS)
515            self.failUnlessEqual(len(self.FAKE_LIB.changed), 7)
516            self.FAKE_LIB.reset()
517            pl.remove_songs(self.TWO_SONGS, leave_dupes=True)
518            self.failUnless(first in pl)
519            self.failUnless(second in pl)
520            self.failIf(len(self.FAKE_LIB.changed))
521
522    def test_remove_fully(self):
523        with self.wrap("playlist") as pl:
524            pl.extend(self.TWO_SONGS * 2)
525            self.FAKE_LIB.reset()
526            pl.remove_songs(self.TWO_SONGS, leave_dupes=False)
527            self.failIf(len(pl))
528            self.failUnlessEqual(self.FAKE_LIB.changed, self.TWO_SONGS)
529
530
531class TFileBackedPlaylist(TPlaylist):
532
533    def setUp(self):
534        super(TFileBackedPlaylist, self).setUp()
535        self.temp = mkdtemp()
536        self.temp2 = mkdtemp()
537
538    def tearDown(self):
539        shutil.rmtree(self.temp)
540        shutil.rmtree(self.temp2)
541
542    def pl(self, name, lib=None):
543        return FileBackedPlaylist(self.temp, name, lib)
544
545    def test_from_songs(self):
546        pl = FileBackedPlaylist.from_songs(self.temp, NUMERIC_SONGS)
547        self.failUnlessEqual(pl.songs, NUMERIC_SONGS)
548        pl.delete()
549
550    def test_read(self):
551        with self.wrap("playlist") as pl:
552            pl.extend(NUMERIC_SONGS)
553            pl.write()
554
555            lib = FileLibrary("foobar")
556            lib.add(NUMERIC_SONGS)
557            pl = self.pl("playlist", lib)
558            self.assertEqual(len(pl), len(NUMERIC_SONGS))
559
560    def test_write(self):
561        with self.wrap("playlist") as pl:
562            pl.extend(NUMERIC_SONGS)
563            pl.extend([fsnative(u"xf0xf0")])
564            pl.write()
565
566            with open(pl.filename, "rb") as h:
567                self.assertEqual(len(h.read().splitlines()),
568                                 len(NUMERIC_SONGS) + 1)
569
570    def test_make_dup(self):
571        p1 = FileBackedPlaylist.new(self.temp, "Does not exist")
572        p2 = FileBackedPlaylist.new(self.temp, "Does not exist")
573        self.failUnlessEqual(p1.name, "Does not exist")
574        self.failUnless(p2.name.startswith("Does not exist"))
575        self.failIfEqual(p1.name, p2.name)
576        p1.delete()
577        p2.delete()
578
579    def test_rename_removes(self):
580        with self.wrap("foo") as pl:
581            pl.rename("bar")
582            self.failUnless(os.path.exists(os.path.join(self.temp, 'bar')))
583            self.failIf(os.path.exists(os.path.join(self.temp, 'foo')))
584
585    def test_rename_fails_if_file_exists(self):
586        with self.wrap("foo") as foo:
587            with self.wrap("bar") as bar:
588                try:
589                    foo.rename("bar")
590                    self.fail("Should have raised, %s exists" % bar.filename)
591                except ValueError:
592                    pass
593
594    def test_masked_handling(self):
595        if os.name == "nt":
596            # FIXME: masking isn't properly implemented on Windows
597            return
598        # playlists can contain songs and paths for masked handling..
599        lib = FileLibrary("foobar")
600        with self.wrap("playlist", lib) as pl:
601            song = Fakesong({"date": "2038", "~filename": fsnative(u"/fake")})
602            song.sanitize()
603            lib.add([song])
604
605            # mask and update
606            lib.mask("/")
607            pl.append(song)
608            pl.remove_songs([song])
609            self.failUnless("/fake" in pl)
610
611            pl.extend(self.TWO_SONGS)
612
613            # check if collections can handle the mix
614            self.failUnlessEqual(pl("date"), "2038")
615
616            # unmask and update
617            lib.unmask("/")
618            pl.add_songs(["/fake"], lib)
619            self.failUnless(song in pl)
620
621            lib.destroy()
622