Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/searching #27

Merged
merged 2 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]},
},
)