Skip to content

Commit 0595146

Browse files
authored
Add methods for playlists (#489)
1 parent 10efa40 commit 0595146

File tree

9 files changed

+9516
-13
lines changed

9 files changed

+9516
-13
lines changed

src/spotifyaio/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ class Devices(DataClassORJSONMixin):
5252
devices: list[Device]
5353

5454

55+
@dataclass
56+
class AddedBy(DataClassORJSONMixin):
57+
"""Added by model."""
58+
59+
external_urls: dict[str, str]
60+
href: str
61+
user_id: str = field(metadata=field_options(alias="id"))
62+
uri: str
63+
64+
5565
class RepeatMode(StrEnum):
5666
"""Repeat mode."""
5767

@@ -431,9 +441,25 @@ def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
431441
class PlaylistTrack(DataClassORJSONMixin):
432442
"""PlaylistTrack model."""
433443

444+
added_at: datetime
445+
added_by: AddedBy
434446
track: Annotated[Item, Discriminator(field="type", include_subtypes=True)]
435447

436448

449+
@dataclass
450+
class PlaylistTrackResponse(DataClassORJSONMixin):
451+
"""PlaylistTrack response model."""
452+
453+
items: list[PlaylistTrack]
454+
455+
456+
@dataclass
457+
class ModifyPlaylistResponse(DataClassORJSONMixin):
458+
"""Modify playlist response model."""
459+
460+
snapshot_id: str
461+
462+
437463
@dataclass
438464
class PlaylistResponse(DataClassORJSONMixin):
439465
"""Playlist response model."""

src/spotifyaio/spotify.py

Lines changed: 121 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from aiohttp import ClientSession
1111
from aiohttp.hdrs import METH_DELETE, METH_GET, METH_POST, METH_PUT
12+
from mashumaro.codecs.json import JSONDecoder
1213
import orjson
1314
from yarl import URL
1415

@@ -38,13 +39,17 @@
3839
EpisodesResponse,
3940
FeaturedPlaylistResponse,
4041
FollowedArtistResponse,
42+
Image,
43+
ModifyPlaylistResponse,
4144
NewReleasesResponse,
4245
NewReleasesResponseInner,
4346
PlaybackState,
4447
PlayedTrack,
4548
PlayedTrackResponse,
4649
Playlist,
4750
PlaylistResponse,
51+
PlaylistTrack,
52+
PlaylistTrackResponse,
4853
RepeatMode,
4954
SavedAlbum,
5055
SavedAlbumResponse,
@@ -442,10 +447,6 @@ async def are_episodes_saved(self, episode_ids: list[str]) -> dict[str, bool]:
442447
body: list[bool] = orjson.loads(response) # pylint: disable=no-member
443448
return dict(zip(identifiers, body))
444449

445-
# Get genre seeds
446-
447-
# Get available markets
448-
449450
async def get_playback(self) -> PlaybackState | None:
450451
"""Get playback state."""
451452
response = await self._get(
@@ -570,23 +571,126 @@ async def get_playlist(self, playlist_id: str) -> Playlist:
570571
)
571572
return Playlist.from_json(response)
572573

573-
# Update playlist details
574-
575-
# Get a playlist items
576-
577-
# Update a playlist items
574+
async def update_playlist_details(
575+
self,
576+
playlist_id: str,
577+
*,
578+
name: str | None = None,
579+
description: str | None = None,
580+
public: bool | None = None,
581+
collaborative: bool | None = None,
582+
) -> None:
583+
"""Update playlist details."""
584+
identifier = get_identifier(playlist_id)
585+
data: dict[str, Any] = {}
586+
if name:
587+
data["name"] = name
588+
if description:
589+
data["description"] = description
590+
if public is not None:
591+
data["public"] = public
592+
if collaborative is not None:
593+
data["collaborative"] = collaborative
594+
await self._put(f"v1/playlists/{identifier}", data=data)
595+
596+
async def get_playlist_items(self, playlist_id: str) -> list[PlaylistTrack]:
597+
"""Get playlist tracks."""
598+
identifier = get_identifier(playlist_id)
599+
params: dict[str, Any] = {"limit": 48}
600+
response = await self._get(f"v1/playlists/{identifier}/tracks", params=params)
601+
return PlaylistTrackResponse.from_json(response).items
578602

579-
# Remove a playlist items
603+
async def update_playlist_items(
604+
self,
605+
playlist_id: str,
606+
uris: list[str],
607+
*,
608+
range_start: int | None = None,
609+
insert_before: int | None = None,
610+
range_length: int | None = None,
611+
snapshot_id: str | None = None,
612+
) -> str:
613+
"""Update playlist tracks."""
614+
identifier = get_identifier(playlist_id)
615+
data: dict[str, Any] = {"uris": uris}
616+
if range_start is not None:
617+
data["range_start"] = range_start
618+
if insert_before is not None:
619+
data["insert_before"] = insert_before
620+
if range_length is not None:
621+
data["range_length"] = range_length
622+
if snapshot_id:
623+
data["snapshot_id"] = snapshot_id
624+
response = await self._put(f"v1/playlists/{identifier}/tracks", data=data)
625+
return ModifyPlaylistResponse.from_json(response).snapshot_id
626+
627+
async def add_playlist_items(
628+
self,
629+
playlist_id: str,
630+
uris: list[str],
631+
position: int | None = None,
632+
) -> str:
633+
"""Add playlist tracks."""
634+
if len(uris) > 100:
635+
msg = "Maximum of 100 tracks can be added at once"
636+
raise ValueError(msg)
637+
identifier = get_identifier(playlist_id)
638+
data: dict[str, Any] = {"uris": uris}
639+
if position is not None:
640+
data["position"] = position
641+
response = await self._post(f"v1/playlists/{identifier}/tracks", data=data)
642+
return ModifyPlaylistResponse.from_json(response).snapshot_id
643+
644+
async def remove_playlist_items(
645+
self,
646+
playlist_id: str,
647+
uris: list[str],
648+
snapshot_id: str | None = None,
649+
) -> str:
650+
"""Remove playlist tracks."""
651+
if len(uris) > 100:
652+
msg = "Maximum of 100 tracks can be removed at once"
653+
raise ValueError(msg)
654+
identifier = get_identifier(playlist_id)
655+
data: dict[str, Any] = {"tracks": [{"uri": uri} for uri in uris]}
656+
if snapshot_id:
657+
data["snapshot_id"] = snapshot_id
658+
response = await self._delete(f"v1/playlists/{identifier}/tracks", data=data)
659+
return ModifyPlaylistResponse.from_json(response).snapshot_id
580660

581661
async def get_playlists_for_current_user(self) -> list[BasePlaylist]:
582662
"""Get playlists."""
583663
params: dict[str, Any] = {"limit": 48}
584664
response = await self._get("v1/me/playlists", params=params)
585665
return PlaylistResponse.from_json(response).items
586666

587-
# Get users playlists
667+
async def get_playlists_for_user(self, user_id: str) -> list[BasePlaylist]:
668+
"""Get playlists."""
669+
identifier = get_identifier(user_id)
670+
params: dict[str, Any] = {"limit": 48}
671+
response = await self._get(f"v1/user/{identifier}/playlists", params=params)
672+
return PlaylistResponse.from_json(response).items
588673

589-
# Create a playlist
674+
async def create_playlist(
675+
self,
676+
user_id: str,
677+
name: str,
678+
*,
679+
description: str | None = None,
680+
public: bool | None = None,
681+
collaborative: bool | None = None,
682+
) -> Playlist:
683+
"""Create playlist."""
684+
identifier = get_identifier(user_id)
685+
data: dict[str, Any] = {"name": name}
686+
if description:
687+
data["description"] = description
688+
if public is not None:
689+
data["public"] = public
690+
if collaborative is not None:
691+
data["collaborative"] = collaborative
692+
response = await self._post(f"v1/users/{identifier}/playlists", data=data)
693+
return Playlist.from_json(response)
590694

591695
async def get_featured_playlists(self) -> list[BasePlaylist]:
592696
"""Get featured playlists."""
@@ -603,7 +707,11 @@ async def get_category_playlists(self, category_id: str) -> list[BasePlaylist]:
603707
)
604708
return CategoryPlaylistResponse.from_json(response).playlists.items
605709

606-
# Get playlist cover image
710+
async def get_playlist_cover_image(self, playlist_id: str) -> list[Image]:
711+
"""Get playlist cover image."""
712+
identifier = get_identifier(playlist_id)
713+
response = await self._get(f"v1/playlists/{identifier}/images")
714+
return JSONDecoder(list[Image]).decode(response)
607715

608716
# Upload a custom playlist cover image
609717

0 commit comments

Comments
 (0)