diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0fb0894 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Changelog + +## 0.1.0 +- PyPI automatic CI update upload test. diff --git a/TODO.md b/TODO.md index 5914000..e9c91cc 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,6 @@ # TODO -## **Fix POST support.** +- [ ] Remove prints. +- [ ] Make the README.md more beautiful. +- [ ] Add tags to PyPI +- [ ] Release the `1.0.0`. diff --git a/src/knuckles/_api.py b/src/knuckles/_api.py index 5abe77d..2aa303e 100644 --- a/src/knuckles/_api.py +++ b/src/knuckles/_api.py @@ -1,5 +1,4 @@ import hashlib -import json import secrets from enum import Enum from typing import Any @@ -146,7 +145,7 @@ def raw_request( case RequestMethod.POST: return requests.post( url=f"{self.url}/rest/{endpoint}", - data=json.dumps(self._generate_params(extra_params)), + data=self._generate_params(extra_params), ) case RequestMethod.GET | _: diff --git a/src/knuckles/_subsonic.py b/src/knuckles/_subsonic.py index 8dc7ec3..c99b76f 100644 --- a/src/knuckles/_subsonic.py +++ b/src/knuckles/_subsonic.py @@ -60,7 +60,7 @@ def __init__( client: str, use_https: bool = True, use_token: bool = True, - request_method: RequestMethod = RequestMethod.POST, + request_method: RequestMethod = RequestMethod.GET, ) -> None: """Construction method of the Subsonic object used to interact with the OpenSubsonic REST API. diff --git a/src/knuckles/models/_playlist.py b/src/knuckles/models/_playlist.py index 2b93328..3d7995b 100644 --- a/src/knuckles/models/_playlist.py +++ b/src/knuckles/models/_playlist.py @@ -116,7 +116,7 @@ def update(self) -> Self: """ self._subsonic.playlists.update_playlist( - self.id, self.name, self.comment, self.public + self.id, comment=self.comment, public=self.public ) return self diff --git a/tests/api/test_media_retrieval.py b/tests/api/test_media_retrieval.py index 1eccdaf..92f5957 100644 --- a/tests/api/test_media_retrieval.py +++ b/tests/api/test_media_retrieval.py @@ -2,10 +2,11 @@ from typing import Any from urllib import parse +import knuckles import pytest import responses from _pytest.fixtures import FixtureRequest -from knuckles import Subsonic, SubtitlesFileFormat +from knuckles import Subsonic from responses import Response from tests.conftest import AddResponses @@ -165,7 +166,14 @@ def test_get_captions_without_a_given_filename( add_responses(get_mock) - download_path = subsonic.media_retrieval.get_captions(video["id"], tmp_path) + if metadata == "vtt_metadata": + subtitle_format = knuckles.SubtitlesFileFormat.VTT + else: + subtitle_format = knuckles.SubtitlesFileFormat.SRT + + download_path = subsonic.media_retrieval.get_captions( + video["id"], tmp_path, subtitle_format + ) # Check if the file data has been altered with open(tmp_path / get_metadata.default_filename, "r") as file: @@ -176,10 +184,10 @@ def test_get_captions_without_a_given_filename( @responses.activate @pytest.mark.parametrize( - "mock, metadata, file_format", + "mock, metadata", [ - ("mock_get_captions_prefer_vtt", "vtt_metadata", SubtitlesFileFormat.VTT), - ("mock_get_captions_prefer_srt", "srt_metadata", SubtitlesFileFormat.SRT), + ("mock_get_captions_prefer_vtt", "vtt_metadata"), + ("mock_get_captions_prefer_srt", "srt_metadata"), ], ) def test_get_captions_with_a_preferred_file_format( @@ -191,7 +199,6 @@ def test_get_captions_with_a_preferred_file_format( placeholder_data: str, video: dict[str, Any], metadata: str, - file_format: SubtitlesFileFormat, ) -> None: # Retrieve the mocks dynamically as their tests are equal get_mock: list[Response] = request.getfixturevalue(mock) @@ -199,8 +206,13 @@ def test_get_captions_with_a_preferred_file_format( add_responses(get_mock) + if metadata == "vtt_metadata": + subtitle_format = knuckles.SubtitlesFileFormat.VTT + else: + subtitle_format = knuckles.SubtitlesFileFormat.SRT + download_path = subsonic.media_retrieval.get_captions( - video["id"], tmp_path, file_format + video["id"], tmp_path, subtitle_format ) # Check if the file data has been altered diff --git a/tests/conftest.py b/tests/conftest.py index 284376f..b1b3fca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,4 @@ -import json +import urllib.parse from typing import Any, Callable, Protocol import knuckles @@ -100,50 +100,42 @@ def __call__( def match_json( - mocked_data: dict[str, Any], + mocked_params: dict[str, Any], ) -> Callable[[requests.PreparedRequest], tuple[bool, str]]: def inner( response: requests.PreparedRequest, ) -> tuple[bool, str]: - print(response.body) - print(mocked_data) - if not isinstance(response.body, str): return ( False, "The request body wasn't a string", ) - for key, value in json.loads(response.body).items(): - print(key, value) + decoded_post_body = urllib.parse.parse_qs(response.body) + + if "t" in decoded_post_body: + del decoded_post_body["t"] - if key not in mocked_data: + if "s" in decoded_post_body: + del decoded_post_body["s"] + + for key, value in decoded_post_body.items(): + if not isinstance(value, list): continue - # When the passed value is a list different checks should be made, - # this happens when the same parameters is in the request multiple times, - # this happens for example when using - # the parameter "songId" in the endpoint "createPlaylist". - if ( - isinstance(value, list) - and isinstance(mocked_data[key], str) - and ( - mocked_data[key] in value - # The values inside the list "value" can sometimes be integers, - # so a extra check is needed for them. - or ( - str.isdigit(mocked_data[key]) and int(mocked_data[key]) in value - ) - ) - ): + if len(value) != 1: continue - if str(value) != str(mocked_data[key]): - return ( - False, - f"The value '{value}' in the key '{key}' is" - + f"not equal to '{mocked_data[key]}'", - ) + decoded_post_body[key] = value[0] + + print(f"{mocked_params=}") + print(f"{decoded_post_body=}") + + if mocked_params != decoded_post_body: + return False, ( + "Mismatch data between the request POST body and " + + "the mocked parameters" + ) return True, "" @@ -168,6 +160,8 @@ def inner( if extra_params is not None: mocked_params.update(extra_params) + print("CC", extra_params) + mocked_data = {"subsonic-response": {**subsonic_response}} if extra_data is not None: mocked_data["subsonic-response"].update(extra_data) diff --git a/tests/mocks/jukebox_control.py b/tests/mocks/jukebox_control.py index ae6fe1d..101bb00 100644 --- a/tests/mocks/jukebox_control.py +++ b/tests/mocks/jukebox_control.py @@ -68,9 +68,10 @@ def mock_jukebox_control_status( @pytest.fixture def mock_jukebox_control_set( + song: dict[str, Any], jukebox_status_generator: JukeboxStatusGenerator, ) -> list[Response]: - return jukebox_status_generator("set") + return jukebox_status_generator("set", {"id": song["id"]}) @pytest.fixture @@ -91,7 +92,7 @@ def mock_jukebox_control_stop( def mock_jukebox_control_skip_without_offset( jukebox_status_generator: JukeboxStatusGenerator, ) -> list[Response]: - return jukebox_status_generator("skip", {"index": 0}) + return jukebox_status_generator("skip", {"index": 0, "offset": 0}) @pytest.fixture diff --git a/tests/mocks/lists.py b/tests/mocks/lists.py index 6b8c815..839709c 100644 --- a/tests/mocks/lists.py +++ b/tests/mocks/lists.py @@ -457,6 +457,7 @@ def mock_get_random_songs( "getRandomSongs", { "genre": genre["value"], + "size": num_of_songs, "fromYear": from_year, "toYear": to_year, "musicFolderId": music_folders[0]["id"], diff --git a/tests/mocks/media_annotation.py b/tests/mocks/media_annotation.py index 01ca5ec..de44edf 100644 --- a/tests/mocks/media_annotation.py +++ b/tests/mocks/media_annotation.py @@ -49,8 +49,10 @@ def mock_unstar_artist( @pytest.fixture -def mock_set_rating_zero(mock_generator: MockGenerator) -> list[Response]: - return mock_generator("setRating", {"rating": 0}) +def mock_set_rating_zero( + song: dict[str, Any], mock_generator: MockGenerator +) -> list[Response]: + return mock_generator("setRating", {"id": song["id"], "rating": 0}) @pytest.fixture diff --git a/tests/mocks/media_retrieval.py b/tests/mocks/media_retrieval.py index 7225015..a1b3f55 100644 --- a/tests/mocks/media_retrieval.py +++ b/tests/mocks/media_retrieval.py @@ -91,7 +91,7 @@ def mock_get_captions_vtt( ) -> list[Response]: return mock_download_file_generator( "getCaptions", - {"id": video["id"]}, + {"id": video["id"], "format": "vtt"}, vtt_metadata.content_type, ) @@ -123,7 +123,7 @@ def mock_get_captions_srt( ) -> list[Response]: return mock_download_file_generator( "getCaptions", - {"id": video["id"]}, + {"id": video["id"], "format": "srt"}, srt_metadata.content_type, ) diff --git a/tests/mocks/searching.py b/tests/mocks/searching.py index fdd3b8b..991f466 100644 --- a/tests/mocks/searching.py +++ b/tests/mocks/searching.py @@ -14,7 +14,16 @@ def mock_search_song_non_id3( ) -> list[Response]: return mock_generator( "search2", - {"songCount": 1, "songOffset": 0}, + { + "query": song["title"], + "songCount": 1, + "songOffset": 0, + "albumCount": 0, + "albumOffset": 0, + "artistCount": 0, + "artistOffset": 0, + "musicFolderId": 1, + }, {"musicFolderId": music_folders[0]["id"], "searchResult2": {"song": [song]}}, ) @@ -27,7 +36,16 @@ def mock_search_album_non_id3( ) -> list[Response]: return mock_generator( "search2", - {"albumCount": 1, "albumOffset": 0}, + { + "query": album["title"], + "songCount": 0, + "songOffset": 0, + "albumCount": 1, + "albumOffset": 0, + "artistCount": 0, + "artistOffset": 0, + "musicFolderId": 1, + }, {"musicFolderId": music_folders[0]["id"], "searchResult2": {"album": [album]}}, ) @@ -40,7 +58,16 @@ def mock_search_artist_non_id3( ) -> list[Response]: return mock_generator( "search2", - {"artistCount": 1, "artistOffset": 0}, + { + "query": artist["name"], + "songCount": 0, + "songOffset": 0, + "albumCount": 0, + "albumOffset": 0, + "artistCount": 1, + "artistOffset": 0, + "musicFolderId": 1, + }, { "musicFolderId": music_folders[0]["id"], "searchResult2": {"artist": [artist]}, @@ -56,7 +83,15 @@ def mock_search_song( ) -> list[Response]: return mock_generator( "search3", - {"songCount": 1, "songOffset": 0}, + { + "query": song["title"], + "songCount": 1, + "songOffset": 0, + "albumCount": 0, + "albumOffset": 0, + "artistCount": 0, + "artistOffset": 0, + }, {"musicFolderId": music_folders[0]["id"], "searchResult3": {"song": [song]}}, ) @@ -69,7 +104,15 @@ def mock_search_album( ) -> list[Response]: return mock_generator( "search3", - {"albumCount": 1, "albumOffset": 0}, + { + "query": album["title"], + "songCount": 0, + "songOffset": 0, + "albumCount": 1, + "albumOffset": 0, + "artistCount": 0, + "artistOffset": 0, + }, {"musicFolderId": music_folders[0]["id"], "searchResult3": {"album": [album]}}, ) @@ -82,7 +125,15 @@ def mock_search_artist( ) -> list[Response]: return mock_generator( "search3", - {"artistCount": 1, "artistOffset": 0}, + { + "query": artist["name"], + "songCount": 0, + "songOffset": 0, + "albumCount": 0, + "albumOffset": 0, + "artistCount": 1, + "artistOffset": 0, + }, { "musicFolderId": music_folders[0]["id"], "searchResult3": {"artist": [artist]}, diff --git a/tests/models/test_jukebox.py b/tests/models/test_jukebox.py index 9bd1c3f..7e5dff5 100644 --- a/tests/models/test_jukebox.py +++ b/tests/models/test_jukebox.py @@ -81,11 +81,11 @@ def test_jukebox_skip_with_offset( add_responses: AddResponses, subsonic: Subsonic, mock_jukebox_control_status: list[Response], - mock_jukebox_control_skip_without_offset: list[Response], + mock_jukebox_control_skip_with_offset: list[Response], offset_time: int, ) -> None: add_responses(mock_jukebox_control_status) - add_responses(mock_jukebox_control_skip_without_offset) + add_responses(mock_jukebox_control_skip_with_offset) response: Jukebox = subsonic.jukebox.status() response = response.skip(0, offset_time)