/
/
/
1"""Tests for the Plex provider's artist top tracks."""
2
3from __future__ import annotations
4
5from typing import Any
6from unittest.mock import AsyncMock, MagicMock
7
8import plexapi.exceptions
9import pytest
10
11from music_assistant.providers.plex import PlexProvider
12from music_assistant.providers.plex.constants import FAKE_ARTIST_PREFIX, MAX_TOP_TRACKS
13from music_assistant.providers.plex.helpers import SUPPORTED_FEATURES
14
15ARTIST_ID = "/library/metadata/12377"
16
17# the cache decorator is bypassed so these tests target the lookup itself
18get_toptracks: Any = PlexProvider.get_artist_toptracks.__wrapped__ # type: ignore[attr-defined]
19
20
21class _FakePlexTrack:
22 """Minimal PlexTrack stub carrying only what the ranking reads."""
23
24 def __init__(self, key: str, title: str, rating_count: int | None = None) -> None:
25 self.key = key
26 self.title = title
27 self.ratingCount = rating_count
28
29
30class _FakePlexArtist:
31 """
32 Minimal PlexArtist stub.
33
34 Deliberately has no station() attribute: a Plex artist station is a radio playlist,
35 which plexapi never enumerates, so touching it here fails the test loudly.
36 """
37
38 def __init__(
39 self,
40 popular_tracks: list[_FakePlexTrack] | None = None,
41 own_tracks: list[_FakePlexTrack] | None = None,
42 ) -> None:
43 self._popular_tracks = popular_tracks
44 self._own_tracks = own_tracks or []
45
46 def popularTracks(self) -> list[_FakePlexTrack]: # noqa: N802
47 """Return the canned popular tracks, or fail like a server without filter metadata."""
48 if self._popular_tracks is None:
49 raise plexapi.exceptions.NotFound('Unknown libtype "artist"')
50 return self._popular_tracks
51
52 def tracks(self) -> list[_FakePlexTrack]:
53 """Return the artist's own tracks."""
54 return self._own_tracks
55
56
57def _make_provider(plex_artist: _FakePlexArtist) -> Any:
58 """Create a PlexProvider that resolves any artist id to the given stub."""
59 mock_config = MagicMock()
60 mock_config.instance_id = "plex_instance_1"
61 mock_config.get_value = lambda key: "INFO" if key == "log_level" else None
62 mock_manifest = MagicMock()
63 mock_manifest.domain = "plex"
64
65 provider = PlexProvider(MagicMock(), mock_manifest, mock_config, SUPPORTED_FEATURES)
66 provider._get_data = AsyncMock(return_value=plex_artist) # type: ignore[method-assign]
67 provider._run_async = AsyncMock( # type: ignore[method-assign]
68 side_effect=lambda call, *args, **kwargs: call(*args, **kwargs)
69 )
70 # parsing is covered elsewhere; return the key so ordering is easy to assert
71 provider._parse_track = AsyncMock( # type: ignore[method-assign]
72 side_effect=lambda plex_track: plex_track.key
73 )
74 return provider
75
76
77@pytest.mark.asyncio
78async def test_top_tracks_come_from_popular_tracks() -> None:
79 """Top tracks are Plex's popular tracks, in the order Plex ranked them."""
80 artist = _FakePlexArtist(
81 popular_tracks=[
82 _FakePlexTrack("/1", "First"),
83 _FakePlexTrack("/2", "Second"),
84 _FakePlexTrack("/3", "Third"),
85 ]
86 )
87 result = await get_toptracks(_make_provider(artist), ARTIST_ID)
88 assert result == ["/1", "/2", "/3"]
89
90
91@pytest.mark.asyncio
92async def test_top_tracks_are_capped() -> None:
93 """No more than MAX_TOP_TRACKS are returned."""
94 artist = _FakePlexArtist(
95 popular_tracks=[_FakePlexTrack(f"/{i}", f"Track {i}") for i in range(MAX_TOP_TRACKS + 5)]
96 )
97 result = await get_toptracks(_make_provider(artist), ARTIST_ID)
98 assert len(result) == MAX_TOP_TRACKS
99
100
101@pytest.mark.asyncio
102async def test_fallback_ranks_own_tracks_when_filters_unavailable() -> None:
103 """
104 Servers without filter metadata fall back to ranking the artist's own tracks.
105
106 The highest ranked version of each title wins, and unranked tracks are dropped.
107 """
108 artist = _FakePlexArtist(
109 popular_tracks=None,
110 own_tracks=[
111 _FakePlexTrack("/1", "Hit", 500),
112 _FakePlexTrack("/2", "hit", 900),
113 _FakePlexTrack("/3", "Deep Cut", 10),
114 _FakePlexTrack("/4", "No Data", None),
115 _FakePlexTrack("/5", "Never Scrobbled", 0),
116 ],
117 )
118 result = await get_toptracks(_make_provider(artist), ARTIST_ID)
119 assert result == ["/2", "/3"]
120
121
122@pytest.mark.asyncio
123async def test_fake_artist_returns_empty() -> None:
124 """A placeholder artist is never looked up on the server."""
125 provider = _make_provider(_FakePlexArtist())
126 result = await get_toptracks(provider, f"{FAKE_ARTIST_PREFIX}Some Artist")
127 assert result == []
128 provider._get_data.assert_not_called()
129