/
/
/
1"""Tests for the music controller helper functions."""
2
3from __future__ import annotations
4
5from music_assistant_models.enums import MediaType
6from music_assistant_models.media_items import Artist, ItemMapping, ProviderMapping, Track
7from music_assistant_models.unique_list import UniqueList
8
9from music_assistant.controllers.music.helpers import sort_search_result
10
11
12def _mapping(item_id: str, name: str, provider: str = "spotify") -> ItemMapping:
13 """Build a minimal track ItemMapping for sort tests."""
14 return ItemMapping(media_type=MediaType.TRACK, item_id=item_id, provider=provider, name=name)
15
16
17def _track(item_id: str, name: str, provider: str = "spotify", artist: str | None = None) -> Track:
18 """Build a minimal Track (optionally with a single artist) for sort tests."""
19 artists: UniqueList[Artist | ItemMapping] = UniqueList()
20 if artist is not None:
21 artists.append(
22 ItemMapping(
23 media_type=MediaType.ARTIST, item_id=f"ar_{item_id}", provider=provider, name=artist
24 )
25 )
26 return Track(
27 item_id=item_id,
28 provider=provider,
29 name=name,
30 artists=artists,
31 provider_mappings={
32 ProviderMapping(item_id=item_id, provider_domain=provider, provider_instance=provider)
33 },
34 )
35
36
37def test_empty_input_returns_empty_uniquelist() -> None:
38 """An empty input yields an empty UniqueList."""
39 items: list[ItemMapping] = []
40 result = sort_search_result("anything", items)
41 assert isinstance(result, UniqueList)
42 assert result == []
43
44
45def test_no_name_match_keeps_original_order() -> None:
46 """Items whose name does not match the query are returned in their original order."""
47 items = [_mapping("1", "Foo"), _mapping("2", "Bar")]
48 assert sort_search_result("totally different", items) == items
49
50
51def test_exact_name_match_ranked_first() -> None:
52 """An item whose name matches the query is placed before non-matching items."""
53 other = _mapping("1", "Something else")
54 match = _mapping("2", "Hello")
55 result = sort_search_result("hello", [other, match])
56 assert result[0] is match
57 assert other in result
58
59
60def test_match_is_case_insensitive() -> None:
61 """Name matching ignores case (and surrounding noise)."""
62 match = _mapping("1", "HELLO")
63 assert sort_search_result("hello", [match])[0] is match
64
65
66def test_library_item_prioritized_over_streaming() -> None:
67 """Between two exact name matches, the library item ranks first."""
68 streaming = _mapping("1", "Hello", provider="spotify")
69 library = _mapping("2", "Hello", provider="library")
70 result = sort_search_result("hello", [streaming, library])
71 assert result[0] is library
72
73
74def test_artist_query_gives_bonus_to_matching_artist() -> None:
75 """An 'artist - title' query gives a ranking bonus to the matching artist."""
76 by_other = _track("1", "Hello", artist="Lionel Richie")
77 by_adele = _track("2", "Hello", artist="Adele")
78 result = sort_search_result("Adele - Hello", [by_other, by_adele])
79 assert result[0] is by_adele
80 assert by_other in result
81
82
83def test_duplicate_items_are_deduplicated() -> None:
84 """Equal items appearing more than once collapse to a single entry."""
85 item = _mapping("1", "Hello")
86 duplicate = _mapping("1", "Hello")
87 result = sort_search_result("hello", [item, duplicate])
88 assert len(result) == 1
89
90
91def test_all_items_preserved_with_matches_first() -> None:
92 """Every unique item is preserved; scored matches come before the rest."""
93 library_match = _mapping("1", "Hello", provider="library")
94 streaming_match = _mapping("2", "Hello", provider="spotify")
95 non_match = _mapping("3", "Goodbye")
96 result = sort_search_result("hello", [non_match, streaming_match, library_match])
97 assert result[0] is library_match
98 assert result[1] is streaming_match
99 assert non_match in result
100 assert len(result) == 3
101