diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..afad818 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.0 diff --git a/src/knuckles/media_annotation.py b/src/knuckles/media_annotation.py index 4926d3a..40bedb9 100644 --- a/src/knuckles/media_annotation.py +++ b/src/knuckles/media_annotation.py @@ -136,7 +136,7 @@ def remove_rating(self, id_: str) -> "Subsonic": return self.subsonic def scrobble( - self, id_: list[str], time: datetime, submission: bool = True + self, id_: list[str], time: list[datetime], submission: bool = True ) -> "Subsonic": """Calls to the "scrobble" endpoint of the API @@ -155,7 +155,11 @@ def scrobble( "scrobble", # Multiply by 1000 because the API uses # milliseconds instead of seconds for UNIX time - {"id": id_, "time": int(time.timestamp() * 1000), "submission": submission}, + { + "id": id_, + "time": [int(seconds.timestamp()) * 1000 for seconds in time], + "submission": submission, + }, ) return self.subsonic diff --git a/src/knuckles/media_retrieval.py b/src/knuckles/media_retrieval.py index 81ba75e..8b7771e 100644 --- a/src/knuckles/media_retrieval.py +++ b/src/knuckles/media_retrieval.py @@ -1,7 +1,7 @@ from enum import Enum from mimetypes import guess_extension from pathlib import Path -from typing import Any +from typing import Any, Callable from requests import Response from requests.models import PreparedRequest @@ -64,6 +64,20 @@ def _download_file(response: Response, downloaded_file_path: Path) -> Path: return downloaded_file_path + @classmethod + def _handle_download( + cls, + response: Response, + file_or_directory_path: Path, + determinate_filename: Callable[[Response], str], + ) -> Path: + if not file_or_directory_path.is_dir(): + return cls._download_file(response, file_or_directory_path) + + filename = determinate_filename(response) + + return cls._download_file(response, file_or_directory_path / filename) + def stream( self, id_: str, @@ -126,20 +140,26 @@ def download(self, id_: str, file_or_directory_path: Path) -> Path: response = self.api.raw_request("download", {"id": id_}) - if not file_or_directory_path.is_dir(): - return self._download_file(response, file_or_directory_path) + def determinate_filename(file_response: Response) -> str: + filename = ( + file_response.headers["Content-Disposition"] + .split("filename=")[1] + .strip() + ) - filename = response.headers["Content-Disposition"].split("filename=")[1].strip() + # Remove leading quote char + if filename[0] == '"': + filename = filename[1:] - # Remove leading quote char - if filename[0] == '"': - filename = filename[1:] + # Remove trailing quote char + if filename[-1] == '"': + filename = filename[:-1] - # Remove trailing quote char - if filename[-1] == '"': - filename = filename[:-1] + return filename - return self._download_file(response, file_or_directory_path / filename) + return self._handle_download( + response, file_or_directory_path, determinate_filename + ) def hls( self, @@ -195,20 +215,21 @@ def get_captions( {"id": id_, "format": subtitles_file_format.value}, ) - if not file_or_directory_path.is_dir(): - return self._download_file(response, file_or_directory_path) - - mime_type = response.headers["content-type"].partition(";")[0].strip() + def determinate_filename(file_response: Response) -> str: + mime_type = file_response.headers["content-type"].partition(";")[0].strip() - # As application/x-subrip is not a valid MIME TYPE so a manual check is done - if mime_type != "application/x-subrip": - file_extension = guess_extension(mime_type) - else: - file_extension = ".srt" + # application/x-subrip is not a valid MIME TYPE so a manual check is needed + file_extension: str | None = None + if mime_type == "application/x-subrip": + file_extension = ".srt" + else: + file_extension = guess_extension(mime_type) - filename = id_ + file_extension if file_extension else id_ + return id_ + file_extension if file_extension else id_ - return self._download_file(response, file_or_directory_path / filename) + return self._handle_download( + response, file_or_directory_path, determinate_filename + ) def get_cover_art( self, id_: str, file_or_directory_path: Path, size: int | None = None @@ -230,16 +251,16 @@ def get_cover_art( response = self.api.raw_request("getCoverArt", {"id": id_, "size": size}) - if not file_or_directory_path.is_dir(): - return self._download_file(response, file_or_directory_path) + def determinate_filename(file_response: Response) -> str: + file_extension = guess_extension( + file_response.headers["content-type"].partition(";")[0].strip() + ) - file_extension = guess_extension( - response.headers["content-type"].partition(";")[0].strip() - ) + return id_ + file_extension if file_extension else id_ - filename = id_ + file_extension if file_extension else id_ - - return self._download_file(response, file_or_directory_path / filename) + return self._handle_download( + response, file_or_directory_path, determinate_filename + ) def get_lyrics(self) -> None: ... @@ -260,13 +281,13 @@ def get_avatar(self, username: str, file_or_directory_path: Path) -> Path: response = self.api.raw_request("getAvatar", {"username": username}) - if not file_or_directory_path.is_dir(): - return self._download_file(response, file_or_directory_path) - - file_extension = guess_extension( - response.headers["content-type"].partition(";")[0].strip() - ) + def determinate_filename(file_response: Response) -> str: + file_extension = guess_extension( + file_response.headers["content-type"].partition(";")[0].strip() + ) - filename = username + file_extension if file_extension else username + return username + file_extension if file_extension else username - return self._download_file(response, file_or_directory_path / filename) + return self._handle_download( + response, file_or_directory_path, determinate_filename + ) diff --git a/src/knuckles/models/play_queue.py b/src/knuckles/models/play_queue.py index 3a47e37..8cb1520 100644 --- a/src/knuckles/models/play_queue.py +++ b/src/knuckles/models/play_queue.py @@ -25,7 +25,7 @@ def __init__( self.__subsonic = subsonic self.current = Song(self.__subsonic, current) if current else None self.position = position - self.user = User(username) if username else None + self.user = User(self.__subsonic, username) if username else None self.changed = parser.parse(changed) if changed else None self.changed_by = changedBy self.songs = ( diff --git a/src/knuckles/models/playlist.py b/src/knuckles/models/playlist.py index d253c1d..9c13011 100644 --- a/src/knuckles/models/playlist.py +++ b/src/knuckles/models/playlist.py @@ -69,11 +69,11 @@ def __init__( self.created = parser.parse(created) if created else None self.changed = parser.parse(changed) if changed else None self.comment = comment - self.owner = User(owner, subsonic=self.__subsonic) if owner else None + self.owner = User(self.__subsonic, owner) if owner else None self.public = public self.cover_art = CoverArt(coverArt) if coverArt else None self.allowed_users = ( - [User(username) for username in allowedUser] if allowedUser else None + [User(self.__subsonic, username) for username in allowedUser] if allowedUser else None ) self.songs = ( [Song(self.__subsonic, **song) for song in entry] if entry else None diff --git a/src/knuckles/models/share.py b/src/knuckles/models/share.py index 765bce6..210a01d 100644 --- a/src/knuckles/models/share.py +++ b/src/knuckles/models/share.py @@ -58,7 +58,7 @@ def __init__( self.id = id self.url = url self.description = description - self.user = User(username) if username else None + self.user = User(self.__subsonic, username) if username else None self.created = parser.parse(created) if created else None self.expires = parser.parse(expires) if expires else None self.last_visited = parser.parse(lastVisited) if lastVisited else None diff --git a/src/knuckles/models/song.py b/src/knuckles/models/song.py index e1b7910..28de0d9 100644 --- a/src/knuckles/models/song.py +++ b/src/knuckles/models/song.py @@ -244,6 +244,6 @@ def scrobble(self, time: datetime, submission: bool = True) -> Self: :rtype: Self """ - self.__subsonic.media_annotation.scrobble([self.id], time, submission) + self.__subsonic.media_annotation.scrobble([self.id], [time], submission) return self diff --git a/src/knuckles/models/user.py b/src/knuckles/models/user.py index f33f967..9f35245 100644 --- a/src/knuckles/models/user.py +++ b/src/knuckles/models/user.py @@ -11,87 +11,47 @@ class User: def __init__( self, + # Internal + subsonic: "Subsonic", # Subsonic fields username: str, + password: str | None = None, email: str | None = None, - scrobblingEnabled: bool = False, - adminRole: bool = False, - settingsRole: bool = False, - downloadRole: bool = False, - uploadRole: bool = False, - playlistRole: bool = False, - coverArtRole: bool = False, - commentRole: bool = False, - podcastRole: bool = False, - streamRole: bool = False, - jukeboxRole: bool = False, - shareRole: bool = False, - videoConversionRole: bool = False, - # Internal - subsonic: "Subsonic | None" = None, + ldap_authenticated: bool | None = None, + admin_role: bool | None = None, + settings_role: bool | None = None, + stream_role: bool | None = None, + jukebox_role: bool | None = None, + download_role: bool | None = None, + upload_role: bool | None = None, + playlist_role: bool | None = None, + cover_art_role: bool | None = None, + comment_role: bool | None = None, + podcast_role: bool | None = None, + share_role: bool | None = None, + video_conversion_role: bool | None = None, + music_folder_id: list[str] | None = None, + max_bit_rate: int | None = None, ) -> None: - """Representation of all the data related to a user in Subsonic. - - :param username: The username of the user - :type username: str - :param email: The email of the user - :type email: str - :param scrobblingEnabled: If the user can do scrobbling, - defaults to False. - :type scrobblingEnabled: bool, optional - :param adminRole: If the user has admin privileges, - overrides all the rest of roles,defaults to False. - :type adminRole: bool, optional - :param settingsRole: If the user can modify global settings, - defaults to False. - :type settingsRole: bool, optional - :param downloadRole: If the user can download songs, defaults to False. - :type downloadRole: bool, optional - :param uploadRole: If the user can upload data to the server, - defaults to False. - :type uploadRole: bool, optional - :param playlistRole: If the user can use playlist, defaults to False. - :type playlistRole: bool, optional - :param coverArtRole: If the user can access cover arts, - defaults to False. - :type coverArtRole: bool, optional - :param commentRole: If the user can do comments, defaults to False. - :type commentRole: bool, optional - :param podcastRole: If the user can listen to podcasts, - defaults to False. - :type podcastRole: bool, optional - :param streamRole: If the user can listen media with streaming, - defaults to False. - :type streamRole: bool, optional - :param jukeboxRole: If the user can use the jukebox, defaults to False - :type jukeboxRole: bool, optional - :param shareRole: If the user can use sharing capabilities, - defaults to False - :type shareRole: bool, optional - :param videoConversionRole: If the user can do video conversion, - defaults to False - :type videoConversionRole: bool, optional - :param subsonic: The subsonic object to make all the internal requests with it, - defaults to None - :type subsonic: Subsonic | None, optional - """ - self.subsonic = subsonic self.username = username + self.password = password self.email = email - self.scrobbling_enabled = scrobblingEnabled - self.admin_role = adminRole - self.settings_role = settingsRole - self.download_role = downloadRole - self.upload_role = uploadRole - self.playlist_role = playlistRole - self.cover_art_role = coverArtRole - self.comment_role = commentRole - self.podcast_role = podcastRole - self.stream_role = streamRole - self.jukebox_role = jukeboxRole - self.share_role = shareRole - self.video_conversion_role = videoConversionRole + self.ldap_authenticated = ldap_authenticated + self.admin_role = admin_role + self.settings_role = settings_role + self.stream_role = stream_role + self.jukebox_role = jukebox_role + self.download_role = download_role + self.upload_role = upload_role + self.playlist_role = playlist_role + self.cover_art_role = cover_art_role + self.comment_role = comment_role + self.podcast_role = podcast_role + self.share_role = share_role + self.video_conversion_role = video_conversion_role + self.music_folder_id = music_folder_id + self.max_bit_rate = max_bit_rate def __check_api_access(self) -> None: """Check if the object has a valid subsonic property @@ -135,7 +95,30 @@ def create(self) -> Self: self.__check_api_access() - self.subsonic.user_management.create_user(self) # type: ignore[union-attr] + #! TODO This is bad + if not self.password or not self.email: + raise NoApiAccess() + + self.subsonic.user_management.create_user( + self.username, + self.password, + self.email, + self.ldap_authenticated, + self.admin_role, + self.settings_role, + self.stream_role, + self.jukebox_role, + self.download_role, + self.upload_role, + self.playlist_role, + self.cover_art_role, + self.comment_role, + self.podcast_role, + self.share_role, + self.video_conversion_role, + self.music_folder_id, + self.max_bit_rate, + ) # type: ignore[union-attr] return self @@ -152,7 +135,26 @@ def update(self) -> Self: self.__check_api_access() - self.subsonic.user_management.update_user(self) # type: ignore[union-attr] + self.subsonic.user_management.update_user( + self.username, + self.password, + self.email, + self.ldap_authenticated, + self.admin_role, + self.settings_role, + self.stream_role, + self.jukebox_role, + self.download_role, + self.upload_role, + self.playlist_role, + self.cover_art_role, + self.comment_role, + self.podcast_role, + self.share_role, + self.video_conversion_role, + self.music_folder_id, + self.max_bit_rate, + ) # type: ignore[union-attr] return self diff --git a/src/knuckles/user_management.py b/src/knuckles/user_management.py index c154951..f330718 100644 --- a/src/knuckles/user_management.py +++ b/src/knuckles/user_management.py @@ -17,34 +17,6 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.api = api self.subsonic = subsonic - @staticmethod - def __user_properties_to_json(user: User) -> dict[str, Any]: - """Converts the data in a User object to a dictionary - with the keys used in all the user related calls to the API. - - :param user: The user to convert to a dictionary - :type user: User - :return: The dictionary with the data and valid keys to the API. - :rtype: dict[str, Any]""" - - return { - "username": user.username, - "email": user.email, - "scrobblingEnabled": user.scrobbling_enabled, - "adminRole": user.admin_role, - "settingsRole": user.settings_role, - "downloadRole": user.download_role, - "uploadRole": user.upload_role, - "playlistRole": user.playlist_role, - "coverArtRole": user.cover_art_role, - "commentRole": user.comment_role, - "podcastRole": user.podcast_role, - "streamRole": user.stream_role, - "jukeboxRole": user.jukebox_role, - "shareRole": user.share_role, - "videoConversionRole": user.video_conversion_role, - } - def get_user(self, username: str) -> User: """Calls the "getUser" endpoint of the API. @@ -56,7 +28,27 @@ def get_user(self, username: str) -> User: request = self.api.json_request("getUser", {"username": username})["user"] - return User(subsonic=self.subsonic, **request) + return User( + self.subsonic, + request["username"], + request["password"], + request["email"], + request["ldapAuthenticated"], + request["adminRole"], + request["settingsRole"], + request["streamRole"], + request["jukeboxRole"], + request["downloadRole"], + request["uploadRole"], + request["playlistRole"], + request["coverArtRole"], + request["commentRole"], + request["podcastRole"], + request["shareRole"], + request["videoConversionRole"], + request["musicFolderId"], + request["maxBitRate"], + ) def get_users(self) -> list[User]: """Calls the "getUsers" endpoint of the API. @@ -67,29 +59,123 @@ def get_users(self) -> list[User]: request = self.api.json_request("getUsers")["users"]["user"] - users = [User(subsonic=self.subsonic, **user) for user in request] + users: list[User] = [] + for user in request: + users.append(User( + self.subsonic, + user["username"], + user["password"], + user["email"], + user["ldapAuthenticated"], + user["adminRole"], + user["settingsRole"], + user["streamRole"], + user["jukeboxRole"], + user["downloadRole"], + user["uploadRole"], + user["playlistRole"], + user["coverArtRole"], + user["commentRole"], + user["podcastRole"], + user["shareRole"], + user["videoConversionRole"], + user["musicFolderId"], + user["maxBitRate"], + )) return users - def create_user(self, new_user: User) -> User: - """Calls the "createUser" endpoint of the API. - - :param new_user: A user object with all the data for the new user. - :type new_user: User - :return: The object itself to allow method chaining. - :rtype: User - """ - - user_json_data = self.__user_properties_to_json(new_user) - - self.api.json_request("createUser", {**user_json_data}) + def create_user( + self, + username: str, + password: str, + email: str, + ldap_authenticated: bool | None = None, + admin_role: bool | None = None, + settings_role: bool | None = None, + stream_role: bool | None = None, + jukebox_role: bool | None = None, + download_role: bool | None = None, + upload_role: bool | None = None, + playlist_role: bool | None = None, + cover_art_role: bool | None = None, + comment_role: bool | None = None, + podcast_role: bool | None = None, + share_role: bool | None = None, + video_conversion_role: bool | None = None, + music_folder_id: list[str] | None = None, + max_bit_rate: int | None = None, + ) -> User: + self.api.json_request( + "createUser", + { + "username": username, + "password": password, + "email": email, + "ldapAuthenticated": ldap_authenticated, + "adminRole": admin_role, + "settingsRole": settings_role, + "streamRole": stream_role, + "jukeboxRole": jukebox_role, + "downloadRole": download_role, + "uploadRole": upload_role, + "playlistRole": playlist_role, + "coverArtRole": cover_art_role, + "commentRole": comment_role, + "podcastRole": podcast_role, + "shareRole": share_role, + "videoConversionRole": video_conversion_role, + "musicFolderId": music_folder_id, + "maxBitRate": max_bit_rate, + }, + ) # Attach the Subsonic object - new_user.subsonic = self.subsonic + new_user = User( + self.subsonic, + username, + password, + email, + ldap_authenticated, + admin_role, + settings_role, + stream_role, + jukebox_role, + download_role, + upload_role, + playlist_role, + cover_art_role, + comment_role, + podcast_role, + share_role, + video_conversion_role, + music_folder_id, + max_bit_rate, + ) return new_user - def update_user(self, updated_data_user: User) -> User: + def update_user( + self, + username: str, + password: str | None = None, + email: str | None = None, + ldap_authenticated: bool | None = None, + admin_role: bool | None = None, + settings_role: bool | None = None, + stream_role: bool | None = None, + jukebox_role: bool | None = None, + download_role: bool | None = None, + upload_role: bool | None = None, + playlist_role: bool | None = None, + cover_art_role: bool | None = None, + comment_role: bool | None = None, + podcast_role: bool | None = None, + share_role: bool | None = None, + video_conversion_role: bool | None = None, + music_folder_id: list[str] | None = None, + max_bit_rate: int | None = None, + ) -> User: """Calls the "updateUser" endpoint of the API. The user to update with the new data will be @@ -101,14 +187,50 @@ def update_user(self, updated_data_user: User) -> User: :rtype: User """ - user_json_data = self.__user_properties_to_json(updated_data_user) - - self.api.json_request("updateUser", {**user_json_data}) - - # Attach the Subsonic object - updated_data_user.subsonic = self.subsonic + self.api.json_request("updateUser", { + "username": username, + "password": password, + "email": email, + "ldapAuthenticated": ldap_authenticated, + "adminRole": admin_role, + "settingsRole": settings_role, + "streamRole": stream_role, + "jukeboxRole": jukebox_role, + "downloadRole": download_role, + "uploadRole": upload_role, + "playlistRole": playlist_role, + "coverArtRole": cover_art_role, + "commentRole": comment_role, + "podcastRole": podcast_role, + "shareRole": share_role, + "videoConversionRole": video_conversion_role, + "musicFolderId": music_folder_id, + "maxBitRate": max_bit_rate, + }) + + updated_user = User( + self.subsonic, + username, + password, + email, + ldap_authenticated, + admin_role, + settings_role, + stream_role, + jukebox_role, + download_role, + upload_role, + playlist_role, + cover_art_role, + comment_role, + podcast_role, + share_role, + video_conversion_role, + music_folder_id, + max_bit_rate, + ) - return updated_data_user + return updated_user def delete_user(self, username: str) -> "Subsonic": """Calls the "deleteUser" endpoint of the API. diff --git a/tests/api/test_media_annotation.py b/tests/api/test_media_annotation.py index b8f79cf..7a93687 100644 --- a/tests/api/test_media_annotation.py +++ b/tests/api/test_media_annotation.py @@ -132,7 +132,7 @@ def test_default_scrobble( response = subsonic.media_annotation.scrobble( [song["id"]], # Divide by 1000 because messages are saved in milliseconds instead of seconds - datetime.fromtimestamp(1678935707000 / 1000), + [datetime.fromtimestamp(1678935707000 / 1000)], ) assert type(response) is Subsonic @@ -150,7 +150,7 @@ def test_submission_scrobble( response = subsonic.media_annotation.scrobble( [song["id"]], # Divide by 1000 because messages are saved in milliseconds instead of seconds - datetime.fromtimestamp(scrobble_time / 1000), + [datetime.fromtimestamp(scrobble_time / 1000)], True, ) @@ -169,7 +169,7 @@ def test_now_playing_scrobble( response = subsonic.media_annotation.scrobble( [song["id"]], # Divide by 1000 because messages are saved in milliseconds instead of seconds - datetime.fromtimestamp(scrobble_time / 1000), + [datetime.fromtimestamp(scrobble_time / 1000)], False, ) diff --git a/tests/api/test_user_management.py b/tests/api/test_user_management.py index d82cc82..b487d5a 100644 --- a/tests/api/test_user_management.py +++ b/tests/api/test_user_management.py @@ -15,20 +15,23 @@ def test_get_user( response = subsonic.user_management.get_user(user["username"]) assert response.username == user["username"] + assert response.password == user["password"] assert response.email == user["email"] - assert response.scrobbling_enabled == user["scrobblingEnabled"] + assert response.ldap_authenticated == user["ldapAuthenticated"] assert response.admin_role == user["adminRole"] assert response.settings_role == user["settingsRole"] + assert response.stream_role == user["streamRole"] + assert response.jukebox_role == user["jukeboxRole"] assert response.download_role == user["downloadRole"] assert response.upload_role == user["uploadRole"] assert response.playlist_role == user["playlistRole"] assert response.cover_art_role == user["coverArtRole"] assert response.comment_role == user["commentRole"] assert response.podcast_role == user["podcastRole"] - assert response.stream_role == user["streamRole"] - assert response.jukebox_role == user["jukeboxRole"] assert response.share_role == user["shareRole"] assert response.video_conversion_role == user["videoConversionRole"] + assert response.music_folder_id == user["musicFolderId"] + assert response.max_bit_rate == user["maxBitRate"] @responses.activate @@ -46,7 +49,26 @@ def test_create_user( ) -> None: responses.add(mock_create_user) - response = subsonic.user_management.create_user(User(**user)) + response = subsonic.user_management.create_user( + user["username"], + user["password"], + user["email"], + user["ldapAuthenticated"], + user["adminRole"], + user["settingsRole"], + user["streamRole"], + user["jukeboxRole"], + user["downloadRole"], + user["uploadRole"], + user["playlistRole"], + user["coverArtRole"], + user["commentRole"], + user["podcastRole"], + user["shareRole"], + user["videoConversionRole"], + user["musicFolderId"], + user["maxBitRate"], + ) assert response.username == user["username"] @@ -57,7 +79,26 @@ def test_update_user( ) -> None: responses.add(mock_update_user) - response = subsonic.user_management.update_user(User(**user)) + response = subsonic.user_management.update_user( + user["username"], + user["password"], + user["email"], + user["ldapAuthenticated"], + user["adminRole"], + user["settingsRole"], + user["streamRole"], + user["jukeboxRole"], + user["downloadRole"], + user["uploadRole"], + user["playlistRole"], + user["coverArtRole"], + user["commentRole"], + user["podcastRole"], + user["shareRole"], + user["videoConversionRole"], + user["musicFolderId"], + user["maxBitRate"], + ) assert response.username == user["username"] diff --git a/tests/mocks/user_management.py b/tests/mocks/user_management.py index 97c2fab..f3ee556 100644 --- a/tests/mocks/user_management.py +++ b/tests/mocks/user_management.py @@ -10,20 +10,23 @@ def user(username: str) -> dict[str, Any]: return { "username": username, + "password": "password", "email": f"{username}@example.com", - "scrobblingEnabled": True, + "ldapAuthenticated": False, "adminRole": False, "settingsRole": False, + "streamRole": True, + "jukeboxRole": False, "downloadRole": True, "uploadRole": False, "playlistRole": False, "coverArtRole": False, "commentRole": False, "podcastRole": False, - "streamRole": True, - "jukeboxRole": False, "shareRole": True, "videoConversionRole": False, + "musicFolderId": ["0", "1"], + "maxBitRate": 0, } diff --git a/tests/models/test_user.py b/tests/models/test_user.py index 2c494f3..48354ed 100644 --- a/tests/models/test_user.py +++ b/tests/models/test_user.py @@ -13,10 +13,10 @@ def test_user_generate(subsonic: Subsonic, mock_get_user, user: dict[str, Any]) responses.add(mock_get_user) response = subsonic.user_management.get_user(user["username"]) - response.scrobbling_enabled = False + response.admin_role = not user["adminRole"] response = response.generate() - assert response.scrobbling_enabled is True + assert response.admin_role == user["adminRole"] @responses.activate @@ -73,18 +73,3 @@ def test_user_change_password( response = response.change_password(new_password) assert type(response) is User - - -def test_user_without_api_access(user: dict[str, Any]) -> None: - no_api_user = User(**user) - - no_api_access_message = ( - "This user isn't associated with a Subsonic object." - + "A non None value in the subsonic property is required" - ) - - with pytest.raises( - NoApiAccess, - match=no_api_access_message, - ): - no_api_user.generate()