Skip to content

Commit 10efa40

Browse files
authored
Add methods for episodes (#488)
1 parent 5bb3a26 commit 10efa40

File tree

7 files changed

+1037
-5
lines changed

7 files changed

+1037
-5
lines changed

src/spotifyaio/models.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,33 @@ class ShowEpisodesResponse(DataClassORJSONMixin):
557557
items: list[SimplifiedEpisode]
558558

559559

560+
@dataclass
561+
class SavedEpisode(DataClassORJSONMixin):
562+
"""Saved episode model."""
563+
564+
added_at: datetime
565+
episode: Episode
566+
567+
568+
@dataclass
569+
class SavedEpisodeResponse(DataClassORJSONMixin):
570+
"""Saved episodes response model."""
571+
572+
items: list[SavedEpisode]
573+
574+
@classmethod
575+
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
576+
"""Pre deserialize hook."""
577+
return {
578+
**d,
579+
"items": [
580+
item
581+
for item in d["items"]
582+
if item.get("episode", {}).get("id") is not None
583+
],
584+
}
585+
586+
560587
@dataclass
561588
class Episode(SimplifiedEpisode, Item):
562589
"""Episode model."""
@@ -565,6 +592,19 @@ class Episode(SimplifiedEpisode, Item):
565592
show: SimplifiedShow
566593

567594

595+
@dataclass
596+
class EpisodesResponse(DataClassORJSONMixin):
597+
"""Episodes response model."""
598+
599+
episodes: list[Episode]
600+
601+
@classmethod
602+
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
603+
"""Pre deserialize hook."""
604+
items = [item for item in d["episodes"] if item is not None]
605+
return {"episodes": items}
606+
607+
568608
@dataclass
569609
class Show(SimplifiedShow):
570610
"""Show model."""

src/spotifyaio/spotify.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
Device,
3636
Devices,
3737
Episode,
38+
EpisodesResponse,
3839
FeaturedPlaylistResponse,
3940
FollowedArtistResponse,
4041
NewReleasesResponse,
@@ -48,6 +49,8 @@
4849
SavedAlbum,
4950
SavedAlbumResponse,
5051
SavedAudiobookResponse,
52+
SavedEpisode,
53+
SavedEpisodeResponse,
5154
SavedShow,
5255
SavedShowResponse,
5356
SavedTrack,
@@ -383,15 +386,61 @@ async def get_episode(self, episode_id: str) -> Episode:
383386
response = await self._get(f"v1/episodes/{identifier}")
384387
return Episode.from_json(response)
385388

386-
# Get several episodes
389+
async def get_episodes(self, episode_ids: list[str]) -> list[Episode]:
390+
"""Get episodes."""
391+
if not episode_ids:
392+
return []
393+
if len(episode_ids) > 50:
394+
msg = "Maximum of 50 episodes can be requested at once"
395+
raise ValueError(msg)
396+
params: dict[str, Any] = {
397+
"ids": ",".join([get_identifier(i) for i in episode_ids])
398+
}
399+
response = await self._get("v1/episodes", params=params)
400+
return EpisodesResponse.from_json(response).episodes
387401

388-
# Get saved episodes
402+
async def get_saved_episodes(self) -> list[SavedEpisode]:
403+
"""Get saved episodes."""
404+
params: dict[str, Any] = {"limit": 48}
405+
response = await self._get("v1/me/episodes", params=params)
406+
return SavedEpisodeResponse.from_json(response).items
389407

390-
# Save an episode
408+
async def save_episodes(self, episode_ids: list[str]) -> None:
409+
"""Save episodes."""
410+
if not episode_ids:
411+
return
412+
if len(episode_ids) > 50:
413+
msg = "Maximum of 50 episodes can be saved at once"
414+
raise ValueError(msg)
415+
params: dict[str, Any] = {
416+
"ids": ",".join([get_identifier(i) for i in episode_ids])
417+
}
418+
await self._put("v1/me/episodes", params=params)
391419

392-
# Remove an episode
420+
async def remove_saved_episodes(self, episode_ids: list[str]) -> None:
421+
"""Remove saved episodes."""
422+
if not episode_ids:
423+
return
424+
if len(episode_ids) > 50:
425+
msg = "Maximum of 50 episodes can be removed at once"
426+
raise ValueError(msg)
427+
params: dict[str, Any] = {
428+
"ids": ",".join([get_identifier(i) for i in episode_ids])
429+
}
430+
await self._delete("v1/me/episodes", params=params)
393431

394-
# Check if one or more episodes is already saved
432+
async def are_episodes_saved(self, episode_ids: list[str]) -> dict[str, bool]:
433+
"""Check if episodes are saved."""
434+
if not episode_ids:
435+
return {}
436+
if len(episode_ids) > 50:
437+
msg = "Maximum of 50 episodes can be checked at once"
438+
raise ValueError(msg)
439+
identifiers = [get_identifier(i) for i in episode_ids]
440+
params: dict[str, Any] = {"ids": ",".join(identifiers)}
441+
response = await self._get("v1/me/episodes/contains", params=params)
442+
body: list[bool] = orjson.loads(response) # pylint: disable=no-member
443+
return dict(zip(identifiers, body))
395444

396445
# Get genre seeds
397446

tests/__snapshots__/test_spotify.ambr

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
'7iHfbu1YPACw6oZPAFJtqe': False,
77
})
88
# ---
9+
# name: test_check_saved_episodes
10+
dict({
11+
'3o0RYoo5iOMKSmEbunsbvW': True,
12+
'3o0RYoo5iOMKSmEbunsbvX': False,
13+
})
14+
# ---
915
# name: test_checking_saved_albums
1016
dict({
1117
'1A2GTWGtFfWp7KSQTwWOyo': False,
@@ -10426,6 +10432,71 @@
1042610432
'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW',
1042710433
})
1042810434
# ---
10435+
# name: test_get_episodes
10436+
list([
10437+
dict({
10438+
'description': 'Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy',
10439+
'duration_ms': 3690161,
10440+
'episode_id': '3o0RYoo5iOMKSmEbunsbvW',
10441+
'explicit': False,
10442+
'external_urls': dict({
10443+
'spotify': 'https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW',
10444+
}),
10445+
'href': 'https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW',
10446+
'images': list([
10447+
dict({
10448+
'height': 640,
10449+
'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a',
10450+
'width': 640,
10451+
}),
10452+
dict({
10453+
'height': 300,
10454+
'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a',
10455+
'width': 300,
10456+
}),
10457+
dict({
10458+
'height': 64,
10459+
'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a',
10460+
'width': 64,
10461+
}),
10462+
]),
10463+
'name': 'My Squirrel Has Brain Damage - Safety Third 119',
10464+
'release_date': '2024-07-26',
10465+
'release_date_precision': <ReleaseDatePrecision.DAY: 'day'>,
10466+
'show': dict({
10467+
'description': 'Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube "Scientists". Sometimes we have guests, sometimes it\'s just us, but always: safety is our number three priority.',
10468+
'external_urls': dict({
10469+
'spotify': 'https://open.spotify.com/show/1Y9ExMgMxoBVrgrfU7u0nD',
10470+
}),
10471+
'href': 'https://api.spotify.com/v1/shows/1Y9ExMgMxoBVrgrfU7u0nD',
10472+
'images': list([
10473+
dict({
10474+
'height': 640,
10475+
'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a',
10476+
'width': 640,
10477+
}),
10478+
dict({
10479+
'height': 300,
10480+
'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a',
10481+
'width': 300,
10482+
}),
10483+
dict({
10484+
'height': 64,
10485+
'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a',
10486+
'width': 64,
10487+
}),
10488+
]),
10489+
'name': 'Safety Third',
10490+
'publisher': 'Safety Third ',
10491+
'show_id': '1Y9ExMgMxoBVrgrfU7u0nD',
10492+
'total_episodes': 122,
10493+
'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD',
10494+
}),
10495+
'type': <ItemType.EPISODE: 'episode'>,
10496+
'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW',
10497+
}),
10498+
])
10499+
# ---
1042910500
# name: test_get_featured_playlists
1043010501
list([
1043110502
dict({
@@ -19783,6 +19854,74 @@
1978319854
}),
1978419855
])
1978519856
# ---
19857+
# name: test_get_saved_episodes
19858+
list([
19859+
dict({
19860+
'added_at': datetime.datetime(2021, 4, 1, 23, 21, 46, tzinfo=datetime.timezone.utc),
19861+
'episode': dict({
19862+
'description': "This week on the podcast Ed is joined by actor, writer and Series 10 contestant, Katherine Parkinson. As well as discussing the new line up and this week's tasks they go back to Series 10 and talk about Katherine's time on the show - expect chat about clay masks, marble runs, spiders and THAT fart noise. You can watch Series 11 of Taskmaster each Thursday on Channel 4 at 9pm.Watch Taskmaster Bleeped on All 4Get in touch with Ed and future guests:[email protected]\xa0Visit the Taskmaster Youtube channelwww.youtube.com/taskmaster\xa0For all your Taskmaster goodies visit\xa0www.taskmasterstore.com\xa0\xa0Taskmaster the podcast is produced by Daisy Knight for Avalon Television Ltd",
19863+
'duration_ms': 3724303,
19864+
'episode_id': '0x25dVaCtjWMmcjDJyuMM5',
19865+
'explicit': True,
19866+
'external_urls': dict({
19867+
'spotify': 'https://open.spotify.com/episode/0x25dVaCtjWMmcjDJyuMM5',
19868+
}),
19869+
'href': 'https://api.spotify.com/v1/episodes/0x25dVaCtjWMmcjDJyuMM5',
19870+
'images': list([
19871+
dict({
19872+
'height': 640,
19873+
'url': 'https://i.scdn.co/image/ab6765630000ba8aee2ef0ad038698401b200131',
19874+
'width': 640,
19875+
}),
19876+
dict({
19877+
'height': 300,
19878+
'url': 'https://i.scdn.co/image/ab67656300005f1fee2ef0ad038698401b200131',
19879+
'width': 300,
19880+
}),
19881+
dict({
19882+
'height': 64,
19883+
'url': 'https://i.scdn.co/image/ab6765630000f68dee2ef0ad038698401b200131',
19884+
'width': 64,
19885+
}),
19886+
]),
19887+
'name': 'Ep 26. Katherine Parkinson - S11 Ep.3',
19888+
'release_date': '2021-04-01',
19889+
'release_date_precision': <ReleaseDatePrecision.DAY: 'day'>,
19890+
'show': dict({
19891+
'description': 'This is the official Taskmaster podcast, hosted by former champion and chickpea lover, Ed Gamble. Each week, released straight after the show is broadcast on Channel 4, Ed will be joined by a special guest to dissect and discuss the latest episode. Past contestants, little Alex Horne, and even the Taskmaster himself will feature in this brand-new podcast from the producers of the BAFTA-winning comedy show.',
19892+
'external_urls': dict({
19893+
'spotify': 'https://open.spotify.com/show/4BZc9sOdNilJJ8irsuOzdg',
19894+
}),
19895+
'href': 'https://api.spotify.com/v1/shows/4BZc9sOdNilJJ8irsuOzdg',
19896+
'images': list([
19897+
dict({
19898+
'height': 640,
19899+
'url': 'https://i.scdn.co/image/ab6765630000ba8a940dfd502920d407436baca2',
19900+
'width': 640,
19901+
}),
19902+
dict({
19903+
'height': 300,
19904+
'url': 'https://i.scdn.co/image/ab67656300005f1f940dfd502920d407436baca2',
19905+
'width': 300,
19906+
}),
19907+
dict({
19908+
'height': 64,
19909+
'url': 'https://i.scdn.co/image/ab6765630000f68d940dfd502920d407436baca2',
19910+
'width': 64,
19911+
}),
19912+
]),
19913+
'name': 'Taskmaster The Podcast',
19914+
'publisher': 'Avalon ',
19915+
'show_id': '4BZc9sOdNilJJ8irsuOzdg',
19916+
'total_episodes': 200,
19917+
'uri': 'spotify:show:4BZc9sOdNilJJ8irsuOzdg',
19918+
}),
19919+
'type': <ItemType.EPISODE: 'episode'>,
19920+
'uri': 'spotify:episode:0x25dVaCtjWMmcjDJyuMM5',
19921+
}),
19922+
}),
19923+
])
19924+
# ---
1978619925
# name: test_get_saved_shows
1978719926
list([
1978819927
dict({

tests/fixtures/episode_saved.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[
2+
true,
3+
false
4+
]

0 commit comments

Comments
 (0)