/
/
/
1"""Tests for the crossfade guard that keeps tracks of one album gapless."""
2
3from __future__ import annotations
4
5from typing import Any, cast
6from unittest.mock import MagicMock
7
8from music_assistant_models.enums import CrossfadeMode, MediaType
9from music_assistant_models.media_items import Album, ItemMapping, ProviderMapping, Track
10from music_assistant_models.queue_item import QueueItem
11
12from music_assistant.controllers.streams.audio import StreamsAudio
13
14PROVIDER_ALBUM = ItemMapping(
15 media_type=MediaType.ALBUM,
16 item_id="album-prov-1",
17 provider="spotify--abc",
18 name="Kind of Blue",
19)
20OTHER_PROVIDER_ALBUM = ItemMapping(
21 media_type=MediaType.ALBUM,
22 item_id="album-prov-2",
23 provider="spotify--abc",
24 name="Sketches of Spain",
25)
26LIBRARY_ALBUM = Album(
27 item_id="7",
28 provider="library",
29 name="Kind of Blue",
30 provider_mappings={
31 ProviderMapping(
32 item_id="album-prov-1",
33 provider_domain="spotify",
34 provider_instance="spotify--abc",
35 )
36 },
37)
38
39
40def _queue_item(item_id: str, album: Album | ItemMapping) -> QueueItem:
41 """Build a queue item holding a track on the given album."""
42 return QueueItem(
43 queue_id="queue-1",
44 queue_item_id=item_id,
45 name=item_id,
46 duration=300,
47 media_item=Track(
48 item_id=item_id,
49 provider="spotify--abc",
50 name=item_id,
51 provider_mappings={
52 ProviderMapping(
53 item_id=item_id,
54 provider_domain="spotify",
55 provider_instance="spotify--abc",
56 )
57 },
58 album=album,
59 ),
60 )
61
62
63def _audio(next_item: QueueItem, allow_same_album: bool = False) -> StreamsAudio:
64 """Build a StreamsAudio whose queue reports the given (unloaded) next item."""
65 mass = MagicMock()
66 mass.player_queues.get.return_value = MagicMock()
67 mass.players.get_player.return_value = MagicMock()
68 mass.player_queues.get_next_item.return_value = next_item
69 mass.config.get_raw_core_config_value.return_value = allow_same_album
70 return StreamsAudio(cast("Any", mass))
71
72
73def _crossfade_allowed(current: QueueItem, next_item: QueueItem, **kwargs: Any) -> bool:
74 """Run the guard the way the flow stream does: only the current item is loaded."""
75 return _audio(next_item, **kwargs).crossfade_allowed(
76 current,
77 crossfade_mode=CrossfadeMode.STANDARD_CROSSFADE,
78 player_id="queue-1",
79 flow_mode=True,
80 )
81
82
83def test_same_album_from_one_provider_is_not_crossfaded() -> None:
84 """Two tracks that both still carry the provider album are recognised as one album."""
85 assert not _crossfade_allowed(
86 _queue_item("track-1", PROVIDER_ALBUM), _queue_item("track-2", PROVIDER_ALBUM)
87 )
88
89
90def test_library_album_matches_the_provider_album_of_an_unloaded_next_item() -> None:
91 """
92 A loaded item carries the library album while the next item still carries the provider one.
93
94 Both describe the same album, so the boundary must stay gapless.
95 """
96 assert not _crossfade_allowed(
97 _queue_item("track-1", LIBRARY_ALBUM), _queue_item("track-2", PROVIDER_ALBUM)
98 )
99
100
101def test_different_albums_are_crossfaded() -> None:
102 """A boundary between two genuinely different albums still gets its crossfade."""
103 assert _crossfade_allowed(
104 _queue_item("track-1", LIBRARY_ALBUM), _queue_item("track-2", OTHER_PROVIDER_ALBUM)
105 )
106
107
108def test_same_album_is_crossfaded_when_the_user_allows_it() -> None:
109 """The same-album guard stays overridable by the streams config option."""
110 assert _crossfade_allowed(
111 _queue_item("track-1", LIBRARY_ALBUM),
112 _queue_item("track-2", PROVIDER_ALBUM),
113 allow_same_album=True,
114 )
115