/
/
/
1"""Tests for the media resolver's get_tracks_for_playback umbrella dispatch."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING, cast
6from unittest.mock import AsyncMock, MagicMock
7
8import pytest
9from music_assistant_models.enums import MediaType
10from music_assistant_models.media_items import Audiobook, MediaCollection, Track, UniqueList
11
12from music_assistant.controllers.player_queues.media_resolver import MediaResolver
13
14if TYPE_CHECKING:
15 from music_assistant_models.media_items import MediaItemType
16
17
18def _trk(item_id: str) -> Track:
19 return Track(item_id=item_id, provider="library", name=item_id, provider_mappings=set())
20
21
22def _resolver() -> MediaResolver:
23 resolver = MediaResolver.__new__(MediaResolver)
24 resolver.get_artist_tracks = AsyncMock(return_value=[_trk("artist")]) # type: ignore[method-assign]
25 resolver.get_album_tracks = AsyncMock(return_value=[_trk("album")]) # type: ignore[method-assign]
26 resolver.get_genre_tracks = AsyncMock(return_value=[_trk("genre")]) # type: ignore[method-assign]
27 resolver.get_playlist_tracks = AsyncMock(return_value=[]) # type: ignore[method-assign]
28 return resolver
29
30
31def _item(media_type: MediaType) -> MediaItemType:
32 item = MagicMock()
33 item.media_type = media_type
34 return cast("MediaItemType", item)
35
36
37@pytest.mark.asyncio
38async def test_track_resolves_to_itself() -> None:
39 """A track resolves to itself without touching the per-type resolvers."""
40 resolver = _resolver()
41 track = _trk("solo")
42 assert await resolver.get_tracks_for_playback(track) == [track]
43
44
45@pytest.mark.asyncio
46async def test_album_routes_to_get_album_tracks() -> None:
47 """An album is resolved through the config-driven get_album_tracks."""
48 resolver = _resolver()
49 result = await resolver.get_tracks_for_playback(_item(MediaType.ALBUM))
50 assert [t.item_id for t in result] == ["album"]
51
52
53@pytest.mark.asyncio
54async def test_artist_routes_to_get_artist_tracks() -> None:
55 """An artist is resolved through the config-driven get_artist_tracks."""
56 resolver = _resolver()
57 result = await resolver.get_tracks_for_playback(_item(MediaType.ARTIST))
58 assert [t.item_id for t in result] == ["artist"]
59
60
61@pytest.mark.asyncio
62async def test_genre_routes_to_get_genre_tracks() -> None:
63 """A genre is resolved through get_genre_tracks."""
64 resolver = _resolver()
65 result = await resolver.get_tracks_for_playback(_item(MediaType.GENRE))
66 assert [t.item_id for t in result] == ["genre"]
67
68
69@pytest.mark.asyncio
70async def test_playlist_filters_to_tracks() -> None:
71 """A playlist resolves to its tracks, dropping any non-track playable items."""
72 resolver = _resolver()
73 track = _trk("p1")
74 resolver.get_playlist_tracks = AsyncMock(return_value=[track, MagicMock()]) # type: ignore[method-assign]
75 assert await resolver.get_tracks_for_playback(_item(MediaType.PLAYLIST)) == [track]
76
77
78@pytest.mark.asyncio
79async def test_unsupported_type_is_empty() -> None:
80 """A type with no playable-track resolution yields an empty list."""
81 resolver = _resolver()
82 assert await resolver.get_tracks_for_playback(_item(MediaType.AUDIOBOOK)) == []
83
84
85@pytest.mark.asyncio
86async def test_audiobook_collection_resolves_existing_items() -> None:
87 """An audiobook collection resolves its model items without deserializing them again."""
88 resolver = _resolver()
89 resolver.mass = MagicMock()
90 resolver.mass.music.get_resume_position = AsyncMock(side_effect=[(True, 0), (False, 12_000)])
91 first = Audiobook(
92 item_id="book-1",
93 provider="library",
94 name="Book 1",
95 provider_mappings=set(),
96 )
97 second = Audiobook(
98 item_id="book-2",
99 provider="library",
100 name="Book 2",
101 provider_mappings=set(),
102 )
103 collection = MediaCollection[Audiobook](
104 item_id="audiobook___Series",
105 provider="library",
106 name="Series",
107 provider_mappings=set(),
108 items=UniqueList([first, second]),
109 )
110
111 result = await resolver._resolve_media_items(collection, userid="user")
112
113 assert result == [second]
114 assert result[0] is second
115 assert second.resume_position_ms == 12_000
116