Skip to content

Commit

Permalink
Merge pull request #27 from kutu-dev/feat/searching
Browse files Browse the repository at this point in the history
Feat/searching
  • Loading branch information
kutu-dev authored Sep 15, 2023
2 parents 0e45ff0 + 60ca427 commit a27dae5
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 1 deletion.
99 changes: 99 additions & 0 deletions src/knuckles/searching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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.
<https://opensubsonic.netlify.app/categories/searching/>
"""

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,
)
3 changes: 2 additions & 1 deletion src/knuckles/subsonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions tests/api/test_searching.py
Original file line number Diff line number Diff line change
@@ -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"]
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"tests.mocks.podcast",
"tests.mocks.internet_radio",
"tests.mocks.bookmarks",
"tests.mocks.searching",
]


Expand Down
44 changes: 44 additions & 0 deletions tests/mocks/searching.py
Original file line number Diff line number Diff line change
@@ -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]},
},
)

0 comments on commit a27dae5

Please sign in to comment.