Skip to content

Commit

Permalink
feat(api): add support for podcast seasons and enhance episode retrieval
Browse files Browse the repository at this point in the history
This commit introduces several new features and improvements to the NRK Podcast API:

 1 Add get_podcast_season method to fetch specific podcast seasons
 2 Enhance get_podcast_episodes to support optional season filtering
 3 Implement new CLI commands for season and episode retrieval
 4 Refactor and expand data models to better represent podcast structures:
    • Introduce enums for podcast types and series types
    • Create separate classes for standard and umbrella podcasts
    • Improve season and episode data structures
  • Loading branch information
bendikrb committed Jul 28, 2024
1 parent ace4928 commit 6ea8d3a
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 25 deletions.
20 changes: 14 additions & 6 deletions nrk_psapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
NrkPsApiError,
NrkPsApiRateLimitError,
)
from .models.catalog import Episode, Podcast, Series
from .models.catalog import Episode, Podcast, Season, Series
from .models.common import IpCheck
from .models.metadata import PodcastMetadata
from .models.pages import (
Expand Down Expand Up @@ -179,12 +179,20 @@ async def get_podcast(self, podcast_id: str) -> Podcast:
return Podcast.from_dict(result)

async def get_podcasts(self, podcast_ids: list[str]) -> list[Podcast]:
results: list[Podcast] = await asyncio.gather(*[self.get_podcast(podcast_id) for podcast_id in podcast_ids])
return results

async def get_podcast_episodes(self, podcast_id: str) -> list[Episode]:
results = await asyncio.gather(*[self.get_podcast(podcast_id) for podcast_id in podcast_ids])
return list(results)

async def get_podcast_season(self, podcast_id: str, season_id: str):
result = await self._request(f"radio/catalog/podcast/{podcast_id}/seasons/{season_id}")
return Season.from_dict(result)

async def get_podcast_episodes(self, podcast_id: str, season_id: str | None = None) -> list[Episode]:
if season_id is not None:
uri = f"radio/catalog/podcast/{podcast_id}/seasons/{season_id}/episodes"
else:
uri = f"radio/catalog/podcast/{podcast_id}/episodes"
result = await self._request_paged_all(
f"radio/catalog/podcast/{podcast_id}/episodes",
uri,
items_key="_embedded.episodes",
)
return [Episode.from_dict(e) for e in result]
Expand Down
25 changes: 25 additions & 0 deletions nrk_psapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ def main_parser() -> argparse.ArgumentParser:
get_podcast_parser.add_argument('podcast_id', type=str, nargs="+", help='The podcast id(s).')
get_podcast_parser.set_defaults(func=get_podcast)

get_podcast_season_parser = subparsers.add_parser('get_podcast_season', description='Get podcast season.')
get_podcast_season_parser.add_argument('podcast_id', type=str, help='The podcast id.')
get_podcast_season_parser.add_argument('season_id', type=str, help='The season id.')
get_podcast_season_parser.set_defaults(func=get_podcast_season)

get_podcast_episodes_parser = subparsers.add_parser('get_podcast_episodes', description='Get podcast episodes.')
get_podcast_episodes_parser.add_argument('podcast_id', type=str, help='The podcast id.')
get_podcast_episodes_parser.add_argument('--season_id', type=str, required=False, help='The season id.')
get_podcast_episodes_parser.set_defaults(func=get_podcast_episodes)

get_episode_parser = subparsers.add_parser('get_episode', description='Get episode.')
get_episode_parser.add_argument('podcast_id', type=str, help='The podcast id.')
get_episode_parser.add_argument('episode_id', type=str, help='The episode id.')
Expand Down Expand Up @@ -60,6 +70,21 @@ async def get_podcast(args):
rprint(podcast)


async def get_podcast_season(args):
"""Get podcast season."""
async with NrkPodcastAPI() as client:
season = await client.get_podcast_season(args.podcast_id, args.season_id)
rprint(season)


async def get_podcast_episodes(args):
"""Get podcast episodes."""
async with NrkPodcastAPI() as client:
episodes = await client.get_podcast_episodes(args.podcast_id, args.season_id)
for episode in episodes:
rprint(episode)


async def get_episode(args):
"""Get episode."""
async with NrkPodcastAPI() as client:
Expand Down
90 changes: 71 additions & 19 deletions nrk_psapi/models/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,41 @@

from dataclasses import dataclass, field
from datetime import datetime, timedelta # noqa: TCH003
from typing import Literal
from enum import Enum

from isodate import duration_isoformat, parse_duration
from mashumaro import field_options
from mashumaro.config import BaseConfig
from mashumaro.types import Discriminator

from .common import BaseDataClassORJSONMixin


def deserialize_embedded(data, ret_type: Literal["episodes", "seasons"]) -> list[Episode | Season]:
"""Deserialize embedded episodes/seasons."""
class PodcastType(str, Enum):
PODCAST = "podcast"
CUSTOM_SEASON = "customSeason"

if ret_type == "seasons" and "seasons" in data:
return [Season.from_dict(d) for d in data["seasons"]]
if ret_type == "episodes" and "episodes" in data:
return [Episode.from_dict(d) for d in data["episodes"]["_embedded"]["episodes"]]
return []
def __str__(self) -> str:
return str(self.value)


class SeriesType(str, Enum):
UMBRELLA = "umbrella"
STANDARD = "standard"
SEQUENTIAL = "sequential"

def __str__(self) -> str:
return str(self.value)


class SeasonDisplayType(str, Enum):
MANUAL = "manual"
NUMBER = "number"
MONTH = "month"
YEAR = "year"

def __str__(self) -> str:
return str(self.value)


@dataclass
Expand Down Expand Up @@ -92,20 +111,40 @@ class Episode(BaseDataClassORJSONMixin):


@dataclass
class Season(BaseDataClassORJSONMixin):
"""Represents a podcast season."""
class SeasonBase(BaseDataClassORJSONMixin):
"""Base class for a podcast season."""

_links: Links
id: str
titles: Titles
has_available_episodes: bool = field(metadata=field_options(alias="hasAvailableEpisodes"))
episode_count: int = field(metadata=field_options(alias="episodeCount"))
episodes: EpisodesResponse
image: list[Image]
square_image: list[Image] = field(metadata=field_options(alias="squareImage"))
backdrop_image: list[Image] = field(metadata=field_options(alias="backdropImage"))


@dataclass
class SeasonEmbedded(SeasonBase):
"""Represents an embedded podcast season."""

id: str


@dataclass
class Season(SeasonBase):
"""Represents a podcast season."""

series_type: SeriesType = field(metadata=field_options(alias="seriesType"))
type: PodcastType = field(metadata=field_options(alias="type"))
name: str
category: Category
episodes: list[Episode] = field(
metadata=field_options(
alias="_embedded",
deserialize=lambda x: [Episode.from_dict(d) for d in x["episodes"]["_embedded"]["episodes"]],
))


@dataclass
class EpisodesResponse(BaseDataClassORJSONMixin):
"""Contains a list of embedded episodes."""
Expand All @@ -116,7 +155,7 @@ class EpisodesResponse(BaseDataClassORJSONMixin):
alias="_embedded",
deserialize=lambda x: x["episodes"],
))
series_type: str | None = field(default=None, metadata=field_options(alias="seriesType"))
series_type: SeriesType | None = field(default=None, metadata=field_options(alias="seriesType"))


