Skip to content

Commit dbea1c8

Browse files
authored
Add methods for following (#493)
1 parent 7707886 commit dbea1c8

File tree

5 files changed

+260
-7
lines changed

5 files changed

+260
-7
lines changed

src/spotifyaio/models.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -877,3 +877,10 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
877877
if item is not None
878878
],
879879
}
880+
881+
882+
class FollowType(StrEnum):
883+
"""Follow type."""
884+
885+
ARTIST = "artist"
886+
USER = "user"

src/spotifyaio/spotify.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
EpisodesResponse,
4040
FeaturedPlaylistResponse,
4141
FollowedArtistResponse,
42+
FollowType,
4243
Image,
4344
ModifyPlaylistResponse,
4445
NewReleasesResponse,
@@ -858,23 +859,68 @@ async def get_user(self, user_id: str) -> BaseUserProfile:
858859
response = await self._get(f"v1/users/{user_id}")
859860
return BaseUserProfile.from_json(response)
860861

861-
# Follow a playlist
862+
async def follow_playlist(self, playlist_id: str) -> None:
863+
"""Follow a playlist."""
864+
identifier = get_identifier(playlist_id)
865+
await self._put(f"v1/playlists/{identifier}/followers")
862866

863-
# Unfollow a playlist
867+
async def unfollow_playlist(self, playlist_id: str) -> None:
868+
"""Unfollow a playlist."""
869+
identifier = get_identifier(playlist_id)
870+
await self._delete(f"v1/playlists/{identifier}/followers")
864871

865872
async def get_followed_artists(self) -> list[Artist]:
866873
"""Get followed artists."""
867874
params: dict[str, Any] = {"limit": 48, "type": "artist"}
868875
response = await self._get("v1/me/following", params=params)
869876
return FollowedArtistResponse.from_json(response).artists.items
870877

871-
# Follow an artist or user
878+
async def follow_account(self, follow_type: FollowType, ids: list[str]) -> None:
879+
"""Follow an artist or user."""
880+
if not ids:
881+
return
882+
if len(ids) > 50:
883+
msg = "Maximum of 50 accounts can be followed at once"
884+
raise ValueError(msg)
885+
params: dict[str, Any] = {
886+
"type": follow_type,
887+
"ids": ",".join([get_identifier(i) for i in ids]),
888+
}
889+
await self._put("v1/me/following", params=params)
872890

873-
# Unfollow an artist or user
891+
async def unfollow_account(self, follow_type: FollowType, ids: list[str]) -> None:
892+
"""Unfollow an artist or user."""
893+
if not ids:
894+
return
895+
if len(ids) > 50:
896+
msg = "Maximum of 50 accounts can be unfollowed at once"
897+
raise ValueError(msg)
898+
params: dict[str, Any] = {
899+
"type": follow_type,
900+
"ids": ",".join([get_identifier(i) for i in ids]),
901+
}
902+
await self._delete("v1/me/following", params=params)
874903

875-
# Check if a user is following an artist or user
904+
async def are_accounts_followed(
905+
self, follow_type: FollowType, ids: list[str]
906+
) -> dict[str, bool]:
907+
"""Check if artists or users are followed."""
908+
if not ids:
909+
return {}
910+
if len(ids) > 50:
911+
msg = "Maximum of 50 accounts can be checked at once"
912+
raise ValueError(msg)
913+
identifiers = [get_identifier(i) for i in ids]
914+
params: dict[str, Any] = {"type": follow_type, "ids": ",".join(identifiers)}
915+
response = await self._get("v1/me/following/contains", params=params)
916+
body: list[bool] = orjson.loads(response) # pylint: disable=no-member
917+
return dict(zip(identifiers, body))
876918

877-
# Check if a user is following a playlist
919+
async def is_following_playlist(self, playlist_id: str) -> bool:
920+
"""Check if playlist is followed."""
921+
identifier = get_identifier(playlist_id)
922+
response = await self._get(f"v1/playlists/{identifier}/followers/contains")
923+
return bool(orjson.loads(response)[0]) # pylint: disable=no-member
878924

879925
async def close(self) -> None:
880926
"""Close open client session."""

