From 78fd34ae1cc77c547a625d67098927d752845daa Mon Sep 17 00:00:00 2001 From: Kutu Date: Sun, 8 Oct 2023 00:17:29 +0200 Subject: [PATCH] Reduce warning and bad practices --- src/knuckles/api.py | 16 +++-- src/knuckles/bookmarks.py | 34 ++++----- src/knuckles/browsing.py | 55 ++++++++------ src/knuckles/exceptions.py | 1 - src/knuckles/internet_radio.py | 24 +++---- src/knuckles/jukebox.py | 26 +++---- src/knuckles/media_annotation.py | 72 +++++++++---------- src/knuckles/media_retrieval.py | 59 +++++++-------- src/knuckles/models/bookmark.py | 2 +- src/knuckles/models/cover_art.py | 2 +- src/knuckles/models/genre.py | 2 +- src/knuckles/models/internet_radio_station.py | 19 +++-- src/knuckles/models/podcast.py | 2 +- src/knuckles/models/user.py | 30 ++++---- src/knuckles/playlists.py | 30 ++++---- src/knuckles/podcast.py | 40 +++++------ src/knuckles/searching.py | 1 + src/knuckles/sharing.py | 12 ++-- src/knuckles/subsonic.py | 4 +- tests/api/test_playlists.py | 3 +- tests/conftest.py | 30 ++++---- tests/mocks/browsing.py | 2 +- tests/mocks/internet_radio.py | 4 +- tests/mocks/jukebox_control.py | 11 ++- tests/mocks/media_retrieval.py | 4 +- tests/mocks/playlists.py | 2 +- tests/models/test_genre.py | 2 +- tests/models/test_jukebox.py | 8 +-- 28 files changed, 266 insertions(+), 231 deletions(-) diff --git a/src/knuckles/api.py b/src/knuckles/api.py index e9b81e7..a8be46b 100644 --- a/src/knuckles/api.py +++ b/src/knuckles/api.py @@ -38,7 +38,7 @@ def __init__( :type password: str :param client: A unique string identifying the client application. :type client: str - :param use_https: If the requests should be sended using HTTPS, + :param use_https: If the requests should be sent using HTTPS, defaults to True :type use_https: bool, optional :param use_token: If the connection should send to the server the clean password @@ -64,7 +64,9 @@ def __init__( else: self.url = f"http://{base_url}" - def generate_params(self, extra_params: dict[str, Any] = {}) -> dict[str, Any]: + def generate_params( + self, extra_params: dict[str, Any] | None = None + ) -> dict[str, Any]: """Generate the parameters for any request to the API. This allows the user to change any variable in any time without issues. @@ -84,9 +86,11 @@ def generate_params(self, extra_params: dict[str, Any] = {}) -> dict[str, Any]: "v": "1.16.1", "c": self.client, "f": "json", - **extra_params, } + if extra_params is not None: + params.update(extra_params) + # Add authentication based in the method selected by the user if not self.use_token: return { @@ -101,7 +105,9 @@ def generate_params(self, extra_params: dict[str, Any] = {}) -> dict[str, Any]: return {**params, "t": token, "s": salt} - def raw_request(self, endpoint: str, extra_params: dict[str, Any]) -> Response: + def raw_request( + self, endpoint: str, extra_params: dict[str, Any] | None = None + ) -> Response: """Make a request to the Subsonic API. :param endpoint: The endpoint where the request should be made, @@ -122,7 +128,7 @@ def raw_request(self, endpoint: str, extra_params: dict[str, Any]) -> Response: ) def json_request( - self, endpoint: str, extra_params: dict[str, Any] = {} + self, endpoint: str, extra_params: dict[str, Any] | None = None ) -> dict[str, Any]: """Make a request to the Subsonic API and returns a JSON response. Don't use with binary data endpoints. diff --git a/src/knuckles/bookmarks.py b/src/knuckles/bookmarks.py index 0c78144..8027a03 100644 --- a/src/knuckles/bookmarks.py +++ b/src/knuckles/bookmarks.py @@ -29,12 +29,12 @@ def get_bookmarks(self) -> list[Bookmark]: return [Bookmark(self.subsonic, **bookmark) for bookmark in response] - def get_bookmark(self, id: str) -> Bookmark | None: + def get_bookmark(self, id_: str) -> Bookmark | None: """Using the "getBookmarks" endpoint iterates over all the bookmarks and find the one with the same ID. - :param id: The ID of the song of the bookmark to find. - :type id: str + :param id_: The ID of the song of the bookmark to find. + :type id_: str :return: The found bookmark or None if no one is found. :rtype: Bookmark | None """ @@ -42,18 +42,18 @@ def get_bookmark(self, id: str) -> Bookmark | None: bookmarks = self.get_bookmarks() for bookmark in bookmarks: - if bookmark.song.id == id: + if bookmark.song.id == id_: return bookmark return None def create_bookmark( - self, id: str, position: int, comment: str | None = None + self, id_: str, position: int, comment: str | None = None ) -> Bookmark: """Calls the "createBookmark" endpoint of the API. - :param id: The ID of the song of the bookmark. - :type id: str + :param id_: The ID of the song of the bookmark. + :type id_: str :param position: The position in seconds of the bookmark. :type position: int :param comment: The comment of the bookmark, defaults to None. @@ -63,21 +63,21 @@ def create_bookmark( """ self.api.json_request( - "createBookmark", {"id": id, "position": position, "comment": comment} + "createBookmark", {"id": id_, "position": position, "comment": comment} ) # Fake the song structure given by in the API. - return Bookmark(self.subsonic, {"id": id}, position=position, comment=comment) + return Bookmark(self.subsonic, {"id": id_}, position=position, comment=comment) def update_bookmark( - self, id: str, position: int, comment: str | None = None + self, id_: str, position: int, comment: str | None = None ) -> Bookmark: """Method that internally calls the create_bookmark method as creating and updating a bookmark uses the same endpoint. Useful for having more self-descriptive code. - :param id: The ID of the song of the bookmark. - :type id: str + :param id_: The ID of the song of the bookmark. + :type id_: str :param position: The position in seconds of the bookmark. :type position: int :param comment: The comment of the bookmark, defaults to None. @@ -86,18 +86,18 @@ def update_bookmark( :rtype: Bookmark """ - return self.create_bookmark(id, position, comment) + return self.create_bookmark(id_, position, comment) - def delete_bookmark(self, id: str) -> "Subsonic": + def delete_bookmark(self, id_: str) -> "Subsonic": """Calls the "deleteBookmark" endpoint of the API. - :param id: The ID of the song of the bookmark to delete. - :type id: str + :param id_: The ID of the song of the bookmark to delete. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("deleteBookmark", {"id": id}) + self.api.json_request("deleteBookmark", {"id": id_}) return self.subsonic diff --git a/src/knuckles/browsing.py b/src/knuckles/browsing.py index d3998b6..91b62c1 100644 --- a/src/knuckles/browsing.py +++ b/src/knuckles/browsing.py @@ -86,67 +86,82 @@ def get_artists(self, music_folder_id: str | None = None) -> list[Artist]: return artists - def get_artist(self, id: str) -> Artist: + def get_artist(self, id_: str) -> Artist: """Calls the "getArtist" endpoint of the API. - :param id: The ID of the artist to get. - :type id: str + :param id_: The ID of the artist to get. + :type id_: str :return: An object with all the information that the server has given about the album. :rtype: Artist """ - response = self.api.json_request("getArtist", {"id": id})["artist"] + response = self.api.json_request("getArtist", {"id": id_})["artist"] return Artist(self.subsonic, **response) - def get_album(self, id: str) -> Album: + def get_album(self, id_: str) -> Album: """Calls the "getAlbum" endpoint of the API. - :param id: The ID of the album to get. - :type id: str + :param id_: The ID of the album to get. + :type id_: str :return: An object with all the information that the server has given about the album. :rtype: Album """ - response = self.api.json_request("getAlbum", {"id": id})["album"] + response = self.api.json_request("getAlbum", {"id": id_})["album"] return Album(self.subsonic, **response) - def get_album_info(self, id: str) -> AlbumInfo: + def get_album_info(self, id_: str) -> AlbumInfo: """Calls to the "getAlbumInfo2" endpoint of the API. - :param id: The ID of the album to get its info. - :type id: str + :param id_: The ID of the album to get its info. + :type id_: str :return: An object with all the extra info given by the server about the album. :rtype: AlbumInfo """ - response = self.api.json_request("getAlbumInfo2", {"id": id})["albumInfo"] + response = self.api.json_request("getAlbumInfo2", {"id": id_})["albumInfo"] - return AlbumInfo(self.subsonic, id, **response) + return AlbumInfo(self.subsonic, id_, **response) - def get_song(self, id: str) -> Song: + def get_song(self, id_: str) -> Song: """Calls to the "getSong" endpoint of the API. - :param id: The ID of the song to get. - :type id: str + :param id_: The ID of the song to get. + :type id_: str :return: An object with all the information that the server has given about the song. :rtype: Song """ - response = self.api.json_request("getSong", {"id": id})["song"] + response = self.api.json_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 + self, + id_: str, + count: int | None = None, + include_not_present: bool | None = None, ) -> ArtistInfo: + """Calls the "getArtistInfo" endpoint of the API. + + :param id_: The id of the artist to get its info + :type id_: + :param count: + :type count: + :param include_not_present: + :type include_not_present: + :return: + :rtype: + """ + response = self.api.json_request( "getArtistInfo2", - {"id": id, "count": count, "includeNotPresent": include_not_present}, + {"id": id_, "count": count, "includeNotPresent": include_not_present}, )["artistInfo2"] - return ArtistInfo(self.subsonic, id, **response) + return ArtistInfo(self.subsonic, id_, **response) diff --git a/src/knuckles/exceptions.py b/src/knuckles/exceptions.py index 34905a3..7ee4abd 100644 --- a/src/knuckles/exceptions.py +++ b/src/knuckles/exceptions.py @@ -111,4 +111,3 @@ def get_code_error_exception( return CodeError70 case _: return UnknownErrorCode - return UnknownErrorCode diff --git a/src/knuckles/internet_radio.py b/src/knuckles/internet_radio.py index 6f85ff6..011d0b6 100644 --- a/src/knuckles/internet_radio.py +++ b/src/knuckles/internet_radio.py @@ -34,12 +34,12 @@ def get_internet_radio_stations( return [InternetRadioStation(self.subsonic, **station) for station in response] - def get_internet_radio_station(self, id: str) -> InternetRadioStation | None: + def get_internet_radio_station(self, id_: str) -> InternetRadioStation | None: """Using the "getInternetRadioStation" endpoint iterates over all the stations and find the one with the same ID. - :param id: The ID of the station to find. - :type id: str + :param id_: The ID of the station to find. + :type id_: str :return: The found internet radio station or None if no one is found. :rtype: InternetRadioStation | None """ @@ -47,7 +47,7 @@ def get_internet_radio_station(self, id: str) -> InternetRadioStation | None: stations = self.get_internet_radio_stations() for station in stations: - if station.id == id: + if station.id == id_: return station return None @@ -75,12 +75,12 @@ def create_internet_radio_station( return self.subsonic def update_internet_radio_station( - self, id: str, stream_url: str, name: str, homepage_url: str | None = None + self, id_: str, stream_url: str, name: str, homepage_url: str | None = None ) -> "Subsonic": """Calls the "updateInternetRadioStation" endpoint ot the API. - :param id: The ID of the station to update. - :type id: str + :param id_: The ID of the station to update. + :type id_: str :param stream_url: The new steam url of the station. :type stream_url: str :param name: The new name of the station. @@ -95,7 +95,7 @@ def update_internet_radio_station( self.api.json_request( "updateInternetRadioStation", { - "id": id, + "id": id_, "streamUrl": stream_url, "name": name, "homepageUrl": homepage_url, @@ -104,15 +104,15 @@ def update_internet_radio_station( return self.subsonic - def delete_internet_radio_station(self, id: str) -> "Subsonic": + def delete_internet_radio_station(self, id_: str) -> "Subsonic": """Calls the "deleteInternetRadioStation" endpoint of the API. - :param id: The ID of the station to delete - :type id: str + :param id_: The ID of the station to delete + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("deleteInternetRadioStation", {"id": id}) + self.api.json_request("deleteInternetRadioStation", {"id": id_}) return self.subsonic diff --git a/src/knuckles/jukebox.py b/src/knuckles/jukebox.py index aed66f2..6d29536 100644 --- a/src/knuckles/jukebox.py +++ b/src/knuckles/jukebox.py @@ -46,21 +46,21 @@ def status(self) -> Jukebox: return Jukebox(self.subsonic, **response) - def set(self, id: str) -> Jukebox: + def set(self, id_: str) -> Jukebox: """Calls the "jukeboxControl" endpoint of the API with the action "set". - :param id: The ID of a song to set it in the jukebox. - :type id: str + :param id_: The ID of a song to set it in the jukebox. + :type id_: str :return: An object with all the given information about the jukebox. :rtype: Jukebox """ - response = self.api.json_request("jukeboxControl", {"action": "set", "id": id})[ - "jukeboxStatus" - ] + response = self.api.json_request( + "jukeboxControl", {"action": "set", "id": id_} + )["jukeboxStatus"] # Preset the song list as this call changes it in a predictable way - return Jukebox(self.subsonic, **response, entry=[{"id": id}]) + return Jukebox(self.subsonic, **response, entry=[{"id": id_}]) def start(self) -> Jukebox: """Calls the "jukeboxControl" endpoint of the API with the action "start". @@ -108,19 +108,19 @@ def skip(self, index: int, offset: float = 0) -> Jukebox: return Jukebox(self.subsonic, **response) - def add(self, id: str) -> Jukebox: + def add(self, id_: str) -> Jukebox: """Calls the "jukeboxControl" endpoint of the API with the action "add". - :param id: The ID of a song to add it in the jukebox. - :type id: str + :param id_: The ID of a song to add it in the jukebox. + :type id_: str :return: An object with all the given information about the jukebox. Except the jukebox playlist. :rtype: Jukebox """ - response = self.api.json_request("jukeboxControl", {"action": "add", "id": id})[ - "jukeboxStatus" - ] + response = self.api.json_request( + "jukeboxControl", {"action": "add", "id": id_} + )["jukeboxStatus"] return Jukebox(self.subsonic, **response) diff --git a/src/knuckles/media_annotation.py b/src/knuckles/media_annotation.py index 1f48fa6..4926d3a 100644 --- a/src/knuckles/media_annotation.py +++ b/src/knuckles/media_annotation.py @@ -18,89 +18,89 @@ def __init__(self, api: Api, subsonic: "Subsonic") -> None: self.api = api self.subsonic = subsonic - def star_song(self, id: str) -> "Subsonic": + def star_song(self, id_: str) -> "Subsonic": """Calls the "star" endpoint of the API. - :param id: The ID of a song to star. - :type id: str + :param id_: The ID of a song to star. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("star", {"id": id}) + self.api.json_request("star", {"id": id_}) return self.subsonic - def star_album(self, id: str) -> "Subsonic": + def star_album(self, id_: str) -> "Subsonic": """Calls the "star" endpoint of the API. - :param id: The ID of an album to star. - :type id: str + :param id_: The ID of an album to star. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("star", {"albumId": id}) + self.api.json_request("star", {"albumId": id_}) return self.subsonic - def star_artist(self, id: str) -> "Subsonic": + def star_artist(self, id_: str) -> "Subsonic": """Calls the "star" endpoint of the API. - :param id: The ID of an artist to star. - :type id: str + :param id_: The ID of an artist to star. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("star", {"artistId": id}) + self.api.json_request("star", {"artistId": id_}) return self.subsonic - def unstar_song(self, id: str) -> "Subsonic": + def unstar_song(self, id_: str) -> "Subsonic": """Calls the "unstar" endpoint of the API. - :param id: The ID of a song to unstar. - :type id: str + :param id_: The ID of a song to unstar. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("unstar", {"id": id}) + self.api.json_request("unstar", {"id": id_}) return self.subsonic - def unstar_album(self, id: str) -> "Subsonic": + def unstar_album(self, id_: str) -> "Subsonic": """Calls the "unstar" endpoint of the API. - :param id: The ID of an album to unstar. - :type id: str + :param id_: The ID of an album to unstar. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("unstar", {"albumId": id}) + self.api.json_request("unstar", {"albumId": id_}) return self.subsonic - def unstar_artist(self, id: str) -> "Subsonic": + def unstar_artist(self, id_: str) -> "Subsonic": """Calls the "unstar" endpoint of the API. - :param id: The ID of an artist to unstar. - :type id: str + :param id_: The ID of an artist to unstar. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("unstar", {"artistId": id}) + self.api.json_request("unstar", {"artistId": id_}) return self.subsonic - def set_rating(self, id: str, rating: int) -> "Subsonic": + def set_rating(self, id_: str, rating: int) -> "Subsonic": """Calls to the "setRating" endpoint of the API. - :param id: The ID of a song to set its rating. - :type id: str + :param id_: The ID of a song to set its rating. + :type id_: str :param rating: The rating to set. It should be a number between 1 and 5 (inclusive). :type rating: int @@ -118,30 +118,30 @@ def set_rating(self, id: str, rating: int) -> "Subsonic": ) ) - self.api.json_request("setRating", {"id": id, "rating": rating}) + self.api.json_request("setRating", {"id": id_, "rating": rating}) return self.subsonic - def remove_rating(self, id: str) -> "Subsonic": + def remove_rating(self, id_: str) -> "Subsonic": """Calls the "setRating" endpoint of the API with a rating of 0. - :param id: The ID of a song to set its rating. - :type id: str + :param id_: The ID of a song to set its rating. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("setRating", {"id": id, "rating": 0}) + self.api.json_request("setRating", {"id": id_, "rating": 0}) return self.subsonic def scrobble( - self, id: list[str], time: datetime, submission: bool = True + self, id_: list[str], time: datetime, submission: bool = True ) -> "Subsonic": """Calls to the "scrobble" endpoint of the API - :param id: The list of song IDs to scrobble. - :type id: list[str] + :param id_: The list of song IDs to scrobble. + :type id_: list[str] :param time: The time at which the song was listened to. :type time: datetime :param submission: If the scrobble is a submission @@ -155,7 +155,7 @@ 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(time.timestamp() * 1000), "submission": submission}, ) return self.subsonic diff --git a/src/knuckles/media_retrieval.py b/src/knuckles/media_retrieval.py index e8a69fc..81ba75e 100644 --- a/src/knuckles/media_retrieval.py +++ b/src/knuckles/media_retrieval.py @@ -44,7 +44,8 @@ def _generate_url(self, endpoint: str, params: dict[str, Any]) -> str: # as the prepare_url method always set it to a string. return prepared_request.url # type: ignore [return-value] - def _download_file(self, response: Response, downloaded_file_path: Path) -> Path: + @staticmethod + def _download_file(response: Response, downloaded_file_path: Path) -> Path: """Downloads a file attached to a Response object. :param response: The response to get the download binary data. @@ -65,27 +66,27 @@ def _download_file(self, response: Response, downloaded_file_path: Path) -> Path def stream( self, - id: str, + id_: str, max_bitrate_rate: int | None = None, - format: str | None = None, + format_: str | None = None, time_offset: int | None = None, size: str | None = None, estimate_content_length: bool | None = None, converted: bool | None = None, ) -> str: - """Returns a valid url for streaming the requested song or bideo + """Returns a valid url for streaming the requested song or video - :param id: The id of the song or video to stream - :type id: str + :param id_: The id of the song or video to stream + :type id_: str :param max_bitrate_rate: A limit for the stream bitrate :type max_bitrate_rate: int | None - :param format: The file format of preference to be used in the stream. - :type format: str | None + :param format_: The file format of preference to be used in the stream. + :type format_: str | None :param time_offset: Only applicable to video streaming. An offset in seconds from where the video should start. :type time_offset: int | None :param size: Only applicable to video streaming. - The resolution for the streamed video, in the format of "WIDTHxHEIGHT". + The resolution for the streamed video, in the format of "WIDTHHxHEIGHT". :type size: str | None :param estimate_content_length: If the response should set a Content-Length HTTP header with an estimation of the duration of the media. @@ -100,9 +101,9 @@ def stream( return self._generate_url( "stream", { - "id": id, + "id": id_, "maxBitRate": max_bitrate_rate, - "format": format, + "format": format_, "timeOffset": time_offset, "size": size, "estimateContentLength": estimate_content_length, @@ -110,11 +111,11 @@ def stream( }, ) - def download(self, id: str, file_or_directory_path: Path) -> Path: + def download(self, id_: str, file_or_directory_path: Path) -> Path: """Calls the "download" endpoint of the API. - :param id: The id of the song or video to download. - :type id: str + :param id_: The id of the song or video to download. + :type id_: str :param file_or_directory_path: If a directory path is passed the file will be inside of it with the default filename given by the API, if not the file will be saved directly in the given path. @@ -123,7 +124,7 @@ def download(self, id: str, file_or_directory_path: Path) -> Path: :rtype Path """ - response = self.api.raw_request("download", {"id": id}) + 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) @@ -142,14 +143,14 @@ def download(self, id: str, file_or_directory_path: Path) -> Path: def hls( self, - id: str, + id_: str, custom_bitrates: list[str] | None = None, audio_track_id: str | None = None, ) -> str: """Returns a valid url for streaming the requested song with hls.m3u8 - :param id: The id of the song to stream. - :type id: str + :param id_: The id of the song to stream. + :type id_: str :param custom_bitrates: A list of bitrates to be added to the hls playlist for video streaming, the resolution can also be specified with this format: "BITRATE@WIDTHxHEIGHT". @@ -163,19 +164,19 @@ def hls( return self._generate_url( "hls.m3u8", - {"id": id, "bitRate": custom_bitrates, "audioTrack": audio_track_id}, + {"id": id_, "bitRate": custom_bitrates, "audioTrack": audio_track_id}, ) def get_captions( self, - id: str, + id_: str, file_or_directory_path: Path, subtitles_file_format: SubtitlesFileFormat = SubtitlesFileFormat.VTT, ) -> Path: """Calls the "getCaptions" endpoint of the API. - :param id: The ID of the video to get the captions - :type id: str + :param id_: The ID of the video to get the captions + :type id_: str :param file_or_directory_path: If a directory path is passed the file will be inside of it with the default filename given by the API, if not the file will be saved directly in the given path. @@ -191,7 +192,7 @@ def get_captions( response = self.api.raw_request( "getCaptions", - {"id": id, "format": subtitles_file_format.value}, + {"id": id_, "format": subtitles_file_format.value}, ) if not file_or_directory_path.is_dir(): @@ -205,17 +206,17 @@ def get_captions( else: file_extension = ".srt" - filename = 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) def get_cover_art( - self, id: str, file_or_directory_path: Path, size: int | None = None + self, id_: str, file_or_directory_path: Path, size: int | None = None ) -> Path: """Calls the "getCoverArt" endpoint of the API. - :param id: The id of the cover art to download. - :type id: str + :param id_: The id of the cover art to download. + :type id_: str :param file_or_directory_path: If a directory path is passed the file will be inside of it with the filename being the name of the user and a guessed file extension, if not the file will be saved @@ -227,7 +228,7 @@ def get_cover_art( :rtype Path """ - response = self.api.raw_request("getCoverArt", {"id": id, "size": size}) + 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) @@ -236,7 +237,7 @@ def get_cover_art( response.headers["content-type"].partition(";")[0].strip() ) - filename = 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) diff --git a/src/knuckles/models/bookmark.py b/src/knuckles/models/bookmark.py index 2aa459e..804ea6f 100644 --- a/src/knuckles/models/bookmark.py +++ b/src/knuckles/models/bookmark.py @@ -70,7 +70,7 @@ def create(self) -> Self: def update(self) -> Self: """Calls the "createBookmark" endpoint of the API, as creating and updating - a bookmark uses the same endpoint. Useful for having more self descriptive code. + a bookmark uses the same endpoint. Useful for having more self-descriptive code. :return: The object itself to allow method chaining. :rtype: Self diff --git a/src/knuckles/models/cover_art.py b/src/knuckles/models/cover_art.py index 61542b9..94f611f 100644 --- a/src/knuckles/models/cover_art.py +++ b/src/knuckles/models/cover_art.py @@ -2,7 +2,7 @@ class CoverArt: """Representation of all the data related to a cover art in Subsonic.""" def __init__(self, id: str) -> None: - """Representation of all the data related a to cover art in Subsonic. + """Representation of all the data related to a cover art in Subsonic. :param id: The ID of the cover art. :type id: str diff --git a/src/knuckles/models/genre.py b/src/knuckles/models/genre.py index d9102a3..698c790 100644 --- a/src/knuckles/models/genre.py +++ b/src/knuckles/models/genre.py @@ -16,7 +16,7 @@ def __init__( songCount: int | None = None, albumCount: int | None = None, ) -> None: - """Representation of all the data related a to genre in Subsonic. + """Representation of all the data related to a genre in Subsonic. :param subsonic: The subsonic object to make all the internal requests with it. :type subsonic: Subsonic diff --git a/src/knuckles/models/internet_radio_station.py b/src/knuckles/models/internet_radio_station.py index 119156d..e1b1d0c 100644 --- a/src/knuckles/models/internet_radio_station.py +++ b/src/knuckles/models/internet_radio_station.py @@ -8,23 +8,28 @@ class InternetRadioStation: """Representation of all the data related to - a internet radio station in Subsonic. + an internet radio station in Subsonic. """ def __init__( - self, subsonic: "Subsonic", id: str, name: str, streamUrl: str, homepageUrl: str + self, + subsonic: "Subsonic", + id: str, + name: str, + streamUrl: str, + homepageUrl: str, ) -> None: """Representation of all the data related to - a internet radio station in Subsonic. + an internet radio station in Subsonic. + :param id: The id of the radio station. + :type streamUrl: str + :param name: The name of the radio station. + :type name: str :param subsonic: The subsonic object to make all the internal requests with it. :type subsonic: Subsonic - :param streamUrl: The id of the radio station. - :type streamUrl: str :param streamUrl: The stream url of the radio station. :type streamUrl: str - :param name: The name of the radio station. - :type name: str :param homepageUrl: The url of the homepage of the radio station. :type homepageUrl: str """ diff --git a/src/knuckles/models/podcast.py b/src/knuckles/models/podcast.py index 4e1edbe..9db3bc0 100644 --- a/src/knuckles/models/podcast.py +++ b/src/knuckles/models/podcast.py @@ -217,7 +217,7 @@ def create(self) -> Self: """ # Ignore the None type error as the server - # should return a Error Code 10 in response + # should return an Error Code 10 in response self.__subsonic.podcast.create_podcast_channel( self.url # type: ignore[arg-type] ) diff --git a/src/knuckles/models/user.py b/src/knuckles/models/user.py index c3a7154..f33f967 100644 --- a/src/knuckles/models/user.py +++ b/src/knuckles/models/user.py @@ -1,10 +1,10 @@ from typing import TYPE_CHECKING, Self -from ..exceptions import NoApiAccess - if TYPE_CHECKING: from ..subsonic import Subsonic +from ..exceptions import NoApiAccess + class User: """Representation of all the data related to a user in Subsonic.""" @@ -36,39 +36,39 @@ def __init__( :type username: str :param email: The email of the user :type email: str - :param scrobblingEnabled: If the user is allow to do scrobbling, + :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 is allow to modify global settings, + :param settingsRole: If the user can modify global settings, defaults to False. :type settingsRole: bool, optional - :param downloadRole: If the user is allow to download songs, defaults to False. + :param downloadRole: If the user can download songs, defaults to False. :type downloadRole: bool, optional - :param uploadRole: If the user is allow to upload data to the server, + :param uploadRole: If the user can upload data to the server, defaults to False. :type uploadRole: bool, optional - :param playlistRole: If the user is allow to use playlist, defaults to False. + :param playlistRole: If the user can use playlist, defaults to False. :type playlistRole: bool, optional - :param coverArtRole: If the user is allow to access cover arts, + :param coverArtRole: If the user can access cover arts, defaults to False. :type coverArtRole: bool, optional - :param commentRole: If the user is allow to do comments, defaults to False. + :param commentRole: If the user can do comments, defaults to False. :type commentRole: bool, optional - :param podcastRole: If the user is allow to listen to podcasts, + :param podcastRole: If the user can listen to podcasts, defaults to False. :type podcastRole: bool, optional - :param streamRole: If the user is allow to listen media with streaming, + :param streamRole: If the user can listen media with streaming, defaults to False. :type streamRole: bool, optional - :param jukeboxRole: If the user is allow to use the jukebox, defaults to False + :param jukeboxRole: If the user can use the jukebox, defaults to False :type jukeboxRole: bool, optional - :param shareRole: If the user is allow to use sharing capabilities, + :param shareRole: If the user can use sharing capabilities, defaults to False :type shareRole: bool, optional - :param videoConversionRole: If the user is allow to do video conversion, + :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, @@ -108,7 +108,7 @@ def __check_api_access(self) -> None: ) def generate(self) -> "User": - """Returns the function to the the same user with the maximum possible + """Returns the function to the same user with the maximum possible information from the Subsonic API. Useful for making copies with updated data or updating the object itself diff --git a/src/knuckles/playlists.py b/src/knuckles/playlists.py index 0f9fc57..5134ff1 100644 --- a/src/knuckles/playlists.py +++ b/src/knuckles/playlists.py @@ -37,15 +37,15 @@ def get_playlists(self, username: str | None = None) -> list[Playlist]: return playlists - def get_playlist(self, id: str) -> Playlist: + def get_playlist(self, id_: str) -> Playlist: """Calls to the "getPlaylist" endpoint of the API. - :param id: The ID of the playlist to get. - :type id: str + :param id_: The ID of the playlist to get. + :type id_: str :return: The requested playlist. :rtype: Playlist """ - response = self.api.json_request("getPlaylist", {"id": id})["playlist"] + response = self.api.json_request("getPlaylist", {"id": id_})["playlist"] return Playlist(self.subsonic, **response) @@ -79,7 +79,7 @@ def create_playlist( new_playlist = Playlist(self.subsonic, **response) - # Allow modify comment and public + # Allow to modify comment and public # with a workaround using the updatePlaylist endpoint if comment or public: @@ -91,7 +91,7 @@ def create_playlist( def update_playlist( self, - id: str, + id_: str, name: str | None = None, comment: str | None = None, public: bool | None = None, @@ -100,8 +100,8 @@ def update_playlist( ) -> Playlist: """Calls the "updatePlaylist" endpoint of the API. - :param id: The ID of the playlist to update. - :type id: str + :param id_: The ID of the playlist to update. + :type id_: str :param name: A new name for the playlist, defaults to None. :type name: str | None, optional :param comment: A new comment for the playlist, defaults to None. @@ -120,7 +120,7 @@ def update_playlist( self.api.json_request( "updatePlaylist", { - "playlistId": id, + "playlistId": id_, "name": name, "comment": comment, "public": public, @@ -129,16 +129,18 @@ def update_playlist( }, ) - return Playlist(self.subsonic, id=id, name=name, comment=comment, public=public) + return Playlist( + self.subsonic, id=id_, name=name, comment=comment, public=public + ) - def delete_playlist(self, id: str) -> "Subsonic": + def delete_playlist(self, id_: str) -> "Subsonic": """Calls the "deletePlaylist" endpoint of the API. - :param id: The ID of the song to remove. - :type id: str + :param id_: The ID of the song to remove. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("deletePlaylist", {"id": id}) + self.api.json_request("deletePlaylist", {"id": id_}) return self.subsonic diff --git a/src/knuckles/podcast.py b/src/knuckles/podcast.py index 9cc3e63..86427d7 100644 --- a/src/knuckles/podcast.py +++ b/src/knuckles/podcast.py @@ -35,12 +35,12 @@ def get_podcasts(self, with_episodes: bool = True) -> list[Channel]: return [Channel(self.subsonic, **channel) for channel in response] - def get_podcast(self, id: str, with_episodes: bool = True) -> Channel: + def get_podcast(self, id_: str, with_episodes: bool = True) -> Channel: """Calls the "getPodcasts" endpoint of the API with a specific ID to only return the desired podcast channel. - :param id: The ID of the channel to get. - :type id: str + :param id_: The ID of the channel to get. + :type id_: str :param with_episodes: If the channels should also have all the episodes inside of them, defaults to True. :type with_episodes: bool, optional @@ -49,7 +49,7 @@ def get_podcast(self, id: str, with_episodes: bool = True) -> Channel: """ response = self.api.json_request( - "getPodcasts", {"id": id, "includeEpisodes": with_episodes} + "getPodcasts", {"id": id_, "includeEpisodes": with_episodes} )["podcasts"][0] return Channel(self.subsonic, **response) @@ -69,12 +69,12 @@ def get_newest_podcasts(self, number_max_episodes: int) -> list[Episode]: return [Episode(self.subsonic, **episode) for episode in response] - def get_episode(self, id: str) -> Episode | None: + def get_episode(self, id_: str) -> Episode | None: """Calls the "getPodcasts" endpoints of the API and search through all the episodes to find the one with the same ID of the provided ID. - :param id: The provided episode ID. - :type id: str + :param id_: The provided episode ID. + :type id_: str :return: The episode with the same ID or None if no episode with the ID are found. :rtype: Episode | None @@ -91,7 +91,7 @@ def get_episode(self, id: str) -> Episode | None: ] for episode in list_of_episodes: - if episode.id == id: + if episode.id == id_: return episode return None @@ -120,41 +120,41 @@ def create_podcast_channel(self, url: str) -> "Subsonic": return self.subsonic - def delete_podcast_channel(self, id: str) -> "Subsonic": + def delete_podcast_channel(self, id_: str) -> "Subsonic": """Calls the "deletePodcastChannel" endpoint of the API. - :param id: The ID of the channel to delete. - :type id: str + :param id_: The ID of the channel to delete. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("deletePodcastChannel", {"id": id}) + self.api.json_request("deletePodcastChannel", {"id": id_}) return self.subsonic - def download_podcast_episode(self, id: str) -> "Subsonic": + def download_podcast_episode(self, id_: str) -> "Subsonic": """Calls the "downloadPodcastEpisode" endpoint of the API. - :param id: The ID of the episode to download. - :type id: str + :param id_: The ID of the episode to download. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("downloadPodcastEpisode", {"id": id}) + self.api.json_request("downloadPodcastEpisode", {"id": id_}) return self.subsonic - def delete_podcast_episode(self, id: str) -> "Subsonic": + def delete_podcast_episode(self, id_: str) -> "Subsonic": """Calls the "deletePodcastEpisode" endpoint of the API. - :param id: The ID of the episode to delete. - :type id: str + :param id_: The ID of the episode to delete. + :type id_: str :return: The object itself to allow method chaining. :rtype: Subsonic """ - self.api.json_request("deletePodcastEpisode", {"id": id}) + self.api.json_request("deletePodcastEpisode", {"id": id_}) return self.subsonic diff --git a/src/knuckles/searching.py b/src/knuckles/searching.py index a3658eb..e17e058 100644 --- a/src/knuckles/searching.py +++ b/src/knuckles/searching.py @@ -69,6 +69,7 @@ def search( "albumOffset": album_offset, "artistCount": artist_count, "artistOffset": artist_offset, + "musicFolderId": music_folder_id, }, )["searchResult3"] diff --git a/src/knuckles/sharing.py b/src/knuckles/sharing.py index 93da17d..b43709a 100644 --- a/src/knuckles/sharing.py +++ b/src/knuckles/sharing.py @@ -31,12 +31,12 @@ def get_shares(self) -> list[Share]: return [Share(self.subsonic, **share) for share in response] - def get_share(self, id: str) -> Share | None: + def get_share(self, id_: str) -> Share | None: """Using the "getShares" endpoint iterates over all the shares and find the one with the same ID. - :param id: The ID of the share to find. - :type id: str + :param id_: The ID of the share to find. + :type id_: str :return: The found share or None if no one is found. :rtype: Share | None """ @@ -44,7 +44,7 @@ def get_share(self, id: str) -> Share | None: shares = self.get_shares() for share in shares: - if share.id == id: + if share.id == id_: return share return None @@ -61,7 +61,7 @@ def create_share( :type songs_ids: list[str] :param description: A description for the share, defaults to None. :type description: str | None, optional - :param expires: The time when the share should expires, defaults to None. + :param expires: The time when the share should expire, defaults to None. :type expires: datetime | None, optional :return: The new created share. :rtype: Share @@ -90,7 +90,7 @@ def update_share( :type share_id: str :param new_description: A new description for the share, defaults to None. :type new_description: str | None, optional - :param new_expires: A new expire date fot the share, defaults to None. + :param new_expires: A new expiry date fot the share, defaults to None. :type new_expires: datetime | None, optional :return: A Share object with all the updated info. :rtype: Share diff --git a/src/knuckles/subsonic.py b/src/knuckles/subsonic.py index 7f9b66a..9e513f6 100644 --- a/src/knuckles/subsonic.py +++ b/src/knuckles/subsonic.py @@ -37,7 +37,7 @@ def __init__( :type password: str :param client: A unique string identifying the client application. :type client: str - :param use_https: If the requests should be sended using HTTPS, + :param use_https: If the requests should be sent using HTTPS, defaults to True :type use_https: bool, optional :param use_token: If the connection should send to the server the clean password @@ -48,7 +48,7 @@ def __init__( self.api = Api(url, user, password, client, use_https, use_token) self.system = System(self.api) self.browsing = Browsing(self.api, self) - self.lists = None #! ? + self.lists = None # !! self.searching = Searching(self.api, self) self.playlists = Playlists(self.api, self) self.media_retrieval = MediaRetrieval(self.api) diff --git a/tests/api/test_playlists.py b/tests/api/test_playlists.py index 75271ff..b0e5ee1 100644 --- a/tests/api/test_playlists.py +++ b/tests/api/test_playlists.py @@ -71,9 +71,8 @@ def test_create_playlist( playlist: dict[str, Any], song: dict[str, Any], ) -> None: - responses.add(mock_create_playlist) responses.add(mock_update_comment_and_public) - + responses.add(mock_create_playlist) response = subsonic.playlists.create_playlist( playlist["name"], playlist["comment"], playlist["public"], [song["id"]] ) diff --git a/tests/conftest.py b/tests/conftest.py index 18689f1..7e17146 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -37,9 +37,9 @@ class MockGenerator(Protocol): def __call__( self, endpoint: str, - extra_params: dict[str, Any] = {}, - extra_data: dict[str, Any] = {}, - headers: dict[str, str] = {}, + extra_params: dict[str, Any] | None = None, + extra_data: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, content_type: str = "", body: Any = None, ) -> Response: @@ -54,25 +54,27 @@ def mock_generator( ) -> MockGenerator: def inner( endpoint: str, - extra_params: dict[str, Any] = {}, - extra_data: dict[str, Any] = {}, - headers: dict[str, str] = {}, + extra_params: dict[str, Any] | None = None, + extra_data: dict[str, Any] | None = None, + headers: dict[str, str] | None = None, content_type: str = "", body: Any = None, ) -> Response: + mocked_params = params.copy() + if extra_params is not None: + mocked_params.update(extra_params) + + mocked_data = {"subsonic-response": {**subsonic_response}} + if extra_data is not None: + mocked_data["subsonic-response"].update(extra_data) + return Response( method=GET, headers=headers, content_type=content_type, url=f"{base_url}/rest/{endpoint}", - match=[ - matchers.query_param_matcher( - {**params, **extra_params}, strict_match=False - ) - ], - json={"subsonic-response": {**subsonic_response, **extra_data}} - if body is None - else None, + match=[matchers.query_param_matcher(mocked_params, strict_match=False)], + json=mocked_data if body is None else None, body=body, status=200, ) diff --git a/tests/mocks/browsing.py b/tests/mocks/browsing.py index c62cf3f..f075406 100644 --- a/tests/mocks/browsing.py +++ b/tests/mocks/browsing.py @@ -48,7 +48,7 @@ def artist(base_url: str, album: dict[str, Any]) -> dict[str, Any]: @pytest.fixture() def artists(artist: dict[str, Any]) -> dict[str, Any]: return { - "ignoredArticles": "The An A Die Das Ein Eine Les Le La", + "ignoredArticles": "The An A Die Das Ein Les Le La", "index": [ { "name": "#", diff --git a/tests/mocks/internet_radio.py b/tests/mocks/internet_radio.py index 904655f..63cabe7 100644 --- a/tests/mocks/internet_radio.py +++ b/tests/mocks/internet_radio.py @@ -11,8 +11,8 @@ def internet_radio_station() -> dict[str, Any]: return { "id": "1", "name": "HBR1.com - Dream Factory", - "streamUrl": "http://ubuntu.hbr1.com:19800/ambient.aac", - "homepageUrl": "http://www.hbr1.com/", + "streamUrl": "https://ubuntu.hbr1.com:19800/ambient.aac", + "homepageUrl": "https://www.hbr1.com/", } diff --git a/tests/mocks/jukebox_control.py b/tests/mocks/jukebox_control.py index f94c56a..de03412 100644 --- a/tests/mocks/jukebox_control.py +++ b/tests/mocks/jukebox_control.py @@ -26,7 +26,7 @@ class JukeboxStatusGenerator(Protocol): def __call__( self, action: str, - extra_params: dict[str, Any] = {}, + extra_params: dict[str, Any] | None = None, ) -> Response: ... @@ -43,10 +43,15 @@ def jukebox_status_generator( """A factory function to generate all the Response objects that returns jukebox_status as their data.""" - def inner(action: str, extra_params: dict[str, Any] = {}) -> Response: + def inner(action: str, extra_params: dict[str, Any] | None = None) -> Response: + mocked_params = {"action": action} + + if extra_params is not None: + mocked_params.update(extra_params) + return mock_generator( "jukeboxControl", - {"action": action, **extra_params}, + mocked_params, {"jukeboxStatus": jukebox_status}, ) diff --git a/tests/mocks/media_retrieval.py b/tests/mocks/media_retrieval.py index 467a4c6..8eeb291 100644 --- a/tests/mocks/media_retrieval.py +++ b/tests/mocks/media_retrieval.py @@ -18,7 +18,7 @@ def __call__( endpoint: str, extra_params: dict[str, Any], content_type: str, - headers: dict[str, str] = {}, + headers: dict[str, str] | None = None, ) -> Response: ... @@ -31,7 +31,7 @@ def inner( endpoint: str, extra_params: dict[str, Any], content_type: str, - headers: dict[str, str] = {}, + headers: dict[str, str] | None = None, ): fake_file = tmp_path / "file.mock" fake_file.touch() diff --git a/tests/mocks/playlists.py b/tests/mocks/playlists.py index a6e3a95..19dc785 100644 --- a/tests/mocks/playlists.py +++ b/tests/mocks/playlists.py @@ -10,7 +10,7 @@ def playlist(song: dict[str, Any], username: str) -> dict[str, Any]: return { "id": "800000075", - "name": "testcreate", + "name": "Chill", "comment": "comment", "owner": username, "public": True, diff --git a/tests/models/test_genre.py b/tests/models/test_genre.py index 00f4e35..72940a9 100644 --- a/tests/models/test_genre.py +++ b/tests/models/test_genre.py @@ -23,7 +23,7 @@ def test_generate( @responses.activate def test_generate_nonexistent_genre( - subsonic: Subsonic, mock_get_genres: Response, genre: dict[str, Any] + subsonic: Subsonic, mock_get_genres: Response ) -> None: responses.add(mock_get_genres) diff --git a/tests/models/test_jukebox.py b/tests/models/test_jukebox.py index 1a1f4e2..60569ad 100644 --- a/tests/models/test_jukebox.py +++ b/tests/models/test_jukebox.py @@ -103,7 +103,7 @@ def test_jukebox_shuffle( response = response.shuffle() assert type(response) is Jukebox - # Ignore the error as in normal conditions it should exists + # Ignore the error as in normal conditions it should exist assert response.playlist[0].id == song["id"] # type: ignore[index] @@ -152,7 +152,7 @@ def test_jukebox_set( response = response.set(song["id"]) assert type(response) is Jukebox - # Ignore the error as in normal conditions it should exists + # Ignore the error as in normal conditions it should exist assert response.playlist[0].id == song["id"] # type: ignore[index] @@ -186,7 +186,7 @@ def test_jukebox_add_with_a_populated_playlist( response = response.add(song["id"]) assert type(response) is Jukebox - # Ignore the error as in normal conditions it should exists + # Ignore the error as in normal conditions it should exist assert response.playlist[1].id == song["id"] # type: ignore[index] @@ -206,5 +206,5 @@ def test_jukebox_add_without_a_populated_playlist( response = response.add(song["id"]) assert type(response) is Jukebox - # Ignore the error as in normal conditions it should exists + # Ignore the error as in normal conditions it should exist assert response.playlist[0].id == song["id"] # type: ignore[index]