From b3a5e0f433aefc55c2acfa2d2d36b399bb31c757 Mon Sep 17 00:00:00 2001 From: Kutu Date: Fri, 15 Sep 2023 22:32:07 +0200 Subject: [PATCH 1/2] Fix uncommited file --- src/knuckles/searching.py | 101 ++++++++++++++++++++++++++++++++++++ src/knuckles/subsonic.py | 3 +- tests/api/test_searching.py | 62 ++++++++++++++++++++++ tests/conftest.py | 1 + tests/mocks/searching.py | 44 ++++++++++++++++ 5 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 src/knuckles/searching.py create mode 100644 tests/api/test_searching.py create mode 100644 tests/mocks/searching.py diff --git a/src/knuckles/searching.py b/src/knuckles/searching.py new file mode 100644 index 0000000..4a455cf --- /dev/null +++ b/src/knuckles/searching.py @@ -0,0 +1,101 @@ +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from .api import Api +from .models.album import Album +from .models.artist import Artist +from .models.song import Song + +if TYPE_CHECKING: + from .subsonic import Subsonic + + +# Use a plain dataclass as it only stores lists +# and doesn't have any sort of method to be generated + + +@dataclass +class SearchResult: + songs: list[Song] | None = None + albums: list[Album] | None = None + artists: list[Artist] | None = None + + +class Searching: + """Class that contains all the methods needed to interact + with the searching calls and actions in the Subsonic API. + + """ + + def __init__(self, api: Api, subsonic: "Subsonic") -> None: + self.api = api + + # Only to pass it to the models + self.subsonic = subsonic + + def search( + self, + query: str = "", + song_count: int | None = None, + song_offset: int | None = None, + album_count: int | None = None, + album_offset: int | None = None, + artist_count: int | None = None, + artist_offset: int | None = None, + music_folder_id: str | None = None, + ) -> SearchResult: + """Calls the "search3" endpoint of the API. + + :param query: The query + :type query: str + :param song_count: Maximum number of songs to return + :type song_count: int + :param song_offset: Offset the results for songs + :type song_offset: int + :param album_count: Maximum number of albums to return + :type album_count: int + :param album_offset: Offset the results for albums + :type album_offset: int + :param artist_count: Maximum number of artists to return + :type artist_count: int + :param artist_offset: Offset the results for artists + :type artist_offset: int + :param music_folder_id: The id of the music folder to search results + :type music_folder_id: str + :return: + :rtype: + """ + response = self.api.request( + "search3", + { + "query": query, + "songCount": song_count, + "songOffset": song_offset, + "albumCount": album_count, + "albumOffset": album_offset, + "artistCount": artist_count, + "artistOffset": artist_offset, + }, + )["searchResult3"] + + search_result_songs = ( + [Song(self.subsonic, **song) for song in response["song"]] + if "song" in response + else None + ) + search_result_albums = ( + [Album(self.subsonic, **album) for album in response["album"]] + if "album" in response + else None + ) + search_result_artists = ( + [Artist(self.subsonic, **artist) for artist in response["artist"]] + if "artist" in response + else None + ) + + return SearchResult( + songs=search_result_songs, + albums=search_result_albums, + artists=search_result_artists, + ) diff --git a/src/knuckles/subsonic.py b/src/knuckles/subsonic.py index c0a142b..ad3ee10 100644 --- a/src/knuckles/subsonic.py +++ b/src/knuckles/subsonic.py @@ -8,6 +8,7 @@ from .media_library_scanning import MediaLibraryScanning from .playlists import Playlists from .podcast import Podcast +from .searching import Searching from .sharing import Sharing from .system import System from .user_management import UserManagement @@ -47,7 +48,7 @@ def __init__( self.system = System(self.api) self.browsing = Browsing(self.api, self) self.lists = None #! ? - self.searching = None #! ? + self.searching = Searching(self.api, self) self.playlists = Playlists(self.api, self) self.media_retrieval = None self.media_annotation = MediaAnnotation(self.api, self) diff --git a/tests/api/test_searching.py b/tests/api/test_searching.py new file mode 100644 index 0000000..686c120 --- /dev/null +++ b/tests/api/test_searching.py @@ -0,0 +1,62 @@ +from typing import Any + +import responses +from knuckles import Subsonic +from responses import Response + + +@responses.activate +def test_search_song( + subsonic: Subsonic, + mock_search_song: Response, + song: dict[str, Any], + music_folders: list[dict[str, Any]], +) -> None: + responses.add(mock_search_song) + + response = subsonic.searching.search( + song["title"], 1, 0, 0, 0, 0, 0, music_folders[0]["id"] + ) + + assert response.songs is not None + assert response.songs[0].title == song["title"] + assert response.albums is None + assert response.artists is None + + +@responses.activate +def test_search_album( + subsonic: Subsonic, + mock_search_album: Response, + album: dict[str, Any], + music_folders: list[dict[str, Any]], +) -> None: + responses.add(mock_search_album) + + response = subsonic.searching.search( + album["title"], 0, 0, 1, 0, 0, 0, music_folders[0]["id"] + ) + + assert response.songs is None + assert response.albums is not None + assert response.albums[0].title == album["title"] + assert response.artists is None + + +@responses.activate +def test_search_artist( + subsonic: Subsonic, + mock_search_artist: Response, + artist: dict[str, Any], + music_folders: list[dict[str, Any]], +) -> None: + responses.add(mock_search_artist) + + response = subsonic.searching.search( + artist["name"], 0, 0, 0, 0, 1, 0, music_folders[0]["id"] + ) + + assert response.songs is None + assert response.albums is None + assert response.artists is not None + assert response.artists[0].name == artist["name"] diff --git a/tests/conftest.py b/tests/conftest.py index 39d35b1..a350c20 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,6 +17,7 @@ "tests.mocks.podcast", "tests.mocks.internet_radio", "tests.mocks.bookmarks", + "tests.mocks.searching", ] diff --git a/tests/mocks/searching.py b/tests/mocks/searching.py new file mode 100644 index 0000000..8cc5d2b --- /dev/null +++ b/tests/mocks/searching.py @@ -0,0 +1,44 @@ +from typing import Any + +import pytest +from responses import Response + +from tests.conftest import MockGenerator + + +@pytest.fixture +def mock_search_song( + mock_generator: MockGenerator, + music_folders: list[dict[str, Any]], + song: dict[str, Any], +) -> Response: + return mock_generator( + "search3", + {"songCount": 1, "songOffset": 0}, + {"musicFolderId": music_folders[0]["id"], "searchResult3": {"song": [song]}}, + ) + + +@pytest.fixture +def mock_search_album( + mock_generator: MockGenerator, music_folders, album: dict[str, Any] +) -> Response: + return mock_generator( + "search3", + {"albumCount": 1, "albumOffset": 0}, + {"musicFolderId": music_folders[0]["id"], "searchResult3": {"album": [album]}}, + ) + + +@pytest.fixture +def mock_search_artist( + mock_generator: MockGenerator, music_folders, artist: dict[str, Any] +) -> Response: + return mock_generator( + "search3", + {"artistCount": 1, "artistOffset": 0}, + { + "musicFolderId": music_folders[0]["id"], + "searchResult3": {"artist": [artist]}, + }, + ) From 60ca427e9e3d694b481dbaeacc6c8b7ff9d4b97e Mon Sep 17 00:00:00 2001 From: Kutu Date: Fri, 15 Sep 2023 22:36:10 +0200 Subject: [PATCH 2/2] Implement and test the search method --- src/knuckles/searching.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/knuckles/searching.py b/src/knuckles/searching.py index 4a455cf..00f162a 100644 --- a/src/knuckles/searching.py +++ b/src/knuckles/searching.py @@ -12,8 +12,6 @@ # Use a plain dataclass as it only stores lists # and doesn't have any sort of method to be generated - - @dataclass class SearchResult: songs: list[Song] | None = None