Skip to content

Commit

Permalink
Add methods for episodes (#488)
Browse files Browse the repository at this point in the history
  • Loading branch information
joostlek authored Jan 25, 2025
1 parent 5bb3a26 commit 10efa40
Show file tree
Hide file tree
Showing 7 changed files with 1,037 additions and 5 deletions.
40 changes: 40 additions & 0 deletions src/spotifyaio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,33 @@ class ShowEpisodesResponse(DataClassORJSONMixin):
items: list[SimplifiedEpisode]


@dataclass
class SavedEpisode(DataClassORJSONMixin):
"""Saved episode model."""

added_at: datetime
episode: Episode


@dataclass
class SavedEpisodeResponse(DataClassORJSONMixin):
"""Saved episodes response model."""

items: list[SavedEpisode]

@classmethod
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
"""Pre deserialize hook."""
return {
**d,
"items": [
item
for item in d["items"]
if item.get("episode", {}).get("id") is not None
],
}


@dataclass
class Episode(SimplifiedEpisode, Item):
"""Episode model."""
Expand All @@ -565,6 +592,19 @@ class Episode(SimplifiedEpisode, Item):
show: SimplifiedShow


@dataclass
class EpisodesResponse(DataClassORJSONMixin):
"""Episodes response model."""

episodes: list[Episode]

@classmethod
def __pre_deserialize__(cls, d: dict[str, Any]) -> dict[str, Any]:
"""Pre deserialize hook."""
items = [item for item in d["episodes"] if item is not None]
return {"episodes": items}


@dataclass
class Show(SimplifiedShow):
"""Show model."""
Expand Down
59 changes: 54 additions & 5 deletions src/spotifyaio/spotify.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Device,
Devices,
Episode,
EpisodesResponse,
FeaturedPlaylistResponse,
FollowedArtistResponse,
NewReleasesResponse,
Expand All @@ -48,6 +49,8 @@
SavedAlbum,
SavedAlbumResponse,
SavedAudiobookResponse,
SavedEpisode,
SavedEpisodeResponse,
SavedShow,
SavedShowResponse,
SavedTrack,
Expand Down Expand Up @@ -383,15 +386,61 @@ async def get_episode(self, episode_id: str) -> Episode:
response = await self._get(f"v1/episodes/{identifier}")
return Episode.from_json(response)

# Get several episodes
async def get_episodes(self, episode_ids: list[str]) -> list[Episode]:
"""Get episodes."""
if not episode_ids:
return []
if len(episode_ids) > 50:
msg = "Maximum of 50 episodes can be requested at once"
raise ValueError(msg)
params: dict[str, Any] = {
"ids": ",".join([get_identifier(i) for i in episode_ids])
}
response = await self._get("v1/episodes", params=params)
return EpisodesResponse.from_json(response).episodes

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

# Save an episode
async def save_episodes(self, episode_ids: list[str]) -> None:
"""Save episodes."""
if not episode_ids:
return
if len(episode_ids) > 50:
msg = "Maximum of 50 episodes can be saved at once"
raise ValueError(msg)
params: dict[str, Any] = {
"ids": ",".join([get_identifier(i) for i in episode_ids])
}
await self._put("v1/me/episodes", params=params)

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

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

# Get genre seeds

Expand Down
139 changes: 139 additions & 0 deletions tests/__snapshots__/test_spotify.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
'7iHfbu1YPACw6oZPAFJtqe': False,
})
# ---
# name: test_check_saved_episodes
dict({
'3o0RYoo5iOMKSmEbunsbvW': True,
'3o0RYoo5iOMKSmEbunsbvX': False,
})
# ---
# name: test_checking_saved_albums
dict({
'1A2GTWGtFfWp7KSQTwWOyo': False,
Expand Down Expand Up @@ -10426,6 +10432,71 @@
'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW',
})
# ---
# name: test_get_episodes
list([
dict({
'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',
'duration_ms': 3690161,
'episode_id': '3o0RYoo5iOMKSmEbunsbvW',
'explicit': False,
'external_urls': dict({
'spotify': 'https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW',
}),
'href': 'https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW',
'images': list([
dict({
'height': 640,
'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a',
'width': 640,
}),
dict({
'height': 300,
'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a',
'width': 300,
}),
dict({
'height': 64,
'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a',
'width': 64,
}),
]),
'name': 'My Squirrel Has Brain Damage - Safety Third 119',
'release_date': '2024-07-26',
'release_date_precision': <ReleaseDatePrecision.DAY: 'day'>,
'show': dict({
'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.',
'external_urls': dict({
'spotify': 'https://open.spotify.com/show/1Y9ExMgMxoBVrgrfU7u0nD',
}),
'href': 'https://api.spotify.com/v1/shows/1Y9ExMgMxoBVrgrfU7u0nD',
'images': list([
dict({
'height': 640,
'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a',
'width': 640,
}),
dict({
'height': 300,
'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a',
'width': 300,
}),
dict({
'height': 64,
'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a',
'width': 64,
}),
]),
'name': 'Safety Third',
'publisher': 'Safety Third ',
'show_id': '1Y9ExMgMxoBVrgrfU7u0nD',
'total_episodes': 122,
'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD',
}),
'type': <ItemType.EPISODE: 'episode'>,
'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW',
}),
])
# ---
# name: test_get_featured_playlists
list([
dict({
Expand Down Expand Up @@ -19783,6 +19854,74 @@
}),
])
# ---
# name: test_get_saved_episodes
list([
dict({
'added_at': datetime.datetime(2021, 4, 1, 23, 21, 46, tzinfo=datetime.timezone.utc),
'episode': dict({
'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",
'duration_ms': 3724303,
'episode_id': '0x25dVaCtjWMmcjDJyuMM5',
'explicit': True,
'external_urls': dict({
'spotify': 'https://open.spotify.com/episode/0x25dVaCtjWMmcjDJyuMM5',
}),
'href': 'https://api.spotify.com/v1/episodes/0x25dVaCtjWMmcjDJyuMM5',
'images': list([
dict({
'height': 640,
'url': 'https://i.scdn.co/image/ab6765630000ba8aee2ef0ad038698401b200131',
'width': 640,
}),
dict({
'height': 300,
'url': 'https://i.scdn.co/image/ab67656300005f1fee2ef0ad038698401b200131',
'width': 300,
}),
dict({
'height': 64,
'url': 'https://i.scdn.co/image/ab6765630000f68dee2ef0ad038698401b200131',
'width': 64,
}),
]),
'name': 'Ep 26. Katherine Parkinson - S11 Ep.3',
'release_date': '2021-04-01',
'release_date_precision': <ReleaseDatePrecision.DAY: 'day'>,
'show': dict({
'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.',
'external_urls': dict({
'spotify': 'https://open.spotify.com/show/4BZc9sOdNilJJ8irsuOzdg',
}),
'href': 'https://api.spotify.com/v1/shows/4BZc9sOdNilJJ8irsuOzdg',
'images': list([
dict({
'height': 640,
'url': 'https://i.scdn.co/image/ab6765630000ba8a940dfd502920d407436baca2',
'width': 640,
}),
dict({
'height': 300,
'url': 'https://i.scdn.co/image/ab67656300005f1f940dfd502920d407436baca2',
'width': 300,
}),
dict({
'height': 64,
'url': 'https://i.scdn.co/image/ab6765630000f68d940dfd502920d407436baca2',
'width': 64,
}),
]),
'name': 'Taskmaster The Podcast',
'publisher': 'Avalon ',
'show_id': '4BZc9sOdNilJJ8irsuOzdg',
'total_episodes': 200,
'uri': 'spotify:show:4BZc9sOdNilJJ8irsuOzdg',
}),
'type': <ItemType.EPISODE: 'episode'>,
'uri': 'spotify:episode:0x25dVaCtjWMmcjDJyuMM5',
}),
}),
])
# ---
# name: test_get_saved_shows
list([
dict({
Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/episode_saved.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
true,
false
]
Loading

0 comments on commit 10efa40

Please sign in to comment.