@dataclass
Expand All @@ -135,23 +174,36 @@ class Podcast(BaseDataClassORJSONMixin):
"""Represents the main structure of the API response."""

_links: Links
series_type: str = field(metadata=field_options(alias="seriesType"))
type: str = field(metadata=field_options(alias="type"))
season_display_type: str = field(metadata=field_options(alias="seasonDisplayType"))
type: PodcastType = field(metadata=field_options(alias="type"))
season_display_type: SeasonDisplayType = field(metadata=field_options(alias="seasonDisplayType"))
series: PodcastSeries

class Config(BaseConfig):
discriminator = Discriminator(
field="seriesType",
include_subtypes=True,
)


@dataclass
class PodcastStandard(Podcast):
seriesType = "standard" # noqa: N815
episodes: list[Episode] = field(
default_factory=list,
metadata=field_options(
alias="_embedded",
deserialize=lambda x: deserialize_embedded(x, "episodes"),
deserialize=lambda x: [Episode.from_dict(d) for d in x["episodes"]["_embedded"]["episodes"]],
))


@dataclass
class PodcastUmbrella(Podcast):
seriesType = "umbrella" # noqa: N815
seasons: list[Season] = field(
default_factory=list,
metadata=field_options(
alias="_embedded",
deserialize=lambda x: deserialize_embedded(x, "seasons"),
deserialize=lambda x: [SeasonEmbedded.from_dict(d) for d in x["seasons"]],
))


Expand Down Expand Up @@ -252,7 +304,7 @@ class Series(BaseDataClassORJSONMixin):
id: str
series_id: str = field(metadata=field_options(alias="seriesId"))
title: str
type: str
type: SeriesType
images: list[Image]
square_images: list[Image] = field(metadata=field_options(alias="squareImages"))
season_id: str | None = field(default=None, metadata=field_options(alias="seasonId"))

0 comments on commit 6ea8d3a

Please sign in to comment.