From a975353c057c4e480ef332f025bc184608ef87ab Mon Sep 17 00:00:00 2001 From: Kutu Date: Sat, 16 Sep 2023 19:31:37 +0200 Subject: [PATCH 1/6] Add tests for artist info --- tests/api/test_browsing.py | 27 +++++++++++++++++++++++++-- tests/mocks/browsing.py | 28 ++++++++++++++++++++++++++++ tests/models/test_artist.py | 20 ++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/tests/api/test_browsing.py b/tests/api/test_browsing.py index 611c5bb..ed5414e 100644 --- a/tests/api/test_browsing.py +++ b/tests/api/test_browsing.py @@ -2,9 +2,8 @@ import responses from dateutil import parser -from responses import Response - from knuckles import CoverArt, Subsonic +from responses import Response @responses.activate @@ -175,3 +174,27 @@ def test_get_album_info( assert response.medium_image_url == album_info["mediumImageUrl"] assert response.large_image_url == album_info["largeImageUrl"] assert response.large_image_url == album_info["largeImageUrl"] + + +@responses.activate +def test_get_artist_info( + 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"], len(artist_info["similarArtist"]), False + ) + + assert response.biography == artist_info["biography"] + assert response.music_brainz_id == artist_info["musicBrainzId"] + assert response.last_fm_url == artist_info["lastFmUrl"] + assert response.small_image_url == artist_info["smallImageUrl"] + assert response.medium_image_url == artist_info["mediumImageUrl"] + assert response.large_image_url == artist_info["largeImageUrl"] + assert response.large_image_url == artist_info["largeImageUrl"] + assert len(response.similar_artists) == len(artist_info["similarArtist"]) + assert response.similar_artists[0].name == artist["name"] diff --git a/tests/mocks/browsing.py b/tests/mocks/browsing.py index fb5ae82..9e1dc3f 100644 --- a/tests/mocks/browsing.py +++ b/tests/mocks/browsing.py @@ -160,3 +160,31 @@ def mock_get_album_info( return mock_generator( "getAlbumInfo2", {"id": album["id"]}, {"albumInfo": album_info} ) + + +@pytest.fixture +def artist_info(artist: dict[str, Any]) -> dict[str, Any]: + return { + "biography": {}, + "musicBrainzId": "1", + "lastFmUrl": "", + "smallImageUrl": "http://localhost:8989/play/art/f20070e8e11611cc53542a38801d60fa/artist/2/thumb34.jpg", + "mediumImageUrl": "http://localhost:8989/play/art/2b9b6c057cd4bf21089ce7572e7792b6/artist/2/thumb64.jpg", + "largeImageUrl": "http://localhost:8989/play/art/e18287c23a75e263b64c31b3d64c1944/artist/2/thumb174.jpg", + "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"], + "count": len(artist_info["similarArtist"]), + "includeNotPresent": False, + }, + {"artistInfo2": artist_info}, + ) diff --git a/tests/models/test_artist.py b/tests/models/test_artist.py index d78321a..57a303f 100644 --- a/tests/models/test_artist.py +++ b/tests/models/test_artist.py @@ -10,6 +10,7 @@ def test_generate( subsonic: Subsonic, mock_get_artist: Response, artist: dict[str, Any], + artist_info: dict[str, Any], ) -> None: responses.add(mock_get_artist) @@ -18,3 +19,22 @@ def test_generate( requested_artist = requested_artist.generate() assert requested_artist.name == artist["name"] + assert requested_artist.info.biography == artist_info["biography"] + + +@responses.activate +def test_get_artist_info( + 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"]) + get_artist_info = requested_artist.get_artist_info() + + assert get_artist_info.biography == artist_info["biography"] + assert requested_artist.info.notes == artist_info["notes"] From d00a3a4a7c3396ee805dd16313eb20c88fc0b8aa Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 17 Sep 2023 13:06:47 +0200 Subject: [PATCH 2/6] Implement artist info methods and models --- src/knuckles/browsing.py | 7 ++- src/knuckles/models/artist.py | 78 +++++++++++++++++++++++++++++++- tests/api/test_browsing.py | 8 ++-- tests/mocks/browsing.py | 13 +++++- tests/models/test_artist.py | 4 +- tests/models/test_artist_info.py | 21 +++++++++ 6 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/models/test_artist_info.py 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"] From b6ef825e8f883b8964b9c080a9d7dd5dfd3cb3f0 Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 17 Sep 2023 13:09:35 +0200 Subject: [PATCH 3/6] Fix formatting --- src/knuckles/models/artist.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/knuckles/models/artist.py b/src/knuckles/models/artist.py index d95cbc6..920ca64 100644 --- a/src/knuckles/models/artist.py +++ b/src/knuckles/models/artist.py @@ -8,6 +8,7 @@ from dateutil import parser + class ArtistInfo: """Representation of all the data related to an artist info in Subsonic.""" @@ -23,7 +24,7 @@ def __init__( smallImageUrl: str | None, mediumImageUrl: str | None, largeImageUrl: str | None, - similarArtist: list[dict[str, Any]] | None = 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. @@ -52,9 +53,11 @@ def __init__( 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 - ] + self.similar_artists = ( + [Artist(self.__subsonic, **artist) for artist in similarArtist] + if similarArtist + else None + ) def generate(self) -> "ArtistInfo": """Return a new artist info with all the data updated from the API, @@ -69,6 +72,7 @@ def generate(self) -> "ArtistInfo": return self.__subsonic.browsing.get_artist_info(self.artist_id) + class Artist: """Representation of all the data related to an artist in Subsonic.""" From 6759916740c936e2b684f75a4ff9be1779e2e33d Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 17 Sep 2023 13:28:03 +0200 Subject: [PATCH 4/6] Add type check --- tests/api/test_browsing.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/api/test_browsing.py b/tests/api/test_browsing.py index a46e069..c988946 100644 --- a/tests/api/test_browsing.py +++ b/tests/api/test_browsing.py @@ -185,7 +185,9 @@ def test_get_artist_info( ) -> None: 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"] @@ -194,5 +196,6 @@ def test_get_artist_info( assert response.medium_image_url == artist_info["mediumImageUrl"] assert response.large_image_url == artist_info["largeImageUrl"] assert response.large_image_url == artist_info["largeImageUrl"] + assert response.similar_artists is not None assert len(response.similar_artists) == len(artist_info["similarArtist"]) assert response.similar_artists[0].name == artist["name"] From bbd7ba20431807656199575af753318ef05a10d2 Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 17 Sep 2023 13:30:29 +0200 Subject: [PATCH 5/6] Fix invalid line length --- src/knuckles/browsing.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/knuckles/browsing.py b/src/knuckles/browsing.py index 9aa037a..5ab72b4 100644 --- a/src/knuckles/browsing.py +++ b/src/knuckles/browsing.py @@ -139,7 +139,12 @@ def get_song(self, id: str) -> 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"] + 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) From f1b1955bc3fd518f1698758c71238bf8d3dfdc15 Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 17 Sep 2023 13:39:51 +0200 Subject: [PATCH 6/6] Fix formatting errors --- .pre-commit-config.yaml | 2 +- tests/mocks/browsing.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 22dcb18..4086aaa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: rev: v0.0.279 hooks: - id: ruff - args: [--fix] + args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 22.10.0 diff --git a/tests/mocks/browsing.py b/tests/mocks/browsing.py index 7e1d094..99a5dc5 100644 --- a/tests/mocks/browsing.py +++ b/tests/mocks/browsing.py @@ -174,6 +174,7 @@ 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] @@ -186,6 +187,7 @@ def mock_get_artist_info( {"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]