mirror of
https://github.com/fergalmoran/picard.git
synced 2026-01-03 15:13:57 +00:00
Some of Picard's tag names allow an additional description separated by a colon, e.g. "comment:desc". In case the description part is empty and the tag ends on a colon this is now treated the same as without any colon. So "lyrics" and "lyrics:" are the same tag. Handling this in Metadata makes this change immediately available in scripting and to all formats. Script using both forms will still work.
540 lines
19 KiB
Python
540 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
||
from test.picardtestcase import (
|
||
PicardTestCase,
|
||
load_test_json,
|
||
)
|
||
from test.test_coverart_image import create_image
|
||
|
||
from picard import config
|
||
from picard.cluster import Cluster
|
||
from picard.file import File
|
||
from picard.mbjson import (
|
||
release_to_metadata,
|
||
track_to_metadata,
|
||
)
|
||
from picard.metadata import (
|
||
MULTI_VALUED_JOINER,
|
||
Metadata,
|
||
weights_from_preferred_countries,
|
||
weights_from_preferred_formats,
|
||
weights_from_release_type_scores,
|
||
)
|
||
from picard.track import Track
|
||
from picard.util.imagelist import ImageList
|
||
from picard.util.tags import PRESERVED_TAGS
|
||
|
||
|
||
settings = {
|
||
'write_id3v23': False,
|
||
'id3v23_join_with': '/',
|
||
'preferred_release_countries': [],
|
||
'preferred_release_formats': [],
|
||
'standardize_artists': False,
|
||
'standardize_instruments': False,
|
||
'translate_artist_names': False,
|
||
'release_type_scores': [
|
||
('Album', 1.0)
|
||
],
|
||
}
|
||
|
||
|
||
class MetadataTest(PicardTestCase):
|
||
|
||
original = None
|
||
tags = []
|
||
|
||
def setUp(self):
|
||
super().setUp()
|
||
config.setting = settings.copy()
|
||
self.metadata = Metadata()
|
||
self.metadata["single1"] = "single1-value"
|
||
self.metadata.add_unique("single2", "single2-value")
|
||
self.metadata.add_unique("single2", "single2-value")
|
||
self.multi1 = ["multi1-value", "multi1-value"]
|
||
self.metadata.add("multi1", self.multi1[0])
|
||
self.metadata.add("multi1", self.multi1[1])
|
||
self.multi2 = ["multi2-value1", "multi2-value2"]
|
||
self.metadata["multi2"] = self.multi2
|
||
self.multi3 = ["multi3-value1", "multi3-value2"]
|
||
self.metadata.set("multi3", self.multi3)
|
||
self.metadata["~hidden"] = "hidden-value"
|
||
|
||
self.metadata_d1 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': ''})
|
||
self.metadata_d2 = Metadata({'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': 'z'})
|
||
self.metadata_d3 = Metadata({'c': 3, 'd': ['u', 'w'], 'x': 'p'})
|
||
|
||
def tearDown(self):
|
||
pass
|
||
|
||
def test_metadata_setitem(self):
|
||
self.assertEqual(["single1-value"], self.metadata.getraw("single1"))
|
||
self.assertEqual(["single2-value"], self.metadata.getraw("single2"))
|
||
self.assertEqual(self.multi1, self.metadata.getraw("multi1"))
|
||
self.assertEqual(self.multi2, self.metadata.getraw("multi2"))
|
||
self.assertEqual(self.multi3, self.metadata.getraw("multi3"))
|
||
self.assertEqual(["hidden-value"], self.metadata.getraw("~hidden"))
|
||
|
||
def test_metadata_set_all_values_as_string(self):
|
||
for val in (0, 2, True):
|
||
str_val = str(val)
|
||
self.metadata.set('val1', val)
|
||
self.assertEqual([str_val], self.metadata.getraw("val1"))
|
||
self.metadata['val2'] = val
|
||
self.assertEqual([str_val], self.metadata.getraw("val2"))
|
||
del self.metadata['val3']
|
||
self.metadata.add('val3', val)
|
||
self.assertEqual([str_val], self.metadata.getraw("val3"))
|
||
del self.metadata['val4']
|
||
self.metadata.add_unique('val4', val)
|
||
self.assertEqual([str_val], self.metadata.getraw("val4"))
|
||
|
||
def test_metadata_get(self):
|
||
self.assertEqual("single1-value", self.metadata["single1"])
|
||
self.assertEqual("single1-value", self.metadata.get("single1"))
|
||
self.assertEqual(["single1-value"], self.metadata.getall("single1"))
|
||
self.assertEqual(["single1-value"], self.metadata.getraw("single1"))
|
||
|
||
self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata["multi1"])
|
||
self.assertEqual(MULTI_VALUED_JOINER.join(self.multi1), self.metadata.get("multi1"))
|
||
self.assertEqual(self.multi1, self.metadata.getall("multi1"))
|
||
self.assertEqual(self.multi1, self.metadata.getraw("multi1"))
|
||
|
||
self.assertEqual("", self.metadata["nonexistent"])
|
||
self.assertEqual(None, self.metadata.get("nonexistent"))
|
||
self.assertEqual([], self.metadata.getall("nonexistent"))
|
||
self.assertRaises(KeyError, self.metadata.getraw, "nonexistent")
|
||
|
||
self.assertEqual(self.metadata._store.items(), self.metadata.rawitems())
|
||
metadata_items = [(x, z) for (x, y) in self.metadata.rawitems() for z in y]
|
||
self.assertEqual(metadata_items, list(self.metadata.items()))
|
||
|
||
def test_metadata_delete(self):
|
||
self.metadata.delete("single1")
|
||
self.assertNotIn("single1", self.metadata)
|
||
self.assertIn("single1", self.metadata.deleted_tags)
|
||
|
||
def test_metadata_implicit_delete(self):
|
||
self.metadata["single2"] = ""
|
||
self.assertNotIn("single2", self.metadata)
|
||
self.assertIn("single2", self.metadata.deleted_tags)
|
||
|
||
self.metadata["unknown"] = ""
|
||
self.assertNotIn("unknown", self.metadata)
|
||
self.assertNotIn("unknown", self.metadata.deleted_tags)
|
||
|
||
def test_metadata_undelete(self):
|
||
self.metadata.delete("single1")
|
||
self.assertNotIn("single1", self.metadata)
|
||
self.assertIn("single1", self.metadata.deleted_tags)
|
||
|
||
self.metadata["single1"] = "value1"
|
||
self.assertIn("single1", self.metadata)
|
||
self.assertNotIn("single1", self.metadata.deleted_tags)
|
||
|
||
def test_normalize_tag(self):
|
||
self.assertEqual('sometag', Metadata.normalize_tag('sometag'))
|
||
self.assertEqual('sometag', Metadata.normalize_tag('sometag:'))
|
||
self.assertEqual('sometag', Metadata.normalize_tag('sometag::'))
|
||
self.assertEqual('sometag:desc', Metadata.normalize_tag('sometag:desc'))
|
||
|
||
def test_metadata_tag_trailing_colon(self):
|
||
self.metadata['tag:'] = 'Foo'
|
||
self.assertIn('tag', self.metadata)
|
||
self.assertIn('tag:', self.metadata)
|
||
self.assertEqual('Foo', self.metadata['tag'])
|
||
self.assertEqual('Foo', self.metadata['tag:'])
|
||
del self.metadata['tag']
|
||
self.assertNotIn('tag', self.metadata)
|
||
self.assertNotIn('tag:', self.metadata)
|
||
|
||
def test_metadata_copy(self):
|
||
m = Metadata()
|
||
m["old"] = "old-value"
|
||
self.metadata.delete("single1")
|
||
m.copy(self.metadata)
|
||
self.assertEqual(self.metadata._store, m._store)
|
||
self.assertEqual(self.metadata.deleted_tags, m.deleted_tags)
|
||
self.assertEqual(self.metadata.length, m.length)
|
||
self.assertEqual(self.metadata.images, m.images)
|
||
|
||
def test_metadata_copy_without_images(self):
|
||
m = Metadata()
|
||
m.copy(self.metadata, copy_images=False)
|
||
self.assertEqual(self.metadata._store, m._store)
|
||
self.assertEqual(self.metadata.deleted_tags, m.deleted_tags)
|
||
self.assertEqual(self.metadata.length, m.length)
|
||
self.assertEqual(ImageList(), m.images)
|
||
|
||
def test_metadata_update(self):
|
||
m = Metadata()
|
||
m["old"] = "old-value"
|
||
self.metadata.delete("single1")
|
||
m.update(self.metadata)
|
||
self.assertIn("old", m)
|
||
self.assertNotIn("single1", m)
|
||
self.assertIn("single1", m.deleted_tags)
|
||
self.assertEqual("single2-value", m["single2"])
|
||
self.assertEqual(self.metadata.deleted_tags, m.deleted_tags)
|
||
self.assertEqual(self.metadata.images, m.images)
|
||
|
||
self.metadata["old"] = "old-value"
|
||
self.assertEqual(self.metadata._store, m._store)
|
||
|
||
def test_metadata_clear(self):
|
||
self.metadata.clear()
|
||
self.assertEqual(0, len(self.metadata))
|
||
|
||
def test_metadata_clear_deleted(self):
|
||
self.metadata.delete("single1")
|
||
self.assertIn("single1", self.metadata.deleted_tags)
|
||
self.metadata.clear_deleted()
|
||
self.assertNotIn("single1", self.metadata.deleted_tags)
|
||
|
||
def test_metadata_applyfunc(self):
|
||
def func(x): return x[1:]
|
||
self.metadata.apply_func(func)
|
||
|
||
self.assertEqual("ingle1-value", self.metadata["single1"])
|
||
self.assertEqual("ingle1-value", self.metadata.get("single1"))
|
||
self.assertEqual(["ingle1-value"], self.metadata.getall("single1"))
|
||
|
||
self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata["multi1"])
|
||
self.assertEqual(MULTI_VALUED_JOINER.join(map(func, self.multi1)), self.metadata.get("multi1"))
|
||
self.assertEqual(list(map(func, self.multi1)), self.metadata.getall("multi1"))
|
||
|
||
def test_metadata_applyfunc_preserve_tags(self):
|
||
self.assertTrue(len(PRESERVED_TAGS) > 0)
|
||
m = Metadata()
|
||
m[PRESERVED_TAGS[0]] = 'value1'
|
||
m['not_preserved'] = 'value2'
|
||
|
||
def func(x): return x[1:]
|
||
m.apply_func(func)
|
||
|
||
self.assertEqual("value1", m[PRESERVED_TAGS[0]])
|
||
self.assertEqual("alue2", m['not_preserved'])
|
||
|
||
def test_metadata_applyfunc_delete_tags(self):
|
||
def func(x): return None
|
||
metadata = Metadata(self.metadata)
|
||
metadata.apply_func(func)
|
||
self.assertEqual(0, len(metadata.rawitems()))
|
||
self.assertEqual(self.metadata.keys(), metadata.deleted_tags)
|
||
|
||
def test_length_score(self):
|
||
results = [(20000, 0, 0.333333333333),
|
||
(20000, 10000, 0.666666666667),
|
||
(20000, 20000, 1.0),
|
||
(20000, 30000, 0.666666666667),
|
||
(20000, 40000, 0.333333333333),
|
||
(20000, 50000, 0.0)]
|
||
for (a, b, expected) in results:
|
||
actual = Metadata.length_score(a, b)
|
||
self.assertAlmostEqual(expected, actual,
|
||
msg="a={a}, b={b}".format(a=a, b=b))
|
||
|
||
def test_compare_is_equal(self):
|
||
m1 = Metadata()
|
||
m1["title"] = "title1"
|
||
m1["tracknumber"] = "2"
|
||
m1.length = 360
|
||
m2 = Metadata()
|
||
m2["title"] = "title1"
|
||
m2["tracknumber"] = "2"
|
||
m2.length = 360
|
||
self.assertEqual(m1.compare(m2), m2.compare(m1))
|
||
self.assertEqual(m1.compare(m2), 1)
|
||
|
||
def test_compare_with_ignored(self):
|
||
m1 = Metadata()
|
||
m1["title"] = "title1"
|
||
m1["tracknumber"] = "2"
|
||
m1.length = 360
|
||
m2 = Metadata()
|
||
m2["title"] = "title1"
|
||
m2["tracknumber"] = "3"
|
||
m2.length = 300
|
||
self.assertNotEqual(m1.compare(m2), 1)
|
||
self.assertEqual(m1.compare(m2, ignored=['tracknumber', '~length']), 1)
|
||
|
||
def test_compare_lengths(self):
|
||
m1 = Metadata()
|
||
m1.length = 360
|
||
m2 = Metadata()
|
||
m2.length = 300
|
||
self.assertAlmostEqual(m1.compare(m2), 0.998)
|
||
|
||
def test_compare_tracknumber_difference(self):
|
||
m1 = Metadata()
|
||
m1["tracknumber"] = "1"
|
||
m2 = Metadata()
|
||
m2["tracknumber"] = "2"
|
||
self.assertEqual(m1.compare(m2), 0)
|
||
|
||
def test_compare_deleted(self):
|
||
m1 = Metadata()
|
||
m1["artist"] = "TheArtist"
|
||
m1["title"] = "title1"
|
||
m2 = Metadata()
|
||
m2["artist"] = "TheArtist"
|
||
m2.delete("title")
|
||
self.assertTrue(m1.compare(m2) < 1)
|
||
|
||
def test_strip_whitespace(self):
|
||
m1 = Metadata()
|
||
m1["artist"] = " TheArtist "
|
||
m1["title"] = "\t\u00A0 tit le1 \r\n"
|
||
m1["genre"] = " \t"
|
||
m1.strip_whitespace()
|
||
self.assertEqual(m1["artist"], "TheArtist")
|
||
self.assertEqual(m1["title"], "tit le1")
|
||
|
||
def test_metadata_mapping_init(self):
|
||
d = {'a': 'b', 'c': 2, 'd': ['x', 'y'], 'x': '', 'z': {'u', 'w'}}
|
||
deleted_tags = set('c')
|
||
m = Metadata(d, deleted_tags=deleted_tags, length=1234)
|
||
self.assertIn('a', m)
|
||
self.assertEqual(m.getraw('a'), ['b'])
|
||
self.assertEqual(m['d'], MULTI_VALUED_JOINER.join(d['d']))
|
||
self.assertNotIn('c', m)
|
||
self.assertNotIn('length', m)
|
||
self.assertIn('c', m.deleted_tags)
|
||
self.assertEqual(m.length, 1234)
|
||
|
||
def test_metadata_mapping_init_zero(self):
|
||
m = Metadata(tag1='a', tag2=0, tag3='', tag4=None)
|
||
m['tag5'] = 0
|
||
m['tag1'] = ''
|
||
self.assertIn('tag1', m.deleted_tags)
|
||
self.assertEqual(m['tag2'], '0')
|
||
self.assertNotIn('tag3', m)
|
||
self.assertNotIn('tag4', m)
|
||
self.assertEqual(m['tag5'], '0')
|
||
|
||
def test_metadata_mapping_del(self):
|
||
m = self.metadata_d1
|
||
self.assertEqual(m.getraw('a'), ['b'])
|
||
self.assertNotIn('a', m.deleted_tags)
|
||
|
||
self.assertNotIn('x', m.deleted_tags)
|
||
self.assertRaises(KeyError, m.getraw, 'x')
|
||
|
||
del m['a']
|
||
self.assertRaises(KeyError, m.getraw, 'a')
|
||
self.assertIn('a', m.deleted_tags)
|
||
|
||
# NOTE: historic behavior of Metadata.delete()
|
||
# an attempt to delete an non-existing tag, will add it to the list
|
||
# of deleted tags
|
||
# so this will not raise a KeyError
|
||
# as is it differs from dict or even defaultdict behavior
|
||
del m['unknown']
|
||
self.assertIn('unknown', m.deleted_tags)
|
||
|
||
def test_metadata_mapping_iter(self):
|
||
l = set(self.metadata_d1)
|
||
self.assertEqual(l, {'a', 'c', 'd'})
|
||
|
||
def test_metadata_mapping_keys(self):
|
||
l = set(self.metadata_d1.keys())
|
||
self.assertEqual(l, {'a', 'c', 'd'})
|
||
|
||
def test_metadata_mapping_values(self):
|
||
l = set(self.metadata_d1.values())
|
||
self.assertEqual(l, {'b', '2', 'x; y'})
|
||
|
||
def test_metadata_mapping_len(self):
|
||
m = self.metadata_d1
|
||
self.assertEqual(len(m), 3)
|
||
del m['x']
|
||
self.assertEqual(len(m), 3)
|
||
del m['c']
|
||
self.assertEqual(len(m), 2)
|
||
|
||
def _check_mapping_update(self, m):
|
||
self.assertEqual(m['a'], 'b')
|
||
self.assertEqual(m['c'], '3')
|
||
self.assertEqual(m.getraw('d'), ['u', 'w'])
|
||
self.assertEqual(m['x'], '')
|
||
self.assertIn('x', m.deleted_tags)
|
||
|
||
def test_metadata_mapping_update(self):
|
||
# update from Metadata
|
||
m = self.metadata_d2
|
||
m2 = self.metadata_d3
|
||
|
||
del m2['x']
|
||
m.update(m2)
|
||
self._check_mapping_update(m)
|
||
|
||
def test_metadata_mapping_update_dict(self):
|
||
# update from dict
|
||
m = self.metadata_d2
|
||
|
||
d2 = {'c': 3, 'd': ['u', 'w'], 'x': ''}
|
||
|
||
m.update(d2)
|
||
self._check_mapping_update(m)
|
||
|
||
def test_metadata_mapping_update_tuple(self):
|
||
# update from tuple
|
||
m = self.metadata_d2
|
||
|
||
d2 = (('c', 3), ('d', ['u', 'w']), ('x', ''))
|
||
|
||
m.update(d2)
|
||
self._check_mapping_update(m)
|
||
|
||
def test_metadata_mapping_update_dictlike(self):
|
||
# update from kwargs
|
||
m = self.metadata_d2
|
||
|
||
m.update(c=3, d=['u', 'w'], x='')
|
||
self._check_mapping_update(m)
|
||
|
||
def test_metadata_mapping_update_noparam(self):
|
||
# update without parameter
|
||
m = self.metadata_d2
|
||
|
||
self.assertRaises(TypeError, m.update)
|
||
self.assertEqual(m['a'], 'b')
|
||
|
||
def test_metadata_mapping_update_intparam(self):
|
||
# update without parameter
|
||
m = self.metadata_d2
|
||
|
||
self.assertRaises(TypeError, m.update, 123)
|
||
|
||
def test_metadata_mapping_update_strparam(self):
|
||
# update without parameter
|
||
m = self.metadata_d2
|
||
|
||
self.assertRaises(ValueError, m.update, 'abc')
|
||
|
||
def test_metadata_mapping_update_kw(self):
|
||
m = Metadata(tag1='a', tag2='b')
|
||
m.update(tag1='c')
|
||
self.assertEqual(m['tag1'], 'c')
|
||
self.assertEqual(m['tag2'], 'b')
|
||
m.update(tag2='')
|
||
self.assertIn('tag2', m.deleted_tags)
|
||
|
||
def test_metadata_mapping_update_kw_del(self):
|
||
m = Metadata(tag1='a', tag2='b')
|
||
del m['tag1']
|
||
|
||
m2 = Metadata(tag1='c', tag2='d')
|
||
del m2['tag2']
|
||
|
||
m.update(m2)
|
||
self.assertEqual(m['tag1'], 'c')
|
||
self.assertNotIn('tag2', m)
|
||
self.assertNotIn('tag1', m.deleted_tags)
|
||
self.assertIn('tag2', m.deleted_tags)
|
||
|
||
def test_metadata_mapping_images(self):
|
||
image1 = create_image(b'A', comment='A')
|
||
image2 = create_image(b'B', comment='B')
|
||
|
||
m1 = Metadata(a='b', length=1234, images=[image1])
|
||
self.assertEqual(m1.images[0], image1)
|
||
self.assertEqual(len(m1), 2) # one tag, one image
|
||
|
||
m1.images.append(image2)
|
||
self.assertEqual(m1.images[1], image2)
|
||
|
||
m1.images.pop(0)
|
||
self.assertEqual(m1.images[0], image2)
|
||
|
||
m2 = Metadata(a='c', length=4567, images=[image1])
|
||
m1.update(m2)
|
||
self.assertEqual(m1.images[0], image1)
|
||
|
||
m1.images.pop(0)
|
||
self.assertEqual(len(m1), 1) # one tag, zero image
|
||
self.assertFalse(m1.images)
|
||
|
||
def test_metadata_mapping_iterable(self):
|
||
m = Metadata(tag_tuple=('a', 0))
|
||
m['tag_set'] = {'c', 'd'}
|
||
m['tag_dict'] = {'e': 1, 'f': 2}
|
||
m['tag_str'] = 'gh'
|
||
self.assertIn('0', m.getraw('tag_tuple'))
|
||
self.assertIn('c', m.getraw('tag_set'))
|
||
self.assertIn('e', m.getraw('tag_dict'))
|
||
self.assertIn('gh', m.getraw('tag_str'))
|
||
|
||
def test_compare_to_release(self):
|
||
release = load_test_json('release.json')
|
||
metadata = Metadata()
|
||
release_to_metadata(release, metadata)
|
||
match = metadata.compare_to_release(release, Cluster.comparison_weights)
|
||
self.assertEqual(1.0, match.similarity)
|
||
self.assertEqual(release, match.release)
|
||
|
||
def test_compare_to_release_with_score(self):
|
||
release = load_test_json('release.json')
|
||
metadata = Metadata()
|
||
release_to_metadata(release, metadata)
|
||
for score, sim in ((42, 0.42), ('42', 0.42), ('foo', 1.0), (None, 1.0)):
|
||
release['score'] = score
|
||
match = metadata.compare_to_release(release, Cluster.comparison_weights)
|
||
self.assertEqual(sim, match.similarity)
|
||
|
||
def test_weights_from_release_type_scores(self):
|
||
release = load_test_json('release.json')
|
||
parts = []
|
||
weights_from_release_type_scores(parts, release, {'Album': 0.75}, 666)
|
||
self.assertEqual(
|
||
parts[0],
|
||
(0.75, 666)
|
||
)
|
||
weights_from_release_type_scores(parts, release, {}, 666)
|
||
self.assertEqual(
|
||
parts[1],
|
||
(0.5, 666)
|
||
)
|
||
del release['release-group']
|
||
weights_from_release_type_scores(parts, release, {}, 777)
|
||
self.assertEqual(
|
||
parts[2],
|
||
(0.0, 777)
|
||
)
|
||
|
||
def test_preferred_countries(self):
|
||
release = load_test_json('release.json')
|
||
parts = []
|
||
weights_from_preferred_countries(parts, release, [], 666)
|
||
self.assertFalse(parts)
|
||
weights_from_preferred_countries(parts, release, ['FR'], 666)
|
||
self.assertEqual(parts[0], (0.0, 666))
|
||
weights_from_preferred_countries(parts, release, ['GB'], 666)
|
||
self.assertEqual(parts[1], (1.0, 666))
|
||
|
||
def test_preferred_formats(self):
|
||
release = load_test_json('release.json')
|
||
parts = []
|
||
weights_from_preferred_formats(parts, release, [], 777)
|
||
self.assertFalse(parts)
|
||
weights_from_preferred_formats(parts, release, ['Digital Media'], 777)
|
||
self.assertEqual(parts[0], (0.0, 777))
|
||
weights_from_preferred_formats(parts, release, ['12" Vinyl'], 777)
|
||
self.assertEqual(parts[1], (1.0, 777))
|
||
|
||
def test_compare_to_track(self):
|
||
track_json = load_test_json('track.json')
|
||
track = Track(track_json['id'])
|
||
track_to_metadata(track_json, track)
|
||
match = track.metadata.compare_to_track(track_json, File.comparison_weights)
|
||
self.assertEqual(1.0, match.similarity)
|
||
self.assertEqual(track_json, match.track)
|
||
|
||
def test_compare_to_track_with_score(self):
|
||
track_json = load_test_json('track.json')
|
||
track = Track(track_json['id'])
|
||
track_to_metadata(track_json, track)
|
||
for score, sim in ((42, 0.42), ('42', 0.42), ('foo', 1.0), (None, 1.0)):
|
||
track_json['score'] = score
|
||
match = track.metadata.compare_to_track(track_json, File.comparison_weights)
|
||
self.assertEqual(sim, match.similarity)
|