/
/
/
1"""Tests for the Recently played built-in playlist."""
2
3from __future__ import annotations
4
5from unittest.mock import AsyncMock, MagicMock
6
7import pytest
8from music_assistant_models.enums import MediaType
9from music_assistant_models.media_items import ItemMapping, Track
10
11from music_assistant.providers.builtin import BuiltinProvider
12
13
14def _make_provider() -> BuiltinProvider:
15 """Return a BuiltinProvider instance with a minimal mock mass."""
16 provider = BuiltinProvider.__new__(BuiltinProvider)
17 provider.mass = MagicMock()
18 # instance_id and domain are read-only properties backed by config/manifest
19 provider.config = MagicMock()
20 provider.config.instance_id = "builtin"
21 provider.manifest = MagicMock()
22 provider.manifest.domain = "builtin"
23 return provider
24
25
26@pytest.mark.asyncio
27async def test_recently_played_resolves_library_rows() -> None:
28 """A playlog row for a library-originated play is resolved to its library track."""
29 provider = _make_provider()
30 library_track = Track(
31 item_id="42",
32 provider="library",
33 name="Library Track",
34 provider_mappings=set(),
35 )
36 provider.mass.music.tracks.get_library_items_by_query = AsyncMock( # type: ignore[method-assign]
37 return_value=[library_track]
38 )
39 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
40 return_value=[
41 ItemMapping(
42 item_id="42",
43 provider="library",
44 name="Library Track",
45 media_type=MediaType.TRACK,
46 )
47 ]
48 )
49
50 result = await provider._get_builtin_playlist_recently_played()
51
52 provider.mass.music.tracks.get_library_items_by_query.assert_awaited_once_with(
53 extra_query_parts=["tracks.item_id IN :item_ids"],
54 extra_query_params={"item_ids": [42]},
55 in_library_only=False,
56 )
57 assert result == [library_track]
58 assert library_track.position == 1
59
60
61@pytest.mark.asyncio
62async def test_recently_played_skips_library_row_removed_from_library() -> None:
63 """A library row is skipped when the batched lookup returns no track for it."""
64 provider = _make_provider()
65 provider.mass.music.tracks.get_library_items_by_query = AsyncMock( # type: ignore[method-assign]
66 return_value=[]
67 )
68 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
69 return_value=[
70 ItemMapping(
71 item_id="99",
72 provider="library",
73 name="Deleted Track",
74 media_type=MediaType.TRACK,
75 )
76 ]
77 )
78
79 result = await provider._get_builtin_playlist_recently_played()
80
81 assert result == []
82
83
84@pytest.mark.asyncio
85async def test_recently_played_skips_library_query_when_no_library_rows() -> None:
86 """The batched library query is skipped entirely when no row is library-originated."""
87 provider = _make_provider()
88 item_provider = MagicMock()
89 item_provider.domain = "spotify"
90 item_provider.instance_id = "spotify--test"
91 provider.mass.get_provider = MagicMock(return_value=item_provider) # type: ignore[method-assign]
92 provider.mass.music.tracks.get_library_items_by_query = AsyncMock() # type: ignore[method-assign]
93 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
94 return_value=[
95 ItemMapping(
96 item_id="track123",
97 provider="spotify--test",
98 name="Streamed Track",
99 media_type=MediaType.TRACK,
100 )
101 ]
102 )
103
104 result = await provider._get_builtin_playlist_recently_played()
105
106 provider.mass.music.tracks.get_library_items_by_query.assert_not_awaited()
107 assert len(result) == 1
108 assert result[0].item_id == "track123"
109
110
111@pytest.mark.asyncio
112async def test_recently_played_rereads_playlog_on_every_call() -> None:
113 """Consecutive calls each re-read the playlog instead of sharing a cached result."""
114 provider = _make_provider()
115 first = ItemMapping(item_id="1", provider="library", name="First", media_type=MediaType.TRACK)
116 second = ItemMapping(item_id="2", provider="library", name="Second", media_type=MediaType.TRACK)
117 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
118 side_effect=[[first], [second]]
119 )
120 provider.mass.music.tracks.get_library_items_by_query = AsyncMock( # type: ignore[method-assign]
121 side_effect=[
122 [Track(item_id="1", provider="library", name="First", provider_mappings=set())],
123 [Track(item_id="2", provider="library", name="Second", provider_mappings=set())],
124 ]
125 )
126
127 first_result = await provider._get_builtin_playlist_recently_played()
128 second_result = await provider._get_builtin_playlist_recently_played()
129
130 assert provider.mass.music.recently_played.await_count == 2
131 assert [t.item_id for t in first_result] == ["1"]
132 assert [t.item_id for t in second_result] == ["2"]
133
134
135@pytest.mark.asyncio
136async def test_recently_played_builds_stub_track_for_real_provider() -> None:
137 """A row naming a real provider instance still yields a stub Track for that provider."""
138 provider = _make_provider()
139 item_provider = MagicMock()
140 item_provider.domain = "spotify"
141 item_provider.instance_id = "spotify--test"
142 provider.mass.get_provider = MagicMock(return_value=item_provider) # type: ignore[method-assign]
143 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
144 return_value=[
145 ItemMapping(
146 item_id="track123",
147 provider="spotify--test",
148 name="Streamed Track",
149 media_type=MediaType.TRACK,
150 )
151 ]
152 )
153
154 result = await provider._get_builtin_playlist_recently_played()
155
156 assert len(result) == 1
157 track = result[0]
158 assert track.item_id == "track123"
159 assert track.name == "Streamed Track"
160 assert track.position == 1
161 mapping = next(iter(track.provider_mappings))
162 assert mapping.provider_domain == "spotify"
163 assert mapping.provider_instance == "spotify--test"
164
165
166@pytest.mark.asyncio
167async def test_recently_played_skips_row_for_unknown_provider() -> None:
168 """A row naming a provider instance that is no longer registered is dropped."""
169 provider = _make_provider()
170 provider.mass.get_provider = MagicMock(return_value=None) # type: ignore[method-assign]
171 provider.mass.music.recently_played = AsyncMock( # type: ignore[method-assign]
172 return_value=[
173 ItemMapping(
174 item_id="track123",
175 provider="removed_provider",
176 name="Orphaned Track",
177 media_type=MediaType.TRACK,
178 )
179 ]
180 )
181
182 result = await provider._get_builtin_playlist_recently_played()
183
184 assert result == []
185