Skip to content

Commit

Permalink
Merge pull request #26 from kutu-dev/feat/play-queue
Browse files Browse the repository at this point in the history
Feat/play queue
  • Loading branch information
kutu-dev authored Aug 12, 2023
2 parents 40dd1c9 + 9705e35 commit 0e45ff0
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 9 deletions.
49 changes: 47 additions & 2 deletions src/knuckles/bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from .api import Api
from .models.bookmark import Bookmark
from .models.play_queue import PlayQueue

if TYPE_CHECKING:
from .subsonic import Subsonic
Expand Down Expand Up @@ -65,15 +66,15 @@ def create_bookmark(
"createBookmark", {"id": id, "position": position, "comment": comment}
)

# Fake the structure given by the songs in the API.
# Fake the song structure given by in the API.
return Bookmark(self.subsonic, {"id": id}, position=position, comment=comment)

def update_bookmark(
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.
more self-descriptive code.
:param id: The ID of the song of the bookmark.
:type id: str
Expand All @@ -99,3 +100,47 @@ def delete_bookmark(self, id: str) -> "Subsonic":
self.api.request("deleteBookmark", {"id": id})

return self.subsonic

def get_play_queue(self) -> PlayQueue:
"""Calls the "getPlayQueue" endpoint of the API.
:return: The play queue of the authenticated user.
:rtype: PlayQueue
"""

response = self.api.request("getPlayQueue")["playQueue"]

return PlayQueue(self.subsonic, **response)

def save_play_queue(
self,
song_ids: list[str],
current_song_id: str | None = None,
position: int | None = None,
) -> PlayQueue:
"""Calls the "savePlayQueue" endpoint of the API.
:param song_ids: A list with all the IDs of the songs to add.
:type song_ids: list[str]
:param current_song_id: The ID of the current song in the queue,
defaults to None.
:type current_song_id: str | None, optional
:param position: The position in seconds of the current song,
defaults to None.
:type position: int | None, optional
:return: The new saved play queue.
:rtype: PlayQueue
"""

self.api.request(
"savePlayQueue",
{"id": song_ids, "current": current_song_id, "position": position},
)

# TODO This approach is expensive, a better one is preferred
# Fake the song structure given by in the API.
songs = []
for song_id in song_ids:
songs.append({"id": song_id})

return PlayQueue(self.subsonic, songs, current_song_id, position)
66 changes: 66 additions & 0 deletions src/knuckles/models/play_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import TYPE_CHECKING, Any, Self

from dateutil import parser

from .song import Song
from .user import User

if TYPE_CHECKING:
from ..subsonic import Subsonic


class PlayQueue:
"""Representation of all the data related to a play queue in Subsonic."""

def __init__(
self,
subsonic: "Subsonic",
entry: list[dict[str, Any]],
current: str | None = None,
position: int | None = None,
username: str | None = None,
changed: str | None = None,
changedBy: str | None = None,
) -> None:
self.__subsonic = subsonic
self.current = Song(self.__subsonic, current) if current else None
self.position = position
self.user = User(username) if username else None
self.changed = parser.parse(changed) if changed else None
self.changed_by = changedBy
self.songs = (
[Song(self.__subsonic, **song) for song in entry] if entry else None
)

def generate(self) -> "PlayQueue":
"""Return a new play queue with all the data updated from the API,
using the endpoint that return the most information possible.
Useful for making copies with updated data or updating the object itself
with immutability, e.g., foo = foo.generate().
:return: A new share object with all the data updated.
:rtype: PlayQueue
"""

get_play_queue = self.__subsonic.bookmarks.get_play_queue()

return get_play_queue

def save(self) -> Self:
"""Calls the "savePlayQueue" endpoint of the API.
Saves the play queue using the parameters in the object.
:return: _description_
:rtype: Self
"""

# TODO This should raise an exception?
song_ids: list[str] = [song.id for song in self.songs] if self.songs else []

self.__subsonic.bookmarks.save_play_queue(
song_ids, self.current.id if self.current else None, self.position
)

return self
2 changes: 1 addition & 1 deletion src/knuckles/models/playlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def create(self) -> "Playlist":

new_playlist = self.__subsonic.playlists.create_playlist(
# 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.name, # type: ignore[arg-type]
self.comment,
self.public,
Expand Down
40 changes: 39 additions & 1 deletion tests/api/test_bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

import responses
from dateutil import parser
from knuckles import Subsonic
from responses import Response

from knuckles import Subsonic


@responses.activate
def test_get_bookmarks(
Expand Down Expand Up @@ -77,3 +78,40 @@ def test_delete_bookmark(
response = subsonic.bookmarks.delete_bookmark(song["id"])

assert type(response) == Subsonic


@responses.activate
def test_get_play_queue(
subsonic: Subsonic,
mock_get_play_queue: Response,
username: str,
song: dict[str, Any],
play_queue: dict[str, Any],
client: str,
) -> None:
responses.add(mock_get_play_queue)

response = subsonic.bookmarks.get_play_queue()

assert response.user.username == username
assert response.current.id == song["id"]
assert response.changed == parser.parse(play_queue["changed"])
assert response.changed_by == client
assert type(response.songs) is list
assert response.songs[0].id == song["id"]


@responses.activate
def test_save_play_queue(
subsonic: Subsonic,
mock_save_play_queue: Response,
song: dict[str, Any],
play_queue: dict[str, Any],
) -> None:
responses.add(mock_save_play_queue)

response = subsonic.bookmarks.save_play_queue(
[song["id"]], song["id"], play_queue["position"]
)

assert response.current.id == song["id"]
31 changes: 30 additions & 1 deletion tests/mocks/bookmarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,35 @@ def mock_create_bookmark(

@pytest.fixture
def mock_delete_bookmark(
mock_generator: MockGenerator, song: dict[str, Any], bookmark: dict[str, Any]
mock_generator: MockGenerator, song: dict[str, Any]
) -> Response:
return mock_generator("deleteBookmark", {"id": song["id"]})


@pytest.fixture
def play_queue(song: dict[str, Any], username: str, client: str) -> dict[str, Any]:
return {
"entry": [song],
"current": song["id"],
"position": 18283,
"username": username,
"changed": "2023-06-19T08:04:36.238963605Z",
"changedBy": client,
}


@pytest.fixture
def mock_get_play_queue(
mock_generator: MockGenerator, play_queue: dict[str, Any]
) -> Response:
return mock_generator("getPlayQueue", {}, {"playQueue": play_queue})


@pytest.fixture
def mock_save_play_queue(
mock_generator: MockGenerator, song: dict[str, Any], play_queue: dict[str, Any]
) -> Response:
return mock_generator(
"savePlayQueue",
{"id": song["id"], "current": song["id"], "position": play_queue["position"]},
)
6 changes: 3 additions & 3 deletions tests/models/test_song.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

import pytest
import responses
from knuckles import Song, Subsonic
from responses import Response

from knuckles import Song, Subsonic
from tests.conftest import MockGenerator


Expand All @@ -17,7 +17,7 @@ def test_generate(
) -> None:
responses.add(mock_get_song)

requested_song: Song = subsonic.browsing.get_song(song["id"])
requested_song = subsonic.browsing.get_song(song["id"])
requested_song.title = "Foo"
requested_song = requested_song.generate()

Expand All @@ -34,7 +34,7 @@ def test_song_star(
responses.add(mock_get_song)
responses.add(mock_star_song)

requested_song: Song = subsonic.browsing.get_song(song["id"])
requested_song = subsonic.browsing.get_song(song["id"])

assert type(requested_song.star()) is Song

Expand Down
36 changes: 36 additions & 0 deletions tests/models/test_song_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Any

import responses
from knuckles import Subsonic
from knuckles.models.play_queue import PlayQueue
from responses import Response


@responses.activate
def test_generate(
subsonic: Subsonic,
mock_get_play_queue: Response,
username: dict[str, Any],
) -> None:
responses.add(mock_get_play_queue)

requested_queue = subsonic.bookmarks.get_play_queue()
requested_queue.username = "Foo"
requested_queue = requested_queue.generate()

assert requested_queue.user.username == username


@responses.activate
def test_save(
subsonic: Subsonic,
mock_get_play_queue: Response,
mock_save_play_queue: Response,
) -> None:
responses.add(mock_get_play_queue)
responses.add(mock_save_play_queue)

requested_queue = subsonic.bookmarks.get_play_queue()
requested_queue = requested_queue.save()

assert type(requested_queue) is PlayQueue
2 changes: 1 addition & 1 deletion tests/models/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_user_change_password(
response = subsonic.user_management.get_user(user["username"])
response = response.change_password(new_password)

assert type(response) == User
assert type(response) is User


def test_user_without_api_access(user: dict[str, Any]) -> None:
Expand Down

0 comments on commit 0e45ff0

Please sign in to comment.