diff --git a/src/knuckles/browsing.py b/src/knuckles/browsing.py index 1cc15c9..9aa037a 100644 --- a/src/knuckles/browsing.py +++ b/src/knuckles/browsing.py @@ -2,7 +2,7 @@ from .api import Api from .models.album import Album, AlbumInfo -from .models.artist import Artist +from .models.artist import Artist, ArtistInfo from .models.genre import Genre from .models.music_folder import MusicFolder from .models.song import Song @@ -138,3 +138,8 @@ def get_song(self, id: str) -> Song: response = self.api.request("getSong", {"id": id})["song"] return Song(self.subsonic, **response) + + def get_artist_info(self, id: str, count: int | None = None, include_not_present: bool | None = None) -> ArtistInfo: + response = self.api.request("getArtistInfo2", {"id": id, "count": count, "includeNotPresent":include_not_present})["artistInfo2"] + + return ArtistInfo(self.subsonic, id, **response) diff --git a/src/knuckles/models/artist.py b/src/knuckles/models/artist.py index 829e8e9..d95cbc6 100644 --- a/src/knuckles/models/artist.py +++ b/src/knuckles/models/artist.py @@ -8,6 +8,66 @@ from dateutil import parser +class ArtistInfo: + """Representation of all the data related to an artist info in Subsonic.""" + + def __init__( + self, + # Internal + subsonic: "Subsonic", + artist_id: str, + # Subsonic fields + biography: str, + musicBrainzId: str | None, + lastFmUrl: str | None, + smallImageUrl: str | None, + mediumImageUrl: str | None, + largeImageUrl: str | None, + similarArtist: list[dict[str, Any]] | None = None + ) -> None: + """Representation of all the data related to an album info in Subsonic. + :param subsonic: The subsonic object to make all the internal requests with it. + :type subsonic: Subsonic + :param artist_id: The ID3 of the artist associated with the info. + :type artist_id: str + :param biography: A biography for the album. + :type biography: str + :param musicBrainzId:The ID in music Brainz of the album. + :type musicBrainzId: str + :param smallImageUrl: An URL to the small size cover image of the artist. + :type smallImageUrl: str + :param mediumImageUrl: An URL to the medium size cover image of the artist. + :type mediumImageUrl: str + :param largeImageUrl: An URL to the large size cover image of the artist. + :type largeImageUrl: str + :param similarArtist: A list with all the similar artists. + :type similarArtist: list[str, Any] + """ + + self.__subsonic = subsonic + self.artist_id = artist_id + self.biography = biography + self.music_brainz_id = musicBrainzId + self.last_fm_url = lastFmUrl + self.small_image_url = smallImageUrl + self.medium_image_url = mediumImageUrl + self.large_image_url = largeImageUrl + self.similar_artists = [ + Artist(self.__subsonic, **artist) for artist in similarArtist + ] + + def generate(self) -> "ArtistInfo": + """Return a new artist info with all the data updated from the API, + using the endpoint that return the most information possible. + + Useful for making copies with updated data or updating the object itself + with immutability, e.g., foo = foo.generate(). + + :return: A new album info object with all the data updated. + :rtype: ArtistInfo + """ + + return self.__subsonic.browsing.get_artist_info(self.artist_id) class Artist: """Representation of all the data related to an artist in Subsonic.""" @@ -65,6 +125,7 @@ def __init__( if album else None ) + self.info: ArtistInfo | None = None def generate(self) -> "Artist": """Return a new artist with all the data updated from the API, @@ -77,6 +138,19 @@ def generate(self) -> "Artist": :rtype: Artist """ - get_artist = self.__subsonic.browsing.get_artist(self.id) + new_artist = self.__subsonic.browsing.get_artist(self.id) + new_artist.get_artist_info() + + return new_artist + + def get_artist_info(self) -> ArtistInfo: + """Returns the extra info given by the "getAlbumInfo2" endpoint, + also sets it in the info property of the model. + + :return: An AlbumInfo object with all the extra info given by the API. + :rtype: AlbumInfo + """ + + self.info = self.__subsonic.browsing.get_artist_info(self.id) - return get_artist + return self.info diff --git a/tests/api/test_browsing.py b/tests/api/test_browsing.py index ed5414e..a46e069 100644 --- a/tests/api/test_browsing.py +++ b/tests/api/test_browsing.py @@ -179,15 +179,13 @@ def test_get_album_info( @responses.activate def test_get_artist_info( subsonic: Subsonic, - mock_get_artist_info: Response, + mock_get_artist_info_with_all_optional_params: Response, artist: dict[str, Any], artist_info: dict[str, Any], ) -> None: - responses.add(mock_get_artist_info) + responses.add(mock_get_artist_info_with_all_optional_params) - response = subsonic.browsing.get_artist_info( - artist["id"], len(artist_info["similarArtist"]), False - ) + response = subsonic.browsing.get_artist_info(artist["id"], len(artist_info["similarArtist"]), False) assert response.biography == artist_info["biography"] assert response.music_brainz_id == artist_info["musicBrainzId"] diff --git a/tests/mocks/browsing.py b/tests/mocks/browsing.py index 9e1dc3f..7e1d094 100644 --- a/tests/mocks/browsing.py +++ b/tests/mocks/browsing.py @@ -174,10 +174,21 @@ def artist_info(artist: dict[str, Any]) -> dict[str, Any]: "similarArtist": [artist], } - @pytest.fixture def mock_get_artist_info( mock_generator: MockGenerator, artist: dict[str, Any], artist_info: dict[str, Any] +) -> Response: + return mock_generator( + "getArtistInfo2", + { + "id": artist["id"], + }, + {"artistInfo2": artist_info}, + ) + +@pytest.fixture +def mock_get_artist_info_with_all_optional_params( + mock_generator: MockGenerator, artist: dict[str, Any], artist_info: dict[str, Any] ) -> Response: return mock_generator( "getArtistInfo2", diff --git a/tests/models/test_artist.py b/tests/models/test_artist.py index 57a303f..3ab74e7 100644 --- a/tests/models/test_artist.py +++ b/tests/models/test_artist.py @@ -9,10 +9,12 @@ def test_generate( subsonic: Subsonic, mock_get_artist: Response, + mock_get_artist_info: Response, artist: dict[str, Any], artist_info: dict[str, Any], ) -> None: responses.add(mock_get_artist) + responses.add(mock_get_artist_info) requested_artist = subsonic.browsing.get_artist(artist["id"]) requested_artist.name = "Foo" @@ -37,4 +39,4 @@ def test_get_artist_info( get_artist_info = requested_artist.get_artist_info() assert get_artist_info.biography == artist_info["biography"] - assert requested_artist.info.notes == artist_info["notes"] + assert requested_artist.info.biography == artist_info["biography"] diff --git a/tests/models/test_artist_info.py b/tests/models/test_artist_info.py new file mode 100644 index 0000000..30d0d88 --- /dev/null +++ b/tests/models/test_artist_info.py @@ -0,0 +1,21 @@ +from typing import Any + +import responses +from knuckles import Subsonic +from responses import Response + + +@responses.activate +def test_generate( + subsonic: Subsonic, + mock_get_artist_info: Response, + artist: dict[str, Any], + artist_info: dict[str, Any], +) -> None: + responses.add(mock_get_artist_info) + + response = subsonic.browsing.get_artist_info(artist["id"]) + response.biography = "" + response = response.generate() + + assert response.biography == artist_info["biography"]