Skip to content

Commit 192bf8e

Browse files
committed
Changes to episode retrieval and model to align with the new mobile API endpoint
1 parent 827cd70 commit 192bf8e

File tree

5 files changed

+76
-37
lines changed

5 files changed

+76
-37
lines changed

podme_api/auth/mobile_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,5 +206,6 @@ async def refresh_token(self, credentials: SchibstedCredentials | None = None):
206206

207207
return self._credentials
208208

209+
@property
209210
def credentials_filename(self):
210211
return "credentials_mobile.json"

podme_api/cli/cli.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import asyncio
55
import contextlib
66
import logging
7+
from typing import AsyncGenerator
78

89
from rich.console import Console
910
from rich.live import Live
@@ -19,7 +20,7 @@
1920
from rich.table import Table
2021

2122
from podme_api.__version__ import __version__
22-
from podme_api.auth import PodMeDefaultAuthClient
23+
from podme_api.auth import PodMeDefaultAuthClient, PodMeMobileAuthClient
2324
from podme_api.auth.models import PodMeUserCredentials
2425
from podme_api.cli.utils import bold_star, is_valid_writable_dir, pretty_dataclass, pretty_dataclass_list
2526
from podme_api.client import PodMeClient
@@ -119,6 +120,11 @@ def _add_default_arguments(parser: argparse.ArgumentParser):
119120
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s v{__version__}")
120121
parser.add_argument("-v", "--verbose", action="count", default=0, help="Logging verbosity level")
121122
parser.add_argument("--debug", action="store_true", help="Enable debug mode")
123+
parser.add_argument(
124+
"--legacy",
125+
action="store_true",
126+
help="Use the old web auth (won't work for much more than retrieving metadata)",
127+
)
122128

123129

124130
def _add_paging_arguments(parser: argparse.ArgumentParser):
@@ -129,7 +135,7 @@ def _add_paging_arguments(parser: argparse.ArgumentParser):
129135
default=20,
130136
help="Number of results to return per page (default=20).",
131137
)
132-
parser.add_argument("--pages", type=int, default=1, help="Maxium number of pages to fetch (default=1)")
138+
parser.add_argument("--pages", type=int, default=1, help="Maximum number of pages to fetch (default=1)")
133139

134140

135141
async def login(args):
@@ -399,14 +405,17 @@ async def search(args) -> None:
399405

400406

401407
@contextlib.asynccontextmanager
402-
async def _get_client(args) -> PodMeClient:
408+
async def _get_client(args) -> AsyncGenerator[PodMeClient, None]:
403409
"""Return PodMeClient based on args."""
404410
if hasattr(args, "username") and hasattr(args, "password"):
405411
user_creds = PodMeUserCredentials(args.username, args.password)
406412
else:
407413
user_creds = None
408-
auth_client = PodMeDefaultAuthClient(user_credentials=user_creds)
409-
client = PodMeClient(auth_client=auth_client)
414+
if args.legacy:
415+
auth_client = PodMeDefaultAuthClient(user_credentials=user_creds)
416+
else:
417+
auth_client = PodMeMobileAuthClient(user_credentials=user_creds)
418+
client = PodMeClient(auth_client=auth_client, api_platform="web" if args.legacy else "mobile")
410419
try:
411420
await client.__aenter__()
412421
yield client

