1# -*- coding: utf-8 -*-
2# This file is part of beets.
3# Copyright 2016, Adrian Sampson.
4#
5# Permission is hereby granted, free of charge, to any person obtaining
6# a copy of this software and associated documentation files (the
7# "Software"), to deal in the Software without restriction, including
8# without limitation the rights to use, copy, modify, merge, publish,
9# distribute, sublicense, and/or sell copies of the Software, and to
10# permit persons to whom the Software is furnished to do so, subject to
11# the following conditions:
12#
13# The above copyright notice and this permission notice shall be
14# included in all copies or substantial portions of the Software.
15
16"""Tests for autotagging functionality.
17"""
18from __future__ import division, absolute_import, print_function
19
20import re
21import copy
22import unittest
23
24from test import _common
25from beets import autotag
26from beets.autotag import match
27from beets.autotag.hooks import Distance, string_dist
28from beets.library import Item
29from beets.util import plurality
30from beets.autotag import AlbumInfo, TrackInfo
31from beets import config
32
33
34class PluralityTest(_common.TestCase):
35    def test_plurality_consensus(self):
36        objs = [1, 1, 1, 1]
37        obj, freq = plurality(objs)
38        self.assertEqual(obj, 1)
39        self.assertEqual(freq, 4)
40
41    def test_plurality_near_consensus(self):
42        objs = [1, 1, 2, 1]
43        obj, freq = plurality(objs)
44        self.assertEqual(obj, 1)
45        self.assertEqual(freq, 3)
46
47    def test_plurality_conflict(self):
48        objs = [1, 1, 2, 2, 3]
49        obj, freq = plurality(objs)
50        self.assertTrue(obj in (1, 2))
51        self.assertEqual(freq, 2)
52
53    def test_plurality_empty_sequence_raises_error(self):
54        with self.assertRaises(ValueError):
55            plurality([])
56
57    def test_current_metadata_finds_pluralities(self):
58        items = [Item(artist='The Beetles', album='The White Album'),
59                 Item(artist='The Beatles', album='The White Album'),
60                 Item(artist='The Beatles', album='Teh White Album')]
61        likelies, consensus = match.current_metadata(items)
62        self.assertEqual(likelies['artist'], 'The Beatles')
63        self.assertEqual(likelies['album'], 'The White Album')
64        self.assertFalse(consensus['artist'])
65
66    def test_current_metadata_artist_consensus(self):
67        items = [Item(artist='The Beatles', album='The White Album'),
68                 Item(artist='The Beatles', album='The White Album'),
69                 Item(artist='The Beatles', album='Teh White Album')]
70        likelies, consensus = match.current_metadata(items)
71        self.assertEqual(likelies['artist'], 'The Beatles')
72        self.assertEqual(likelies['album'], 'The White Album')
73        self.assertTrue(consensus['artist'])
74
75    def test_albumartist_consensus(self):
76        items = [Item(artist='tartist1', album='album',
77                      albumartist='aartist'),
78                 Item(artist='tartist2', album='album',
79                      albumartist='aartist'),
80                 Item(artist='tartist3', album='album',
81                      albumartist='aartist')]
82        likelies, consensus = match.current_metadata(items)
83        self.assertEqual(likelies['artist'], 'aartist')
84        self.assertFalse(consensus['artist'])
85
86    def test_current_metadata_likelies(self):
87        fields = ['artist', 'album', 'albumartist', 'year', 'disctotal',
88                  'mb_albumid', 'label', 'catalognum', 'country', 'media',
89                  'albumdisambig']
90        items = [Item(**dict((f, '%s_%s' % (f, i or 1)) for f in fields))
91                 for i in range(5)]
92        likelies, _ = match.current_metadata(items)
93        for f in fields:
94            self.assertEqual(likelies[f], '%s_1' % f)
95
96
97def _make_item(title, track, artist=u'some artist'):
98    return Item(title=title, track=track,
99                artist=artist, album=u'some album',
100                length=1,
101                mb_trackid='', mb_albumid='', mb_artistid='')
102
103
104def _make_trackinfo():
105    return [
106        TrackInfo(u'one', None, artist=u'some artist', length=1, index=1),
107        TrackInfo(u'two', None, artist=u'some artist', length=1, index=2),
108        TrackInfo(u'three', None, artist=u'some artist', length=1, index=3),
109    ]
110
111
112def _clear_weights():
113    """Hack around the lazy descriptor used to cache weights for
114    Distance calculations.
115    """
116    Distance.__dict__['_weights'].computed = False
117
118
119class DistanceTest(_common.TestCase):
120    def tearDown(self):
121        super(DistanceTest, self).tearDown()
122        _clear_weights()
123
124    def test_add(self):
125        dist = Distance()
126        dist.add('add', 1.0)
127        self.assertEqual(dist._penalties, {'add': [1.0]})
128
129    def test_add_equality(self):
130        dist = Distance()
131        dist.add_equality('equality', 'ghi', ['abc', 'def', 'ghi'])
132        self.assertEqual(dist._penalties['equality'], [0.0])
133
134        dist.add_equality('equality', 'xyz', ['abc', 'def', 'ghi'])
135        self.assertEqual(dist._penalties['equality'], [0.0, 1.0])
136
137        dist.add_equality('equality', 'abc', re.compile(r'ABC', re.I))
138        self.assertEqual(dist._penalties['equality'], [0.0, 1.0, 0.0])
139
140    def test_add_expr(self):
141        dist = Distance()
142        dist.add_expr('expr', True)
143        self.assertEqual(dist._penalties['expr'], [1.0])
144
145        dist.add_expr('expr', False)
146        self.assertEqual(dist._penalties['expr'], [1.0, 0.0])
147
148    def test_add_number(self):
149        dist = Distance()
150        # Add a full penalty for each number of difference between two numbers.
151
152        dist.add_number('number', 1, 1)
153        self.assertEqual(dist._penalties['number'], [0.0])
154
155        dist.add_number('number', 1, 2)
156        self.assertEqual(dist._penalties['number'], [0.0, 1.0])
157
158        dist.add_number('number', 2, 1)
159        self.assertEqual(dist._penalties['number'], [0.0, 1.0, 1.0])
160
161        dist.add_number('number', -1, 2)
162        self.assertEqual(dist._penalties['number'], [0.0, 1.0, 1.0, 1.0,
163                                                     1.0, 1.0])
164
165    def test_add_priority(self):
166        dist = Distance()
167        dist.add_priority('priority', 'abc', 'abc')
168        self.assertEqual(dist._penalties['priority'], [0.0])
169
170        dist.add_priority('priority', 'def', ['abc', 'def'])
171        self.assertEqual(dist._penalties['priority'], [0.0, 0.5])
172
173        dist.add_priority('priority', 'gh', ['ab', 'cd', 'ef',
174                                             re.compile('GH', re.I)])
175        self.assertEqual(dist._penalties['priority'], [0.0, 0.5, 0.75])
176
177        dist.add_priority('priority', 'xyz', ['abc', 'def'])
178        self.assertEqual(dist._penalties['priority'], [0.0, 0.5, 0.75,
179                                                       1.0])
180
181    def test_add_ratio(self):
182        dist = Distance()
183        dist.add_ratio('ratio', 25, 100)
184        self.assertEqual(dist._penalties['ratio'], [0.25])
185
186        dist.add_ratio('ratio', 10, 5)
187        self.assertEqual(dist._penalties['ratio'], [0.25, 1.0])
188
189        dist.add_ratio('ratio', -5, 5)
190        self.assertEqual(dist._penalties['ratio'], [0.25, 1.0, 0.0])
191
192        dist.add_ratio('ratio', 5, 0)
193        self.assertEqual(dist._penalties['ratio'], [0.25, 1.0, 0.0, 0.0])
194
195    def test_add_string(self):
196        dist = Distance()
197        sdist = string_dist(u'abc', u'bcd')
198        dist.add_string('string', u'abc', u'bcd')
199        self.assertEqual(dist._penalties['string'], [sdist])
200        self.assertNotEqual(dist._penalties['string'], [0])
201
202    def test_add_string_none(self):
203        dist = Distance()
204        dist.add_string('string', None, 'string')
205        self.assertEqual(dist._penalties['string'], [1])
206
207    def test_add_string_both_none(self):
208        dist = Distance()
209        dist.add_string('string', None, None)
210        self.assertEqual(dist._penalties['string'], [0])
211
212    def test_distance(self):
213        config['match']['distance_weights']['album'] = 2.0
214        config['match']['distance_weights']['medium'] = 1.0
215        _clear_weights()
216
217        dist = Distance()
218        dist.add('album', 0.5)
219        dist.add('media', 0.25)
220        dist.add('media', 0.75)
221        self.assertEqual(dist.distance, 0.5)
222
223        # __getitem__()
224        self.assertEqual(dist['album'], 0.25)
225        self.assertEqual(dist['media'], 0.25)
226
227    def test_max_distance(self):
228        config['match']['distance_weights']['album'] = 3.0
229        config['match']['distance_weights']['medium'] = 1.0
230        _clear_weights()
231
232        dist = Distance()
233        dist.add('album', 0.5)
234        dist.add('medium', 0.0)
235        dist.add('medium', 0.0)
236        self.assertEqual(dist.max_distance, 5.0)
237
238    def test_operators(self):
239        config['match']['distance_weights']['source'] = 1.0
240        config['match']['distance_weights']['album'] = 2.0
241        config['match']['distance_weights']['medium'] = 1.0
242        _clear_weights()
243
244        dist = Distance()
245        dist.add('source', 0.0)
246        dist.add('album', 0.5)
247        dist.add('medium', 0.25)
248        dist.add('medium', 0.75)
249        self.assertEqual(len(dist), 2)
250        self.assertEqual(list(dist), [('album', 0.2), ('medium', 0.2)])
251        self.assertTrue(dist == 0.4)
252        self.assertTrue(dist < 1.0)
253        self.assertTrue(dist > 0.0)
254        self.assertEqual(dist - 0.4, 0.0)
255        self.assertEqual(0.4 - dist, 0.0)
256        self.assertEqual(float(dist), 0.4)
257
258    def test_raw_distance(self):
259        config['match']['distance_weights']['album'] = 3.0
260        config['match']['distance_weights']['medium'] = 1.0
261        _clear_weights()
262
263        dist = Distance()
264        dist.add('album', 0.5)
265        dist.add('medium', 0.25)
266        dist.add('medium', 0.5)
267        self.assertEqual(dist.raw_distance, 2.25)
268
269    def test_items(self):
270        config['match']['distance_weights']['album'] = 4.0
271        config['match']['distance_weights']['medium'] = 2.0
272        _clear_weights()
273
274        dist = Distance()
275        dist.add('album', 0.1875)
276        dist.add('medium', 0.75)
277        self.assertEqual(dist.items(), [('medium', 0.25), ('album', 0.125)])
278
279        # Sort by key if distance is equal.
280        dist = Distance()
281        dist.add('album', 0.375)
282        dist.add('medium', 0.75)
283        self.assertEqual(dist.items(), [('album', 0.25), ('medium', 0.25)])
284
285    def test_update(self):
286        dist1 = Distance()
287        dist1.add('album', 0.5)
288        dist1.add('media', 1.0)
289
290        dist2 = Distance()
291        dist2.add('album', 0.75)
292        dist2.add('album', 0.25)
293        dist2.add('media', 0.05)
294
295        dist1.update(dist2)
296
297        self.assertEqual(dist1._penalties, {'album': [0.5, 0.75, 0.25],
298                                            'media': [1.0, 0.05]})
299
300
301class TrackDistanceTest(_common.TestCase):
302    def test_identical_tracks(self):
303        item = _make_item(u'one', 1)
304        info = _make_trackinfo()[0]
305        dist = match.track_distance(item, info, incl_artist=True)
306        self.assertEqual(dist, 0.0)
307
308    def test_different_title(self):
309        item = _make_item(u'foo', 1)
310        info = _make_trackinfo()[0]
311        dist = match.track_distance(item, info, incl_artist=True)
312        self.assertNotEqual(dist, 0.0)
313
314    def test_different_artist(self):
315        item = _make_item(u'one', 1)
316        item.artist = u'foo'
317        info = _make_trackinfo()[0]
318        dist = match.track_distance(item, info, incl_artist=True)
319        self.assertNotEqual(dist, 0.0)
320
321    def test_various_artists_tolerated(self):
322        item = _make_item(u'one', 1)
323        item.artist = u'Various Artists'
324        info = _make_trackinfo()[0]
325        dist = match.track_distance(item, info, incl_artist=True)
326        self.assertEqual(dist, 0.0)
327
328
329class AlbumDistanceTest(_common.TestCase):
330    def _mapping(self, items, info):
331        out = {}
332        for i, t in zip(items, info.tracks):
333            out[i] = t
334        return out
335
336    def _dist(self, items, info):
337        return match.distance(items, info, self._mapping(items, info))
338
339    def test_identical_albums(self):
340        items = []
341        items.append(_make_item(u'one', 1))
342        items.append(_make_item(u'two', 2))
343        items.append(_make_item(u'three', 3))
344        info = AlbumInfo(
345            artist=u'some artist',
346            album=u'some album',
347            tracks=_make_trackinfo(),
348            va=False,
349            album_id=None,
350            artist_id=None,
351        )
352        self.assertEqual(self._dist(items, info), 0)
353
354    def test_incomplete_album(self):
355        items = []
356        items.append(_make_item(u'one', 1))
357        items.append(_make_item(u'three', 3))
358        info = AlbumInfo(
359            artist=u'some artist',
360            album=u'some album',
361            tracks=_make_trackinfo(),
362            va=False,
363            album_id=None,
364            artist_id=None,
365        )
366        dist = self._dist(items, info)
367        self.assertNotEqual(dist, 0)
368        # Make sure the distance is not too great
369        self.assertTrue(dist < 0.2)
370
371    def test_global_artists_differ(self):
372        items = []
373        items.append(_make_item(u'one', 1))
374        items.append(_make_item(u'two', 2))
375        items.append(_make_item(u'three', 3))
376        info = AlbumInfo(
377            artist=u'someone else',
378            album=u'some album',
379            tracks=_make_trackinfo(),
380            va=False,
381            album_id=None,
382            artist_id=None,
383        )
384        self.assertNotEqual(self._dist(items, info), 0)
385
386    def test_comp_track_artists_match(self):
387        items = []
388        items.append(_make_item(u'one', 1))
389        items.append(_make_item(u'two', 2))
390        items.append(_make_item(u'three', 3))
391        info = AlbumInfo(
392            artist=u'should be ignored',
393            album=u'some album',
394            tracks=_make_trackinfo(),
395            va=True,
396            album_id=None,
397            artist_id=None,
398        )
399        self.assertEqual(self._dist(items, info), 0)
400
401    def test_comp_no_track_artists(self):
402        # Some VA releases don't have track artists (incomplete metadata).
403        items = []
404        items.append(_make_item(u'one', 1))
405        items.append(_make_item(u'two', 2))
406        items.append(_make_item(u'three', 3))
407        info = AlbumInfo(
408            artist=u'should be ignored',
409            album=u'some album',
410            tracks=_make_trackinfo(),
411            va=True,
412            album_id=None,
413            artist_id=None,
414        )
415        info.tracks[0].artist = None
416        info.tracks[1].artist = None
417        info.tracks[2].artist = None
418        self.assertEqual(self._dist(items, info), 0)
419
420    def test_comp_track_artists_do_not_match(self):
421        items = []
422        items.append(_make_item(u'one', 1))
423        items.append(_make_item(u'two', 2, u'someone else'))
424        items.append(_make_item(u'three', 3))
425        info = AlbumInfo(
426            artist=u'some artist',
427            album=u'some album',
428            tracks=_make_trackinfo(),
429            va=True,
430            album_id=None,
431            artist_id=None,
432        )
433        self.assertNotEqual(self._dist(items, info), 0)
434
435    def test_tracks_out_of_order(self):
436        items = []
437        items.append(_make_item(u'one', 1))
438        items.append(_make_item(u'three', 2))
439        items.append(_make_item(u'two', 3))
440        info = AlbumInfo(
441            artist=u'some artist',
442            album=u'some album',
443            tracks=_make_trackinfo(),
444            va=False,
445            album_id=None,
446            artist_id=None,
447        )
448        dist = self._dist(items, info)
449        self.assertTrue(0 < dist < 0.2)
450
451    def test_two_medium_release(self):
452        items = []
453        items.append(_make_item(u'one', 1))
454        items.append(_make_item(u'two', 2))
455        items.append(_make_item(u'three', 3))
456        info = AlbumInfo(
457            artist=u'some artist',
458            album=u'some album',
459            tracks=_make_trackinfo(),
460            va=False,
461            album_id=None,
462            artist_id=None,
463        )
464        info.tracks[0].medium_index = 1
465        info.tracks[1].medium_index = 2
466        info.tracks[2].medium_index = 1
467        dist = self._dist(items, info)
468        self.assertEqual(dist, 0)
469
470    def test_per_medium_track_numbers(self):
471        items = []
472        items.append(_make_item(u'one', 1))
473        items.append(_make_item(u'two', 2))
474        items.append(_make_item(u'three', 1))
475        info = AlbumInfo(
476            artist=u'some artist',
477            album=u'some album',
478            tracks=_make_trackinfo(),
479            va=False,
480            album_id=None,
481            artist_id=None,
482        )
483        info.tracks[0].medium_index = 1
484        info.tracks[1].medium_index = 2
485        info.tracks[2].medium_index = 1
486        dist = self._dist(items, info)
487        self.assertEqual(dist, 0)
488
489
490class AssignmentTest(unittest.TestCase):
491    def item(self, title, track):
492        return Item(
493            title=title, track=track,
494            mb_trackid='', mb_albumid='', mb_artistid='',
495        )
496
497    def test_reorder_when_track_numbers_incorrect(self):
498        items = []
499        items.append(self.item(u'one', 1))
500        items.append(self.item(u'three', 2))
501        items.append(self.item(u'two', 3))
502        trackinfo = []
503        trackinfo.append(TrackInfo(u'one', None))
504        trackinfo.append(TrackInfo(u'two', None))
505        trackinfo.append(TrackInfo(u'three', None))
506        mapping, extra_items, extra_tracks = \
507            match.assign_items(items, trackinfo)
508        self.assertEqual(extra_items, [])
509        self.assertEqual(extra_tracks, [])
510        self.assertEqual(mapping, {
511            items[0]: trackinfo[0],
512            items[1]: trackinfo[2],
513            items[2]: trackinfo[1],
514        })
515
516    def test_order_works_with_invalid_track_numbers(self):
517        items = []
518        items.append(self.item(u'one', 1))
519        items.append(self.item(u'three', 1))
520        items.append(self.item(u'two', 1))
521        trackinfo = []
522        trackinfo.append(TrackInfo(u'one', None))
523        trackinfo.append(TrackInfo(u'two', None))
524        trackinfo.append(TrackInfo(u'three', None))
525        mapping, extra_items, extra_tracks = \
526            match.assign_items(items, trackinfo)
527        self.assertEqual(extra_items, [])
528        self.assertEqual(extra_tracks, [])
529        self.assertEqual(mapping, {
530            items[0]: trackinfo[0],
531            items[1]: trackinfo[2],
532            items[2]: trackinfo[1],
533        })
534
535    def test_order_works_with_missing_tracks(self):
536        items = []
537        items.append(self.item(u'one', 1))
538        items.append(self.item(u'three', 3))
539        trackinfo = []
540        trackinfo.append(TrackInfo(u'one', None))
541        trackinfo.append(TrackInfo(u'two', None))
542        trackinfo.append(TrackInfo(u'three', None))
543        mapping, extra_items, extra_tracks = \
544            match.assign_items(items, trackinfo)
545        self.assertEqual(extra_items, [])
546        self.assertEqual(extra_tracks, [trackinfo[1]])
547        self.assertEqual(mapping, {
548            items[0]: trackinfo[0],
549            items[1]: trackinfo[2],
550        })
551
552    def test_order_works_with_extra_tracks(self):
553        items = []
554        items.append(self.item(u'one', 1))
555        items.append(self.item(u'two', 2))
556        items.append(self.item(u'three', 3))
557        trackinfo = []
558        trackinfo.append(TrackInfo(u'one', None))
559        trackinfo.append(TrackInfo(u'three', None))
560        mapping, extra_items, extra_tracks = \
561            match.assign_items(items, trackinfo)
562        self.assertEqual(extra_items, [items[1]])
563        self.assertEqual(extra_tracks, [])
564        self.assertEqual(mapping, {
565            items[0]: trackinfo[0],
566            items[2]: trackinfo[1],
567        })
568
569    def test_order_works_when_track_names_are_entirely_wrong(self):
570        # A real-world test case contributed by a user.
571        def item(i, length):
572            return Item(
573                artist=u'ben harper',
574                album=u'burn to shine',
575                title=u'ben harper - Burn to Shine {0}'.format(i),
576                track=i,
577                length=length,
578                mb_trackid='', mb_albumid='', mb_artistid='',
579            )
580        items = []
581        items.append(item(1, 241.37243007106997))
582        items.append(item(2, 342.27781704375036))
583        items.append(item(3, 245.95070222338137))
584        items.append(item(4, 472.87662515485437))
585        items.append(item(5, 279.1759535763187))
586        items.append(item(6, 270.33333768012))
587        items.append(item(7, 247.83435613222923))
588        items.append(item(8, 216.54504531525072))
589        items.append(item(9, 225.72775379800484))
590        items.append(item(10, 317.7643606963552))
591        items.append(item(11, 243.57001238834192))
592        items.append(item(12, 186.45916150485752))
593
594        def info(index, title, length):
595            return TrackInfo(title, None, length=length, index=index)
596        trackinfo = []
597        trackinfo.append(info(1, u'Alone', 238.893))
598        trackinfo.append(info(2, u'The Woman in You', 341.44))
599        trackinfo.append(info(3, u'Less', 245.59999999999999))
600        trackinfo.append(info(4, u'Two Hands of a Prayer', 470.49299999999999))
601        trackinfo.append(info(5, u'Please Bleed', 277.86599999999999))
602        trackinfo.append(info(6, u'Suzie Blue', 269.30599999999998))
603        trackinfo.append(info(7, u'Steal My Kisses', 245.36000000000001))
604        trackinfo.append(info(8, u'Burn to Shine', 214.90600000000001))
605        trackinfo.append(info(9, u'Show Me a Little Shame', 224.0929999999999))
606        trackinfo.append(info(10, u'Forgiven', 317.19999999999999))
607        trackinfo.append(info(11, u'Beloved One', 243.733))
608        trackinfo.append(info(12, u'In the Lord\'s Arms', 186.13300000000001))
609
610        mapping, extra_items, extra_tracks = \
611            match.assign_items(items, trackinfo)
612        self.assertEqual(extra_items, [])
613        self.assertEqual(extra_tracks, [])
614        for item, info in mapping.items():
615            self.assertEqual(items.index(item), trackinfo.index(info))
616
617
618class ApplyTestUtil(object):
619    def _apply(self, info=None, per_disc_numbering=False, artist_credit=False):
620        info = info or self.info
621        mapping = {}
622        for i, t in zip(self.items, info.tracks):
623            mapping[i] = t
624        config['per_disc_numbering'] = per_disc_numbering
625        config['artist_credit'] = artist_credit
626        autotag.apply_metadata(info, mapping)
627
628
629class ApplyTest(_common.TestCase, ApplyTestUtil):
630    def setUp(self):
631        super(ApplyTest, self).setUp()
632
633        self.items = []
634        self.items.append(Item({}))
635        self.items.append(Item({}))
636        trackinfo = []
637        trackinfo.append(TrackInfo(
638            u'oneNew',
639            u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c',
640            medium=1,
641            medium_index=1,
642            medium_total=1,
643            index=1,
644            artist_credit='trackArtistCredit',
645            artist_sort='trackArtistSort',
646        ))
647        trackinfo.append(TrackInfo(
648            u'twoNew',
649            u'40130ed1-a27c-42fd-a328-1ebefb6caef4',
650            medium=2,
651            medium_index=1,
652            index=2,
653            medium_total=1,
654        ))
655        self.info = AlbumInfo(
656            tracks=trackinfo,
657            artist=u'artistNew',
658            album=u'albumNew',
659            album_id='7edb51cb-77d6-4416-a23c-3a8c2994a2c7',
660            artist_id='a6623d39-2d8e-4f70-8242-0a9553b91e50',
661            artist_credit=u'albumArtistCredit',
662            artist_sort=u'albumArtistSort',
663            albumtype=u'album',
664            va=False,
665            mediums=2,
666        )
667
668    def test_titles_applied(self):
669        self._apply()
670        self.assertEqual(self.items[0].title, 'oneNew')
671        self.assertEqual(self.items[1].title, 'twoNew')
672
673    def test_album_and_artist_applied_to_all(self):
674        self._apply()
675        self.assertEqual(self.items[0].album, 'albumNew')
676        self.assertEqual(self.items[1].album, 'albumNew')
677        self.assertEqual(self.items[0].artist, 'artistNew')
678        self.assertEqual(self.items[1].artist, 'artistNew')
679
680    def test_track_index_applied(self):
681        self._apply()
682        self.assertEqual(self.items[0].track, 1)
683        self.assertEqual(self.items[1].track, 2)
684
685    def test_track_total_applied(self):
686        self._apply()
687        self.assertEqual(self.items[0].tracktotal, 2)
688        self.assertEqual(self.items[1].tracktotal, 2)
689
690    def test_disc_index_applied(self):
691        self._apply()
692        self.assertEqual(self.items[0].disc, 1)
693        self.assertEqual(self.items[1].disc, 2)
694
695    def test_disc_total_applied(self):
696        self._apply()
697        self.assertEqual(self.items[0].disctotal, 2)
698        self.assertEqual(self.items[1].disctotal, 2)
699
700    def test_per_disc_numbering(self):
701        self._apply(per_disc_numbering=True)
702        self.assertEqual(self.items[0].track, 1)
703        self.assertEqual(self.items[1].track, 1)
704
705    def test_per_disc_numbering_track_total(self):
706        self._apply(per_disc_numbering=True)
707        self.assertEqual(self.items[0].tracktotal, 1)
708        self.assertEqual(self.items[1].tracktotal, 1)
709
710    def test_artist_credit(self):
711        self._apply(artist_credit=True)
712        self.assertEqual(self.items[0].artist, 'trackArtistCredit')
713        self.assertEqual(self.items[1].artist, 'albumArtistCredit')
714        self.assertEqual(self.items[0].albumartist, 'albumArtistCredit')
715        self.assertEqual(self.items[1].albumartist, 'albumArtistCredit')
716
717    def test_artist_credit_prefers_artist_over_albumartist_credit(self):
718        self.info.tracks[0].artist = 'oldArtist'
719        self.info.tracks[0].artist_credit = None
720        self._apply(artist_credit=True)
721        self.assertEqual(self.items[0].artist, 'oldArtist')
722
723    def test_artist_credit_falls_back_to_albumartist(self):
724        self.info.artist_credit = None
725        self._apply(artist_credit=True)
726        self.assertEqual(self.items[1].artist, 'artistNew')
727
728    def test_mb_trackid_applied(self):
729        self._apply()
730        self.assertEqual(self.items[0].mb_trackid,
731                         'dfa939ec-118c-4d0f-84a0-60f3d1e6522c')
732        self.assertEqual(self.items[1].mb_trackid,
733                         '40130ed1-a27c-42fd-a328-1ebefb6caef4')
734
735    def test_mb_albumid_and_artistid_applied(self):
736        self._apply()
737        for item in self.items:
738            self.assertEqual(item.mb_albumid,
739                             '7edb51cb-77d6-4416-a23c-3a8c2994a2c7')
740            self.assertEqual(item.mb_artistid,
741                             'a6623d39-2d8e-4f70-8242-0a9553b91e50')
742
743    def test_albumtype_applied(self):
744        self._apply()
745        self.assertEqual(self.items[0].albumtype, 'album')
746        self.assertEqual(self.items[1].albumtype, 'album')
747
748    def test_album_artist_overrides_empty_track_artist(self):
749        my_info = copy.deepcopy(self.info)
750        self._apply(info=my_info)
751        self.assertEqual(self.items[0].artist, 'artistNew')
752        self.assertEqual(self.items[1].artist, 'artistNew')
753
754    def test_album_artist_overridden_by_nonempty_track_artist(self):
755        my_info = copy.deepcopy(self.info)
756        my_info.tracks[0].artist = 'artist1!'
757        my_info.tracks[1].artist = 'artist2!'
758        self._apply(info=my_info)
759        self.assertEqual(self.items[0].artist, 'artist1!')
760        self.assertEqual(self.items[1].artist, 'artist2!')
761
762    def test_artist_credit_applied(self):
763        self._apply()
764        self.assertEqual(self.items[0].albumartist_credit, 'albumArtistCredit')
765        self.assertEqual(self.items[0].artist_credit, 'trackArtistCredit')
766        self.assertEqual(self.items[1].albumartist_credit, 'albumArtistCredit')
767        self.assertEqual(self.items[1].artist_credit, 'albumArtistCredit')
768
769    def test_artist_sort_applied(self):
770        self._apply()
771        self.assertEqual(self.items[0].albumartist_sort, 'albumArtistSort')
772        self.assertEqual(self.items[0].artist_sort, 'trackArtistSort')
773        self.assertEqual(self.items[1].albumartist_sort, 'albumArtistSort')
774        self.assertEqual(self.items[1].artist_sort, 'albumArtistSort')
775
776    def test_full_date_applied(self):
777        my_info = copy.deepcopy(self.info)
778        my_info.year = 2013
779        my_info.month = 12
780        my_info.day = 18
781        self._apply(info=my_info)
782
783        self.assertEqual(self.items[0].year, 2013)
784        self.assertEqual(self.items[0].month, 12)
785        self.assertEqual(self.items[0].day, 18)
786
787    def test_date_only_zeros_month_and_day(self):
788        self.items = []
789        self.items.append(Item(year=1, month=2, day=3))
790        self.items.append(Item(year=4, month=5, day=6))
791
792        my_info = copy.deepcopy(self.info)
793        my_info.year = 2013
794        self._apply(info=my_info)
795
796        self.assertEqual(self.items[0].year, 2013)
797        self.assertEqual(self.items[0].month, 0)
798        self.assertEqual(self.items[0].day, 0)
799
800    def test_missing_date_applies_nothing(self):
801        self.items = []
802        self.items.append(Item(year=1, month=2, day=3))
803        self.items.append(Item(year=4, month=5, day=6))
804
805        self._apply()
806
807        self.assertEqual(self.items[0].year, 1)
808        self.assertEqual(self.items[0].month, 2)
809        self.assertEqual(self.items[0].day, 3)
810
811    def test_data_source_applied(self):
812        my_info = copy.deepcopy(self.info)
813        my_info.data_source = 'MusicBrainz'
814        self._apply(info=my_info)
815
816        self.assertEqual(self.items[0].data_source, 'MusicBrainz')
817
818
819class ApplyCompilationTest(_common.TestCase, ApplyTestUtil):
820    def setUp(self):
821        super(ApplyCompilationTest, self).setUp()
822
823        self.items = []
824        self.items.append(Item({}))
825        self.items.append(Item({}))
826        trackinfo = []
827        trackinfo.append(TrackInfo(
828            u'oneNew',
829            u'dfa939ec-118c-4d0f-84a0-60f3d1e6522c',
830            artist=u'artistOneNew',
831            artist_id=u'a05686fc-9db2-4c23-b99e-77f5db3e5282',
832            index=1,
833        ))
834        trackinfo.append(TrackInfo(
835            u'twoNew',
836            u'40130ed1-a27c-42fd-a328-1ebefb6caef4',
837            artist=u'artistTwoNew',
838            artist_id=u'80b3cf5e-18fe-4c59-98c7-e5bb87210710',
839            index=2,
840        ))
841        self.info = AlbumInfo(
842            tracks=trackinfo,
843            artist=u'variousNew',
844            album=u'albumNew',
845            album_id='3b69ea40-39b8-487f-8818-04b6eff8c21a',
846            artist_id='89ad4ac3-39f7-470e-963a-56509c546377',
847            albumtype=u'compilation',
848        )
849
850    def test_album_and_track_artists_separate(self):
851        self._apply()
852        self.assertEqual(self.items[0].artist, 'artistOneNew')
853        self.assertEqual(self.items[1].artist, 'artistTwoNew')
854        self.assertEqual(self.items[0].albumartist, 'variousNew')
855        self.assertEqual(self.items[1].albumartist, 'variousNew')
856
857    def test_mb_albumartistid_applied(self):
858        self._apply()
859        self.assertEqual(self.items[0].mb_albumartistid,
860                         '89ad4ac3-39f7-470e-963a-56509c546377')
861        self.assertEqual(self.items[1].mb_albumartistid,
862                         '89ad4ac3-39f7-470e-963a-56509c546377')
863        self.assertEqual(self.items[0].mb_artistid,
864                         'a05686fc-9db2-4c23-b99e-77f5db3e5282')
865        self.assertEqual(self.items[1].mb_artistid,
866                         '80b3cf5e-18fe-4c59-98c7-e5bb87210710')
867
868    def test_va_flag_cleared_does_not_set_comp(self):
869        self._apply()
870        self.assertFalse(self.items[0].comp)
871        self.assertFalse(self.items[1].comp)
872
873    def test_va_flag_sets_comp(self):
874        va_info = copy.deepcopy(self.info)
875        va_info.va = True
876        self._apply(info=va_info)
877        self.assertTrue(self.items[0].comp)
878        self.assertTrue(self.items[1].comp)
879
880
881class StringDistanceTest(unittest.TestCase):
882    def test_equal_strings(self):
883        dist = string_dist(u'Some String', u'Some String')
884        self.assertEqual(dist, 0.0)
885
886    def test_different_strings(self):
887        dist = string_dist(u'Some String', u'Totally Different')
888        self.assertNotEqual(dist, 0.0)
889
890    def test_punctuation_ignored(self):
891        dist = string_dist(u'Some String', u'Some.String!')
892        self.assertEqual(dist, 0.0)
893
894    def test_case_ignored(self):
895        dist = string_dist(u'Some String', u'sOME sTring')
896        self.assertEqual(dist, 0.0)
897
898    def test_leading_the_has_lower_weight(self):
899        dist1 = string_dist(u'XXX Band Name', u'Band Name')
900        dist2 = string_dist(u'The Band Name', u'Band Name')
901        self.assertTrue(dist2 < dist1)
902
903    def test_parens_have_lower_weight(self):
904        dist1 = string_dist(u'One .Two.', u'One')
905        dist2 = string_dist(u'One (Two)', u'One')
906        self.assertTrue(dist2 < dist1)
907
908    def test_brackets_have_lower_weight(self):
909        dist1 = string_dist(u'One .Two.', u'One')
910        dist2 = string_dist(u'One [Two]', u'One')
911        self.assertTrue(dist2 < dist1)
912
913    def test_ep_label_has_zero_weight(self):
914        dist = string_dist(u'My Song (EP)', u'My Song')
915        self.assertEqual(dist, 0.0)
916
917    def test_featured_has_lower_weight(self):
918        dist1 = string_dist(u'My Song blah Someone', u'My Song')
919        dist2 = string_dist(u'My Song feat Someone', u'My Song')
920        self.assertTrue(dist2 < dist1)
921
922    def test_postfix_the(self):
923        dist = string_dist(u'The Song Title', u'Song Title, The')
924        self.assertEqual(dist, 0.0)
925
926    def test_postfix_a(self):
927        dist = string_dist(u'A Song Title', u'Song Title, A')
928        self.assertEqual(dist, 0.0)
929
930    def test_postfix_an(self):
931        dist = string_dist(u'An Album Title', u'Album Title, An')
932        self.assertEqual(dist, 0.0)
933
934    def test_empty_strings(self):
935        dist = string_dist(u'', u'')
936        self.assertEqual(dist, 0.0)
937
938    def test_solo_pattern(self):
939        # Just make sure these don't crash.
940        string_dist(u'The ', u'')
941        string_dist(u'(EP)', u'(EP)')
942        string_dist(u', An', u'')
943
944    def test_heuristic_does_not_harm_distance(self):
945        dist = string_dist(u'Untitled', u'[Untitled]')
946        self.assertEqual(dist, 0.0)
947
948    def test_ampersand_expansion(self):
949        dist = string_dist(u'And', u'&')
950        self.assertEqual(dist, 0.0)
951
952    def test_accented_characters(self):
953        dist = string_dist(u'\xe9\xe1\xf1', u'ean')
954        self.assertEqual(dist, 0.0)
955
956
957class EnumTest(_common.TestCase):
958    """
959    Test Enum Subclasses defined in beets.util.enumeration
960    """
961    def test_ordered_enum(self):
962        OrderedEnumClass = match.OrderedEnum('OrderedEnumTest', ['a', 'b', 'c'])  # noqa
963        self.assertLess(OrderedEnumClass.a, OrderedEnumClass.b)
964        self.assertLess(OrderedEnumClass.a, OrderedEnumClass.c)
965        self.assertLess(OrderedEnumClass.b, OrderedEnumClass.c)
966        self.assertGreater(OrderedEnumClass.b, OrderedEnumClass.a)
967        self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.a)
968        self.assertGreater(OrderedEnumClass.c, OrderedEnumClass.b)
969
970
971def suite():
972    return unittest.TestLoader().loadTestsFromName(__name__)
973
974if __name__ == '__main__':
975    unittest.main(defaultTest='suite')
976