/
/
1"""Tests for mediaitem compare helper functions."""
2
3from music_assistant_models import media_items
4from music_assistant_models.enums import ExternalID
5
6from music_assistant.helpers import compare
7
8
9def test_compare_version() -> None:
10 """Test the version compare helper."""
11 assert compare.compare_version("Remaster", "remaster") is True
12 assert compare.compare_version("Remastered", "remaster") is True
13 assert compare.compare_version("Remaster", "") is False
14 assert compare.compare_version("Remaster", "Remix") is False
15 assert compare.compare_version("", "Deluxe") is False
16 assert compare.compare_version("", "Live") is False
17 assert compare.compare_version("Live", "live") is True
18 assert compare.compare_version("Live", "live version") is True
19 assert compare.compare_version("Live version", "live") is True
20 assert compare.compare_version("Deluxe Edition", "Deluxe") is True
21 assert compare.compare_version("Deluxe Karaoke Edition", "Deluxe") is False
22 assert compare.compare_version("Deluxe Karaoke Edition", "Karaoke") is False
23 assert compare.compare_version("Deluxe Edition", "Edition Deluxe") is True
24 assert compare.compare_version("", "Karaoke Version") is False
25 assert compare.compare_version("Karaoke", "Karaoke Version") is True
26 assert compare.compare_version("Remaster", "Remaster Edition Deluxe") is False
27 assert compare.compare_version("Remastered Version", "Deluxe Version") is False
28 assert compare.compare_version("2011 Remaster", "Remastered 2011") is True
29 assert compare.compare_version("", "Album Version") is True
30 assert compare.compare_version("Deluxe 2022 Remaster", "2022 Remaster") is False
31
32
33def test_compare_artist() -> None:
34 """Test artist comparison."""
35 artist_a = media_items.Artist(
36 item_id="1",
37 provider="test1",
38 name="Artist A",
39 provider_mappings={
40 media_items.ProviderMapping(
41 item_id="1", provider_domain="test", provider_instance="test1"
42 )
43 },
44 )
45 artist_b = media_items.Artist(
46 item_id="1",
47 provider="test2",
48 name="Artist A",
49 provider_mappings={
50 media_items.ProviderMapping(
51 item_id="2", provider_domain="test", provider_instance="test2"
52 )
53 },
54 )
55 # test match on name match
56 assert compare.compare_artist(artist_a, artist_b) is True
57 # test match on name mismatch
58 artist_b.name = "Artist B"
59 assert compare.compare_artist(artist_a, artist_b) is False
60 # test on exact item_id match
61 artist_b.item_id = artist_a.item_id
62 artist_b.provider = artist_a.provider
63 assert compare.compare_artist(artist_a, artist_b) is True
64 # test on external id match
65 artist_b.name = "Artist B"
66 artist_b.item_id = "2"
67 artist_b.provider = "test2"
68 artist_a.external_ids = {(ExternalID.MB_ARTIST, "123")}
69 artist_b.external_ids = artist_a.external_ids
70 assert compare.compare_artist(artist_a, artist_b) is True
71 # test on external id mismatch
72 artist_b.name = artist_a.name
73 artist_b.external_ids = {(ExternalID.MB_ARTIST, "1234")}
74 assert compare.compare_artist(artist_a, artist_b) is False
75 # test on external id mismatch while name matches
76 artist_a = media_items.Artist(
77 item_id="1",
78 provider="test1",
79 name="Artist A",
80 external_ids={(ExternalID.MB_ARTIST, "123")},
81 provider_mappings={
82 media_items.ProviderMapping(
83 item_id="1", provider_domain="test", provider_instance="test1"
84 )
85 },
86 )
87 artist_b = media_items.Artist(
88 item_id="1",
89 provider="test2",
90 name="Artist A",
91 external_ids={(ExternalID.MB_ARTIST, "abc")},
92 provider_mappings={
93 media_items.ProviderMapping(
94 item_id="2", provider_domain="test", provider_instance="test2"
95 )
96 },
97 )
98 assert compare.compare_artist(artist_a, artist_b) is False
99
100
101def test_compare_album() -> None:
102 """Test album comparison."""
103 album_a = media_items.Album(
104 item_id="1",
105 provider="test1",
106 name="Album A",
107 provider_mappings={
108 media_items.ProviderMapping(
109 item_id="1", provider_domain="test", provider_instance="test1"
110 )
111 },
112 artists=media_items.UniqueList(
113 [
114 media_items.Artist(
115 item_id="1",
116 provider="test1",
117 name="Artist A",
118 provider_mappings={
119 media_items.ProviderMapping(
120 item_id="1", provider_domain="test", provider_instance="test1"
121 )
122 },
123 )
124 ]
125 ),
126 )
127 album_b = media_items.Album(
128 item_id="1",
129 provider="test2",
130 name="Album A",
131 provider_mappings={
132 media_items.ProviderMapping(
133 item_id="2", provider_domain="test", provider_instance="test2"
134 )
135 },
136 artists=media_items.UniqueList(
137 [
138 media_items.Artist(
139 item_id="1",
140 provider="test1",
141 name="Artist A",
142 provider_mappings={
143 media_items.ProviderMapping(
144 item_id="1", provider_domain="test", provider_instance="test1"
145 )
146 },
147 )
148 ]
149 ),
150 )
151 # test match on name match
152 assert compare.compare_album(album_a, album_b) is True
153 # test match on name mismatch
154 album_b.name = "Album B"
155 assert compare.compare_album(album_a, album_b) is False
156 # test on version mismatch
157 album_b.name = album_a.name
158 album_b.version = "Deluxe"
159 assert compare.compare_album(album_a, album_b) is False
160 album_b.version = "Remix"
161 assert compare.compare_album(album_a, album_b) is False
162 # test on version match
163 album_b.name = album_a.name
164 album_a.version = "Deluxe"
165 album_b.version = "Deluxe Edition"
166 assert compare.compare_album(album_a, album_b) is True
167 # test on exact item_id match
168 album_b.item_id = album_a.item_id
169 album_b.provider = album_a.provider
170 assert compare.compare_album(album_a, album_b) is True
171 # test on external id match
172 album_b.name = "Album B"
173 album_b.item_id = "2"
174 album_b.provider = "test2"
175 album_a.external_ids = {(ExternalID.MB_ALBUM, "123")}
176 album_b.external_ids = album_a.external_ids
177 assert compare.compare_album(album_a, album_b) is True
178 # test on external id mismatch
179 album_b.name = album_a.name
180 album_b.external_ids = {(ExternalID.MB_ALBUM, "1234")}
181 assert compare.compare_album(album_a, album_b) is False
182 album_a.external_ids = set()
183 album_b.external_ids = set()
184 # fail on year mismatch
185 album_b.external_ids = set()
186 album_a.year = 2021
187 album_b.year = 2020
188 assert compare.compare_album(album_a, album_b) is False
189 # pass on year match
190 album_b.year = 2021
191 assert compare.compare_album(album_a, album_b) is True
192 # fail on artist mismatch
193 album_a.artists = media_items.UniqueList(
194 [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")]
195 )
196 album_b.artists = media_items.UniqueList(
197 [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")]
198 )
199 assert compare.compare_album(album_a, album_b) is False
200 # pass on partial artist match (if first artist matches)
201 album_a.artists = media_items.UniqueList(
202 [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")]
203 )
204 album_b.artists = media_items.UniqueList(
205 [
206 media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"),
207 media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"),
208 ]
209 )
210 assert compare.compare_album(album_a, album_b) is True
211 # fail on partial artist match in strict mode
212 album_b.artists = media_items.UniqueList(
213 [
214 media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"),
215 media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"),
216 ]
217 )
218 assert compare.compare_album(album_a, album_b) is False
219 # partial artist match is allowed in non-strict mode
220 assert compare.compare_album(album_a, album_b, False) is True
221
222
223def test_compare_album_barcode_requires_corroboration() -> None:
224 """A shared retail barcode only overrides year when album identity also agrees."""
225 artist_a = media_items.Artist(
226 item_id="1",
227 provider="test1",
228 name="Artist A",
229 provider_mappings={
230 media_items.ProviderMapping(
231 item_id="1", provider_domain="test", provider_instance="test1"
232 )
233 },
234 )
235 artist_b = media_items.Artist(
236 item_id="2",
237 provider="test2",
238 name="Artist A",
239 provider_mappings={
240 media_items.ProviderMapping(
241 item_id="2", provider_domain="test", provider_instance="test2"
242 )
243 },
244 )
245 barcode_a = (ExternalID.BARCODE, "000724354283857")
246 barcode_b = (ExternalID.BARCODE, "0724354283857")
247 album_a = media_items.Album(
248 item_id="1",
249 provider="test1",
250 name="#1",
251 year=2001,
252 artists=media_items.UniqueList([artist_a]),
253 external_ids={barcode_a},
254 provider_mappings={
255 media_items.ProviderMapping(
256 item_id="1", provider_domain="test", provider_instance="test1"
257 )
258 },
259 )
260 album_b = media_items.Album(
261 item_id="2",
262 provider="test2",
263 name="#1",
264 year=2002,
265 artists=media_items.UniqueList([artist_b]),
266 external_ids={barcode_b},
267 provider_mappings={
268 media_items.ProviderMapping(
269 item_id="2", provider_domain="test", provider_instance="test2"
270 )
271 },
272 )
273
274 assert compare.compare_album(album_a, album_b) is True
275
276 album_b.name = "Different Album"
277 assert compare.compare_album(album_a, album_b) is False
278 album_b.name = album_a.name
279 album_b.artists[0].name = "Different Artist"
280 assert compare.compare_album(album_a, album_b) is False
281
282
283def test_compare_external_ids_checks_all_unique_values() -> None:
284 """One mismatching unique identifier does not hide another matching value."""
285 base_ids = {
286 (ExternalID.MB_ALBUM, "11111111-1111-1111-1111-111111111111"),
287 (ExternalID.MB_ALBUM, "22222222-2222-2222-2222-222222222222"),
288 }
289 compare_ids = {(ExternalID.MB_ALBUM, "22222222-2222-2222-2222-222222222222")}
290
291 assert compare.compare_external_ids(base_ids, compare_ids, ExternalID.MB_ALBUM) is True
292
293
294def test_compare_track() -> None: # noqa: PLR0915
295 """Test track comparison."""
296 track_a = media_items.Track(
297 item_id="1",
298 provider="test1",
299 name="Track A",
300 provider_mappings={
301 media_items.ProviderMapping(
302 item_id="1", provider_domain="test", provider_instance="test1"
303 )
304 },
305 artists=media_items.UniqueList(
306 [
307 media_items.Artist(
308 item_id="1",
309 provider="test1",
310 name="Artist A",
311 provider_mappings={
312 media_items.ProviderMapping(
313 item_id="1", provider_domain="test", provider_instance="test1"
314 )
315 },
316 )
317 ]
318 ),
319 )
320 track_b = media_items.Track(
321 item_id="1",
322 provider="test2",
323 name="Track A",
324 provider_mappings={
325 media_items.ProviderMapping(
326 item_id="2", provider_domain="test", provider_instance="test2"
327 )
328 },
329 artists=media_items.UniqueList(
330 [
331 media_items.Artist(
332 item_id="1",
333 provider="test1",
334 name="Artist A",
335 provider_mappings={
336 media_items.ProviderMapping(
337 item_id="1", provider_domain="test", provider_instance="test1"
338 )
339 },
340 )
341 ]
342 ),
343 )
344 # test match on name match
345 assert compare.compare_track(track_a, track_b) is True
346 # test match on name mismatch
347 track_b.name = "Track B"
348 assert compare.compare_track(track_a, track_b) is False
349 # test on version mismatch
350 track_b.name = track_a.name
351 track_b.version = "Deluxe"
352 assert compare.compare_track(track_a, track_b) is False
353 track_b.version = "Remix"
354 assert compare.compare_track(track_a, track_b) is False
355 # test on version mismatch
356 track_b.name = track_a.name
357 track_a.version = ""
358 track_b.version = "Remaster"
359 assert compare.compare_track(track_a, track_b) is False
360 track_b.version = "Remix"
361 assert compare.compare_track(track_a, track_b) is False
362 # test on version match
363 track_b.name = track_a.name
364 track_a.version = "Deluxe"
365 track_b.version = "Deluxe Edition"
366 assert compare.compare_track(track_a, track_b) is True
367 # test on exact item_id match
368 track_b.item_id = track_a.item_id
369 track_b.provider = track_a.provider
370 assert compare.compare_track(track_a, track_b) is True
371 # test on external id match
372 track_b.name = "Track B"
373 track_b.item_id = "2"
374 track_b.provider = "test2"
375 track_a.external_ids = {(ExternalID.MB_RECORDING, "123")}
376 track_b.external_ids = track_a.external_ids
377 assert compare.compare_track(track_a, track_b) is True
378 # test on external id mismatch
379 track_b.name = track_a.name
380 track_b.external_ids = {(ExternalID.MB_RECORDING, "1234")}
381 assert compare.compare_track(track_a, track_b) is False
382 track_a.external_ids = set()
383 track_b.external_ids = set()
384 # fail on artist mismatch
385 track_a.artists = media_items.UniqueList(
386 [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")]
387 )
388 track_b.artists = media_items.UniqueList(
389 [media_items.ItemMapping(item_id="2", provider="test1", name="Artist B")]
390 )
391 assert compare.compare_track(track_a, track_b) is False
392 # pass on partial artist match (if first artist matches)
393 track_a.artists = media_items.UniqueList(
394 [media_items.ItemMapping(item_id="1", provider="test1", name="Artist A")]
395 )
396 track_b.artists = media_items.UniqueList(
397 [
398 media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"),
399 media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"),
400 ]
401 )
402 assert compare.compare_track(track_a, track_b) is True
403 # fail on partial artist match in strict mode
404 track_b.artists = media_items.UniqueList(
405 [
406 media_items.ItemMapping(item_id="2", provider="test1", name="Artist B"),
407 media_items.ItemMapping(item_id="1", provider="test1", name="Artist A"),
408 ]
409 )
410 assert compare.compare_track(track_a, track_b) is False
411 # partial artist match is allowed in non-strict mode
412 assert compare.compare_track(track_a, track_b, False) is True
413 track_b.artists = track_a.artists
414 # fail on album mismatch
415 track_a.album = media_items.ItemMapping(item_id="1", provider="test1", name="Album A")
416 track_b.album = media_items.ItemMapping(item_id="2", provider="test1", name="Album B")
417 assert compare.compare_track(track_a, track_b) is False
418 # pass on exact album(track) match (regardless duration)
419 track_b.album = track_a.album
420 track_a.disc_number = 1
421 track_a.track_number = 1
422 track_b.disc_number = track_a.disc_number
423 track_b.track_number = track_a.track_number
424 track_a.duration = 300
425 track_b.duration = 310
426 assert compare.compare_track(track_a, track_b) is True
427 # pass on album(track) mismatch
428 track_b.album = track_a.album
429 track_a.disc_number = 1
430 track_a.track_number = 1
431 track_b.disc_number = track_a.disc_number
432 track_b.track_number = 2
433 track_b.duration = track_a.duration
434 assert compare.compare_track(track_a, track_b) is False
435 # test special case - ISRC match but MusicBrainz ID mismatch
436 # this can happen for some classical music albums
437 track_a.external_ids = {
438 (ExternalID.ISRC, "123"),
439 (ExternalID.MB_RECORDING, "abc"),
440 }
441 track_b.external_ids = {
442 (ExternalID.ISRC, "123"),
443 (ExternalID.MB_RECORDING, "abcd"),
444 }
445 assert compare.compare_track(track_a, track_b) is False
446 # test multi-disc: same album, same external IDs, different disc numbers should NOT match
447 track_a.external_ids = {(ExternalID.MB_RECORDING, "same-recording-id")}
448 track_b.external_ids = {(ExternalID.MB_RECORDING, "same-recording-id")}
449 track_a.album = media_items.ItemMapping(item_id="1", provider="test1", name="Album A")
450 track_b.album = media_items.ItemMapping(item_id="1", provider="test1", name="Album A")
451 track_a.disc_number = 1
452 track_b.disc_number = 2
453 track_a.track_number = 3
454 track_b.track_number = 3
455 assert compare.compare_track(track_a, track_b) is False
456 # same disc number should still match via external ID
457 track_b.disc_number = 1
458 assert compare.compare_track(track_a, track_b) is True
459 # different disc but different albums should still match via external ID
460 track_b.disc_number = 2
461 track_b.album = media_items.ItemMapping(item_id="2", provider="test1", name="Album B")
462 assert compare.compare_track(track_a, track_b) is True
463
464
465def test_compare_strings_case_insensitive_fuzzy() -> None:
466 """Test that non-strict fuzzy matching is fully case-insensitive."""
467 # These differ slightly ("Feat." vs "FT.") so create_safe_string won't match,
468 # falling through to SequenceMatcher which must compare both strings lowered.
469 assert compare.compare_strings("Track Feat. John", "TRACK FT. JOHN", strict=False) is True
470
471
472def test_compare_radio() -> None:
473 """Test the radio compare helper."""
474
475 def _radio(
476 item_id: str, provider: str, name: str, *, is_dynamic: bool = False
477 ) -> media_items.Radio:
478 return media_items.Radio(
479 item_id=item_id,
480 provider=provider,
481 name=name,
482 is_dynamic=is_dynamic,
483 provider_mappings={
484 media_items.ProviderMapping(
485 item_id=item_id, provider_domain=provider, provider_instance=provider
486 )
487 },
488 )
489
490 live_a = _radio("a", "tunein", "Chill Vibes")
491 live_b = _radio("b", "radiobrowser", "Chill Vibes")
492 # a live station is matched across providers on its name
493 assert compare.compare_radio(live_a, live_b) is True
494
495 station = _radio("c", "pandora", "Chill Vibes", is_dynamic=True)
496 # a dynamic station only exists on its own provider, so the name is not enough
497 assert compare.compare_radio(station, live_a) is False
498 assert compare.compare_radio(live_a, station) is False
499 # ... but it is still recognised as itself
500 same = _radio("c", "pandora", "Chill Vibes", is_dynamic=True)
501 assert compare.compare_radio(station, same) is True
502