Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/artist info #29

Merged
merged 6 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion src/knuckles/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -138,3 +138,13 @@ 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)
82 changes: 80 additions & 2 deletions src/knuckles/models/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,70 @@
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]
if similarArtist
else None
)

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."""

Expand Down Expand Up @@ -65,6 +129,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,
Expand All @@ -77,6 +142,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
28 changes: 26 additions & 2 deletions tests/api/test_browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -175,3 +174,28 @@ 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_with_all_optional_params: Response,
artist: dict[str, Any],
artist_info: dict[str, Any],
) -> None:
responses.add(mock_get_artist_info_with_all_optional_params)

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 response.similar_artists is not None
assert len(response.similar_artists) == len(artist_info["similarArtist"])
assert response.similar_artists[0].name == artist["name"]
41 changes: 41 additions & 0 deletions tests/mocks/browsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,44 @@ 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"],
},
{"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",
{
"id": artist["id"],
"count": len(artist_info["similarArtist"]),
"includeNotPresent": False,
},
{"artistInfo2": artist_info},
)
22 changes: 22 additions & 0 deletions tests/models/test_artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,34 @@
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"
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.biography == artist_info["biography"]
21 changes: 21 additions & 0 deletions tests/models/test_artist_info.py
Original file line number Diff line number Diff line change
@@ -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"]