/
/
/
1"""Tests for the albums controller."""
2
3from __future__ import annotations
4
5from typing import TYPE_CHECKING
6
7from music_assistant_models.enums import ExternalID, ImageType
8from music_assistant_models.media_items import (
9 MediaItemImage,
10 ProviderMapping,
11 UniqueList,
12)
13
14from .helpers import create_album, create_track
15
16if TYPE_CHECKING:
17 import pytest
18 from music_assistant_models.media_items import Album
19
20 from music_assistant.mass import MusicAssistant
21
22RELEASE_GROUP_MBID = "7d4d1f70-1c99-4c1b-b0f1-9f2c1b2a3d44"
23ALBUM_IMAGE = "http://images/album1.jpg"
24
25
26def _detailed_album(item_id: str = "album1") -> Album:
27 """Return an album carrying the details only a full provider fetch delivers."""
28 album = create_album("spotify_1", item_id)
29 album.year = 1999
30 album.external_ids = {(ExternalID.MB_RELEASEGROUP, RELEASE_GROUP_MBID)}
31 album.metadata.genres = {"rock"}
32 album.metadata.images = UniqueList(
33 [MediaItemImage(type=ImageType.THUMB, path=ALBUM_IMAGE, provider="spotify_1")]
34 )
35 return album
36
37
38def _tidal_mapping() -> ProviderMapping:
39 """Return the album mapping cross-provider matching would have added."""
40 return ProviderMapping(
41 item_id="tidal_album1", provider_domain="tidal", provider_instance="tidal_1"
42 )
43
44
45async def test_overwrite_update_keeps_artists_when_none_are_given(
46 mass: MusicAssistant, caplog: pytest.LogCaptureFixture
47) -> None:
48 """An overwrite update carrying no artists must not clear the stored ones."""
49 db_album = await mass.music.albums.add_item_to_library(create_album("spotify_1", "album1"))
50
51 update = create_album("spotify_1", "album1", artist_name=None)
52 await mass.music.albums.update_item_in_library(db_album.item_id, update, overwrite=True)
53
54 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
55 assert [artist.name for artist in refreshed.artists] == ["Test Artist"]
56 assert "Ignoring request to clear all artists" in caplog.text
57
58
59async def test_overwrite_update_replaces_artists(mass: MusicAssistant) -> None:
60 """An overwrite update carrying artists still replaces the stored ones."""
61 db_album = await mass.music.albums.add_item_to_library(create_album("spotify_1", "album1"))
62
63 # a distinct artist id, so the stored relation is replaced rather than renamed
64 update = create_album(
65 "spotify_1", "album1", artist_name="Other Artist", artist_item_id="other_artist"
66 )
67 await mass.music.albums.update_item_in_library(db_album.item_id, update, overwrite=True)
68
69 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
70 assert [artist.name for artist in refreshed.artists] == ["Other Artist"]
71
72
73async def test_track_overwrite_keeps_album_artists(mass: MusicAssistant) -> None:
74 """A track update carrying an artist-less album must not clear that album's artists."""
75 db_album = await mass.music.albums.add_item_to_library(create_album("spotify_1", "album1"))
76 track = create_track("spotify_1", "track1")
77 track.album = create_album("spotify_1", "album1")
78 db_track = await mass.music.tracks.add_item_to_library(track)
79
80 # a provider that builds an album object without artists (as qqmusic does)
81 update = create_track("spotify_1", "track1")
82 update.album = create_album("spotify_1", "album1", artist_name=None)
83 await mass.music.tracks.update_item_in_library(db_track.item_id, update, overwrite=True)
84
85 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
86 assert [artist.name for artist in refreshed.artists] == ["Test Artist"]
87
88
89async def test_track_overwrite_keeps_album_details(mass: MusicAssistant) -> None:
90 """A track update carrying a bare album stub must not blank that album's details."""
91 db_album = await mass.music.albums.add_item_to_library(_detailed_album())
92 track = create_track("spotify_1", "track1")
93 track.album = create_album("spotify_1", "album1")
94 db_track = await mass.music.tracks.add_item_to_library(track)
95
96 # a provider that embeds a bare album stub in every track (as emby does)
97 update = create_track("spotify_1", "track1")
98 update.album = create_album("spotify_1", "album1")
99 await mass.music.tracks.update_item_in_library(db_track.item_id, update, overwrite=True)
100
101 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
102 assert refreshed.year == 1999
103 assert refreshed.external_ids == {(ExternalID.MB_RELEASEGROUP, RELEASE_GROUP_MBID)}
104 assert refreshed.metadata.genres == {"rock"}
105 assert [image.path for image in refreshed.metadata.images or []] == [ALBUM_IMAGE]
106
107
108async def test_track_overwrite_keeps_other_provider_album_mapping(mass: MusicAssistant) -> None:
109 """A track update must not unlink its album from the other providers it matched."""
110 db_album = await mass.music.albums.add_item_to_library(create_album("spotify_1", "album1"))
111 await mass.music.albums.add_provider_mappings(db_album.item_id, [_tidal_mapping()])
112 track = create_track("spotify_1", "track1")
113 track.album = create_album("spotify_1", "album1")
114 db_track = await mass.music.tracks.add_item_to_library(track)
115
116 update = create_track("spotify_1", "track1")
117 update.album = create_album("spotify_1", "album1")
118 await mass.music.tracks.update_item_in_library(db_track.item_id, update, overwrite=True)
119
120 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
121 assert {mapping.provider_instance for mapping in refreshed.provider_mappings} == {
122 "spotify_1",
123 "tidal_1",
124 }
125
126
127async def test_overwrite_update_replaces_album_details(mass: MusicAssistant) -> None:
128 """An overwrite update carrying details still replaces the stored ones."""
129 db_album = await mass.music.albums.add_item_to_library(_detailed_album())
130
131 update = _detailed_album()
132 update.year = 2001
133 update.external_ids = {(ExternalID.MB_RELEASEGROUP, "11111111-2222-3333-4444-555555555555")}
134 update.metadata.genres = {"jazz"}
135 update.metadata.images = UniqueList(
136 [MediaItemImage(type=ImageType.THUMB, path="http://images/new.jpg", provider="spotify_1")]
137 )
138 await mass.music.albums.update_item_in_library(db_album.item_id, update, overwrite=True)
139
140 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
141 assert refreshed.year == 2001
142 assert refreshed.external_ids == {
143 (ExternalID.MB_RELEASEGROUP, "11111111-2222-3333-4444-555555555555")
144 }
145 assert refreshed.metadata.genres == {"jazz"}
146 assert [image.path for image in refreshed.metadata.images or []] == ["http://images/new.jpg"]
147
148
149async def test_overwrite_update_replaces_only_its_own_provider_mapping(
150 mass: MusicAssistant,
151) -> None:
152 """An overwrite replaces the mapping of its own provider and keeps the others."""
153 db_album = await mass.music.albums.add_item_to_library(create_album("spotify_1", "album1"))
154 await mass.music.albums.add_provider_mappings(db_album.item_id, [_tidal_mapping()])
155
156 # the same album under a new id on its own provider, as a moved folder produces
157 update = create_album("spotify_1", "album2")
158 await mass.music.albums.update_item_in_library(db_album.item_id, update, overwrite=True)
159
160 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
161 assert {
162 (mapping.provider_instance, mapping.item_id) for mapping in refreshed.provider_mappings
163 } == {("spotify_1", "album2"), ("tidal_1", "tidal_album1")}
164
165
166async def test_track_overwrite_keeps_album_details_with_an_empty_image_list(
167 mass: MusicAssistant,
168) -> None:
169 """An empty image list is not metadata, so it must not replace the stored details."""
170 full = _detailed_album()
171 full.metadata.description = "About this album"
172 db_album = await mass.music.albums.add_item_to_library(full)
173 track = create_track("spotify_1", "track1")
174 track.album = create_album("spotify_1", "album1")
175 db_track = await mass.music.tracks.add_item_to_library(track)
176
177 # a provider that always assigns an image list, empty or not (as spotify does)
178 update = create_track("spotify_1", "track1")
179 stub = create_album("spotify_1", "album1")
180 stub.metadata.images = UniqueList([])
181 update.album = stub
182 await mass.music.tracks.update_item_in_library(db_track.item_id, update, overwrite=True)
183
184 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
185 assert refreshed.metadata.genres == {"rock"}
186 assert refreshed.metadata.description == "About this album"
187 assert [image.path for image in refreshed.metadata.images or []] == [ALBUM_IMAGE]
188
189
190async def test_track_overwrite_keeps_album_version(mass: MusicAssistant) -> None:
191 """A track update carrying a version-less album must not blank the stored edition."""
192 full = create_album("spotify_1", "album1")
193 full.version = "Deluxe Edition"
194 db_album = await mass.music.albums.add_item_to_library(full)
195 track = create_track("spotify_1", "track1")
196 track.album = create_album("spotify_1", "album1")
197 db_track = await mass.music.tracks.add_item_to_library(track)
198
199 update = create_track("spotify_1", "track1")
200 update.album = create_album("spotify_1", "album1")
201 await mass.music.tracks.update_item_in_library(db_track.item_id, update, overwrite=True)
202
203 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
204 assert refreshed.version == "Deluxe Edition"
205
206
207async def test_merge_update_keeps_the_stored_year_and_version(mass: MusicAssistant) -> None:
208 """A second provider matching an existing album does not restate its release."""
209 full = _detailed_album()
210 full.version = "Deluxe Edition"
211 db_album = await mass.music.albums.add_item_to_library(full)
212
213 # the same album on another provider, carrying a reissue year and no edition
214 update = create_album("tidal_1", "tidal_album1")
215 update.year = 2011
216 await mass.music.albums.update_item_in_library(db_album.item_id, update)
217
218 refreshed = await mass.music.albums.get_library_item(db_album.item_id)
219 assert refreshed.year == 1999
220 assert refreshed.version == "Deluxe Edition"
221