tests/__snapshots__/test_spotify.ambr

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
# serializer version: 1
2+
# name: test_are_accounts_followed
3+
dict({
4+
'spotify': True,
5+
'spotifyartists': False,
6+
})
7+
# ---
28
# name: test_check_saved_audiobooks
39
dict({
410
'18yVqkdbdRvS24c0Ilj2ci': False,
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
true,
3+
false
4+
]

tests/test_spotify.py

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
SpotifyConnectionError,
1818
SpotifyNotFoundError,
1919
)
20-
from spotifyaio.models import SearchType
20+
from spotifyaio.models import FollowType, SearchType
2121

2222
from . import load_fixture
2323
from .const import HEADERS, SPOTIFY_URL
@@ -2487,3 +2487,193 @@ async def test_check_too_many_saved_tracks(
24872487
with pytest.raises(ValueError, match="Maximum of 50 tracks can be checked at once"):
24882488
await authenticated_client.are_tracks_saved(["abc"] * 51)
24892489
responses.assert_not_called() # type: ignore[no-untyped-call]
2490+
2491+
2492+
async def test_follow_playlist(
2493+
responses: aioresponses,
2494+
authenticated_client: SpotifyClient,
2495+
) -> None:
2496+
"""Test following a playlist."""
2497+
responses.put(
2498+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers",
2499+
status=200,
2500+
body="",
2501+
)
2502+
await authenticated_client.follow_playlist("37i9dQZF1DXcBWIGoYBM5M")
2503+
responses.assert_called_once_with(
2504+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers",
2505+
METH_PUT,
2506+
headers=HEADERS,
2507+
params=None,
2508+
json=None,
2509+
)
2510+
2511+
2512+
async def test_unfollow_playlist(
2513+
responses: aioresponses,
2514+
authenticated_client: SpotifyClient,
2515+
) -> None:
2516+
"""Test unfollowing a playlist."""
2517+
responses.delete(
2518+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers",
2519+
status=200,
2520+
body="",
2521+
)
2522+
await authenticated_client.unfollow_playlist("37i9dQZF1DXcBWIGoYBM5M")
2523+
responses.assert_called_once_with(
2524+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers",
2525+
METH_DELETE,
2526+
headers=HEADERS,
2527+
params=None,
2528+
json=None,
2529+
)
2530+
2531+
2532+
async def test_follow_account(
2533+
responses: aioresponses,
2534+
authenticated_client: SpotifyClient,
2535+
) -> None:
2536+
"""Test following an account."""
2537+
responses.put(
2538+
f"{SPOTIFY_URL}/v1/me/following?ids=spotify&type=user",
2539+
status=200,
2540+
body="",
2541+
)
2542+
await authenticated_client.follow_account(FollowType.USER, ["spotify"])
2543+
responses.assert_called_once_with(
2544+
f"{SPOTIFY_URL}/v1/me/following",
2545+
METH_PUT,
2546+
headers=HEADERS,
2547+
params={"type": "user", "ids": "spotify"},
2548+
json=None,
2549+
)
2550+
2551+
2552+
async def test_follow_no_account(
2553+
responses: aioresponses,
2554+
authenticated_client: SpotifyClient,
2555+
) -> None:
2556+
"""Test following no account."""
2557+
await authenticated_client.follow_account(FollowType.USER, [])
2558+
responses.assert_not_called() # type: ignore[no-untyped-call]
2559+
2560+
2561+
async def test_follow_too_many_accounts(
2562+
responses: aioresponses,
2563+
authenticated_client: SpotifyClient,
2564+
) -> None:
2565+
"""Test following too many accounts."""
2566+
with pytest.raises(
2567+
ValueError, match="Maximum of 50 accounts can be followed at once"
2568+
):
2569+
await authenticated_client.follow_account(FollowType.USER, ["abc"] * 51)
2570+
responses.assert_not_called() # type: ignore[no-untyped-call]
2571+
2572+
2573+
async def test_unfollow_account(
2574+
responses: aioresponses,
2575+
authenticated_client: SpotifyClient,
2576+
) -> None:
2577+
"""Test unfollowing an account."""
2578+
responses.delete(
2579+
f"{SPOTIFY_URL}/v1/me/following?ids=spotify&type=user",
2580+
status=200,
2581+
body="",
2582+
)
2583+
await authenticated_client.unfollow_account(FollowType.USER, ["spotify"])
2584+
responses.assert_called_once_with(
2585+
f"{SPOTIFY_URL}/v1/me/following",
2586+
METH_DELETE,
2587+
headers=HEADERS,
2588+
params={"type": "user", "ids": "spotify"},
2589+
json=None,
2590+
)
2591+
2592+
2593+
async def test_unfollow_no_account(
2594+
responses: aioresponses,
2595+
authenticated_client: SpotifyClient,
2596+
) -> None:
2597+
"""Test unfollowing no account."""
2598+
await authenticated_client.unfollow_account(FollowType.USER, [])
2599+
responses.assert_not_called() # type: ignore[no-untyped-call]
2600+
2601+
2602+
async def test_unfollow_too_many_accounts(
2603+
responses: aioresponses,
2604+
authenticated_client: SpotifyClient,
2605+
) -> None:
2606+
"""Test unfollowing too many accounts."""
2607+
with pytest.raises(
2608+
ValueError, match="Maximum of 50 accounts can be unfollowed at once"
2609+
):
2610+
await authenticated_client.unfollow_account(FollowType.USER, ["abc"] * 51)
2611+
responses.assert_not_called() # type: ignore[no-untyped-call]
2612+
2613+
2614+
async def test_are_accounts_followed(
2615+
responses: aioresponses,
2616+
snapshot: SnapshotAssertion,
2617+
authenticated_client: SpotifyClient,
2618+
) -> None:
2619+
"""Test checking if accounts are followed."""
2620+
responses.get(
2621+
f"{SPOTIFY_URL}/v1/me/following/contains?type=user&ids=spotify%2Cspotifyartists",
2622+
status=200,
2623+
body=load_fixture("accounts_followed.json"),
2624+
)
2625+
response = await authenticated_client.are_accounts_followed(
2626+
FollowType.USER, ["spotify", "spotifyartists"]
2627+
)
2628+
assert response == snapshot
2629+
responses.assert_called_once_with(
2630+
f"{SPOTIFY_URL}/v1/me/following/contains",
2631+
METH_GET,
2632+
headers=HEADERS,
2633+
params={"type": "user", "ids": "spotify,spotifyartists"},
2634+
json=None,
2635+
)
2636+
2637+
2638+
async def test_are_no_accounts_followed(
2639+
responses: aioresponses,
2640+
authenticated_client: SpotifyClient,
2641+
) -> None:
2642+
"""Test checking if no accounts are followed."""
2643+
assert await authenticated_client.are_accounts_followed(FollowType.USER, []) == {}
2644+
responses.assert_not_called() # type: ignore[no-untyped-call]
2645+
2646+
2647+
async def test_are_too_many_accounts_followed(
2648+
responses: aioresponses,
2649+
authenticated_client: SpotifyClient,
2650+
) -> None:
2651+
"""Test checking if too many accounts are followed."""
2652+
with pytest.raises(
2653+
ValueError, match="Maximum of 50 accounts can be checked at once"
2654+
):
2655+
await authenticated_client.are_accounts_followed(FollowType.USER, ["abc"] * 51)
2656+
responses.assert_not_called() # type: ignore[no-untyped-call]
2657+
2658+
2659+
async def test_is_following_playlist(
2660+
responses: aioresponses,
2661+
authenticated_client: SpotifyClient,
2662+
) -> None:
2663+
"""Test checking if a playlist is followed."""
2664+
responses.get(
2665+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers/contains",
2666+
status=200,
2667+
body="[true]",
2668+
)
2669+
response = await authenticated_client.is_following_playlist(
2670+
"37i9dQZF1DXcBWIGoYBM5M"
2671+
)
2672+
assert response is True
2673+
responses.assert_called_once_with(
2674+
f"{SPOTIFY_URL}/v1/playlists/37i9dQZF1DXcBWIGoYBM5M/followers/contains",
2675+
METH_GET,
2676+
headers=HEADERS,
2677+
params=None,
2678+
json=None,
2679+
)

0 commit comments

Comments
 (0)