podme_api/client.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@
3939
PodMeApiStreamUrlNotFoundError,
4040
PodMeApiUnauthorizedError,
4141
)
42+
from podme_api.helpers import async_cache
4243
from podme_api.models import (
4344
PodMeApiPlatform,
4445
PodMeCategory,
4546
PodMeCategoryPage,
4647
PodMeDownloadProgressTask,
4748
PodMeEpisode,
49+
PodMeEpisodeData,
4850
PodMeHomeScreen,
4951
PodMeLanguage,
5052
PodMePodcast,
@@ -76,7 +78,7 @@ class PodMeClient:
7678
auth_client: PodMeAuthClient
7779
"""auth_client (PodMeAuthClient): The authentication client."""
7880

79-
api_platform: PodMeApiPlatform = PodMeApiPlatform.MOBILE
81+
api_platform: PodMeApiPlatform = "mobile"
8082
"""api_platform (PodMeApiPlatform): The API platform to use."""
8183

8284
disable_credentials_storage: bool = False
@@ -158,6 +160,7 @@ async def _request( # noqa: C901
158160
uri: str,
159161
method: str = METH_GET,
160162
retry: int = 0,
163+
api_platform: PodMeApiPlatform | None = None,
161164
**kwargs,
162165
) -> str | dict | list | bool | None:
163166
"""Make a request to the PodMe API.
@@ -166,6 +169,7 @@ async def _request( # noqa: C901
166169
uri (str): The URI for the API endpoint.
167170
method (str): The HTTP method to use for the request.
168171
retry (int): The number of retries for the request.
172+
api_platform (PodMeApiPlatform): The API platform to use.
169173
**kwargs: Additional keyword arguments for the request.
170174
May include:
171175
- params (dict): Query parameters for the request.
@@ -176,7 +180,9 @@ async def _request( # noqa: C901
176180
The response data from the API.
177181
178182
"""
179-
base_url = PODME_API_URL.format(platform=self.api_platform)
183+
if api_platform is None:
184+
api_platform = self.api_platform
185+
base_url = PODME_API_URL.format(platform=api_platform)
180186
url = URL(f"{base_url.strip('/')}/").join(URL(uri))
181187

182188
access_token = await self.auth_client.async_get_access_token()
@@ -224,7 +230,7 @@ async def _request( # noqa: C901
224230
if response.status == HTTPStatus.TOO_MANY_REQUESTS:
225231
raise PodMeApiRateLimitError("Rate limit error has occurred with the PodMe API")
226232
if response.status == HTTPStatus.NOT_FOUND:
227-
raise PodMeApiNotFoundError("Resource not found")
233+
raise PodMeApiNotFoundError(f"Resource not found: <{url}>")
228234
if response.status == HTTPStatus.BAD_REQUEST:
229235
raise PodMeApiError("Bad request syntax or unsupported method")
230236
if response.status == HTTPStatus.UNAUTHORIZED:
@@ -281,6 +287,7 @@ async def _get_pages(
281287
page_size: int | None = None,
282288
params: dict | None = None,
283289
items_key: str | None = None,
290+
**kwargs,
284291
):
285292
"""Retrieve multiple pages of data from the API.
286293
@@ -291,6 +298,7 @@ async def _get_pages(
291298
page_size: The number of items per page.
292299
params: Additional parameters for the request.
293300
items_key: The key for the items in the response.
301+
**kwargs: Additional keyword arguments which will be passed on to the request.
294302
295303
Returns:
296304
list: The retrieved data.
@@ -311,6 +319,7 @@ async def _get_pages(
311319
"getByOldest": "true" if get_by_oldest else None,
312320
**params,
313321
},
322+
**kwargs,
314323
)
315324
if not isinstance(new_results, list) and items_key is not None:
316325
new_results = new_results.get(items_key, [])
@@ -484,11 +493,11 @@ async def download_files(
484493
on_finished=on_finished,
485494
)
486495

487-
async def get_episode_download_url(self, episode: PodMeEpisode | int) -> tuple[int, URL]:
496+
async def get_episode_download_url(self, episode: PodMeEpisodeData | int) -> tuple[int, URL]:
488497
"""Get the download URL for an episode.
489498
490499
Args:
491-
episode (PodMeEpisode | int): The episode object or episode ID to get the download URL for.
500+
episode (PodMeEpisodeData | int): The episode object or episode ID to get the download URL for.
492501
493502
Returns:
494503
tuple[int, URL]: The episode ID and the download URL.
@@ -500,22 +509,24 @@ async def get_episode_download_url(self, episode: PodMeEpisode | int) -> tuple[i
500509
episode_data = episode
501510
if isinstance(episode_data, int):
502511
episode_data = await self.get_episode_info(episode)
512+
if episode_data.url is not None:
513+
return episode_data.id, URL(episode_data.url)
503514
if episode_data.stream_url is None:
504515
raise PodMeApiStreamUrlError(f"No stream URL found for episode {episode_data.id}")
505516
info = await self.resolve_stream_url(URL(episode_data.stream_url))
506517
return episode_data.id, URL(info["url"])
507518

508519
async def get_episode_download_url_bulk(
509520
self,
510-
episodes: list[PodMeEpisode | int],
521+
episodes: list[PodMeEpisodeData | int],
511522
) -> list[tuple[int, URL]]:
512523
"""Get download URLs for a list of episodes.
513524
514525
This method fetches download URLs for multiple episodes concurrently and
515526
ensures that only unique episode IDs are included in the result.
516527
517528
Args:
518-
episodes (list[PodMeEpisode | int]): A list of PodMeEpisode objects
529+
episodes (list[PodMeEpisodeData | int]): A list of PodMeEpisode objects
519530
or episode IDs for which to fetch download URLs.
520531
521532
Returns:
@@ -793,6 +804,7 @@ async def get_currently_playing(self) -> list[PodMeEpisode]:
793804
)
794805
return [PodMeEpisode.from_dict(data) for data in episodes]
795806

807+
@async_cache
796808
async def get_podcast_info(self, podcast_slug: str) -> PodMePodcast:
797809
"""Get information about a podcast.
798810
@@ -815,19 +827,19 @@ async def get_podcasts_info(self, podcast_slugs: list[str]) -> list[PodMePodcast
815827
podcasts = await asyncio.gather(*[self.get_podcast_info(slug) for slug in podcast_slugs])
816828
return list(podcasts)
817829

818-
async def get_episode_info(self, episode_id: int) -> PodMeEpisode:
830+
async def get_episode_info(self, episode_id: int) -> PodMeEpisodeData:
819831
"""Get information about an episode.
820832
821833
Args:
822834
episode_id (int): The ID of the episode.
823835
824836
"""
825837
data = await self._request(
826-
f"episode/{episode_id}",
838+
f"episodes/{episode_id}",
827839
)
828-
return PodMeEpisode.from_dict(data)
840+
return PodMeEpisodeData.from_dict(data)
829841

830-
async def get_episodes_info(self, episode_ids: list[int]) -> list[PodMeEpisode]:
842+
async def get_episodes_info(self, episode_ids: list[int]) -> list[PodMeEpisodeData]:
831843
"""Get information about multiple episodes.
832844
833845
Args:
@@ -862,22 +874,25 @@ async def search_podcast(
862874
)
863875
return [PodMeSearchResult.from_dict(data) for data in podcasts]
864876

865-
async def get_episode_list(self, podcast_slug: str) -> list[PodMeEpisode]:
877+
async def get_episode_list(self, podcast_slug: str) -> list[PodMeEpisodeData]:
866878
"""Get the full list of episodes for a podcast.
867879
868880
Args:
869881
podcast_slug (str): The slug of the podcast.
870882
871883
"""
884+
podcast_info = await self.get_podcast_info(podcast_slug)
872885
episodes = await self._get_pages(
873-
f"episode/slug/{podcast_slug}",
886+
f"episodes/podcast/{podcast_info.id}",
874887
get_by_oldest=True,
875888
)
876889
_LOGGER.debug("Retrieved full episode list, containing %s episodes", len(episodes))
877890

878-
return [PodMeEpisode.from_dict(data) for data in episodes]
891+
return [PodMeEpisodeData.from_dict(data) for data in episodes]
879892

880-
async def get_latest_episodes(self, podcast_slug: str, episodes_limit: int = 20) -> list[PodMeEpisode]:
893+
async def get_latest_episodes(
894+
self, podcast_slug: str, episodes_limit: int = 20
895+
) -> list[PodMeEpisodeData]:
881896
"""Get the latest episodes for a podcast.
882897
883898
Args:
@@ -888,9 +903,10 @@ async def get_latest_episodes(self, podcast_slug: str, episodes_limit: int = 20)
888903
max_per_page = 50
889904
pages = math.ceil(episodes_limit / max_per_page)
890905
page_size = min(max_per_page, episodes_limit)
906+
podcast_info = await self.get_podcast_info(podcast_slug)
891907

892908
episodes = await self._get_pages(
893-
f"episode/slug/{podcast_slug}",
909+
f"episodes/podcast/{podcast_info.id}",
894910
get_pages=pages,
895911
page_size=page_size,
896912
)
@@ -900,7 +916,7 @@ async def get_latest_episodes(self, podcast_slug: str, episodes_limit: int = 20)
900916
episodes_limit,
901917
len(episodes),
902918
)
903-
return [PodMeEpisode.from_dict(data) for data in episodes]
919+
return [PodMeEpisodeData.from_dict(data) for data in episodes]
904920

905921
async def get_episode_ids(self, podcast_slug) -> list[int]:
906922
"""Get the IDs of all episodes for a podcast.

podme_api/helpers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""podme_api helper functions."""
2+
3+
from functools import wraps
4+
5+
6+
def async_cache(func):
7+
cache = {}
8+
9+
@wraps(func)
10+
async def wrapper(self, slug):
11+
if slug in cache:
12+
return cache[slug]
13+
result = await func(self, slug)
14+
cache[slug] = result
15+
return result
16+
17+
return wrapper

podme_api/models.py

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from dataclasses import dataclass, field
66
from datetime import datetime, time
77
from enum import IntEnum, StrEnum, auto
8-
from typing import TYPE_CHECKING, TypedDict
8+
from typing import TYPE_CHECKING, Literal, TypedDict
99

1010
from mashumaro import field_options
1111
from mashumaro.config import BaseConfig
@@ -16,6 +16,9 @@
1616
from yarl import URL
1717

1818

19+
PodMeApiPlatform = Literal["web", "mobile"]
20+
21+
1922
@dataclass
2023
class BaseDataClassORJSONMixin(DataClassORJSONMixin):
2124
class Config(BaseConfig):
@@ -46,13 +49,6 @@ def __repr__(self):
4649
return self.value.lower()
4750

4851

49-
class PodMeApiPlatform(StrEnum):
50-
"""Enumeration of PodMe API platforms."""
51-
52-
WEB = auto()
53-
MOBILE = auto()
54-
55-
5652
class PodMeRegion(IntEnum):
5753
"""Enumeration of PodMe regions."""
5854

@@ -363,18 +359,18 @@ class PodMeEpisode(PodMeEpisodeBase):
363359
total_no_of_episodes: int | None = field(default=None, metadata=field_options(alias="totalNoOfEpisodes"))
364360

365361

366-
@dataclass
362+
@dataclass(kw_only=True)
367363
class PodMeEpisodeData(PodMeEpisode):
368364
"""Represents detailed data for a PodMe episode."""
369365

370366
number: int = field(metadata=field_options(alias="number"))
371367
byte_length: int = field(metadata=field_options(alias="byteLength"))
372368
url: str = field(metadata=field_options(alias="url"))
373369
type: str = field(metadata=field_options(alias="type"))
374-
smooth_streaming_url: str = field(metadata=field_options(alias="smoothStreamingUrl"))
375-
mpeg_dash_url: str = field(metadata=field_options(alias="mpegDashUrl"))
376-
hls_v3_url: str = field(metadata=field_options(alias="hlsV3Url"))
377-
hls_v4_url: str = field(metadata=field_options(alias="hlsV4Url"))
370+
smooth_streaming_url: str | None = field(default=None, metadata=field_options(alias="smoothStreamingUrl"))
371+
mpeg_dash_url: str | None = field(default=None, metadata=field_options(alias="mpegDashUrl"))
372+
hls_v3_url: str | None = field(default=None, metadata=field_options(alias="hlsV3Url"))
373+
hls_v4_url: str | None = field(default=None, metadata=field_options(alias="hlsV4Url"))
378374
publish_date: datetime = field(metadata=field_options(alias="publishDate"))
379375
has_played: bool = field(metadata=field_options(alias="hasPlayed"))
380376
episode_created_at: datetime = field(metadata=field_options(alias="episodeCreatedAt"))
@@ -403,15 +399,15 @@ class PodMeSubscriptionPlan(BaseDataClassORJSONMixin):
403399
next_plan_product_id: float | None = field(
404400
default=None, metadata=field_options(alias="nextPlanProductId")
405401
)
406-
price: int | None = field(default=None)
402+
region_id: int | None = field(default=None, metadata=field_options(alias="regionId"))
407403

408404

409405
@dataclass
410406
class PodMeSubscription(BaseDataClassORJSONMixin):
411407
"""Represents a PodMe subscription."""
412408

409+
subscription_id: int = field(metadata=field_options(alias="subscriptionId"))
413410
subscription_state: int = field(metadata=field_options(alias="subscriptionState"))
414-
subscription_type: int = field(metadata=field_options(alias="subscriptionType"))
415411
subscription_platform: int = field(metadata=field_options(alias="subscriptionPlatform"))
416412
expiration_date: datetime = field(
417413
metadata=field_options(

0 commit comments

Comments
 (0)