Skip to content

Commit 2b942db

Browse files
committed
support multiple regions in auth client (dynamic client id and base urls)
1 parent 7092c7f commit 2b942db

File tree

7 files changed

+101
-53
lines changed

7 files changed

+101
-53
lines changed

podme_api/auth/client.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from podme_api.auth.models import SchibstedCredentials
2222
from podme_api.const import (
2323
PODME_AUTH_BASE_URL,
24+
PODME_AUTH_CLIENT_ID,
2425
PODME_AUTH_USER_AGENT,
2526
PODME_BASE_URL,
2627
)
@@ -30,16 +31,14 @@
3031
PodMeApiConnectionTimeoutError,
3132
PodMeApiError,
3233
)
34+
from podme_api.models import PodMeRegion
3335

3436
if TYPE_CHECKING:
3537
from podme_api.auth.models import PodMeUserCredentials
3638

3739
_LOGGER = logging.getLogger(__name__)
3840

3941

40-
CLIENT_ID = "62557b19f552881812b7431c"
41-
42-
4342
@dataclass
4443
class PodMeDefaultAuthClient(PodMeAuthClient):
4544
"""Default authentication client for PodMe.
@@ -76,6 +75,9 @@ class PodMeDefaultAuthClient(PodMeAuthClient):
7675
credentials: SchibstedCredentials | None = None
7776
"""(SchibstedCredentials | None): Authentication credentials."""
7877

78+
region = PodMeRegion.NO
79+
"""(PodMeRegion): The region setting for the client."""
80+
7981
_credentials: SchibstedCredentials | None = field(default=None, init=False)
8082
_close_session: bool = False
8183

@@ -93,6 +95,14 @@ def request_header(self) -> dict[str, str]:
9395
"Referer": PODME_BASE_URL,
9496
}
9597

98+
@property
99+
def client_id(self) -> str:
100+
return PODME_AUTH_CLIENT_ID.get(self.region)
101+
102+
@property
103+
def base_url(self) -> URL:
104+
return URL(PODME_AUTH_BASE_URL.get(self.region))
105+
96106
async def _request(
97107
self,
98108
uri: str,
@@ -122,7 +132,7 @@ async def _request(
122132
123133
"""
124134
if base_url is None:
125-
base_url = PODME_AUTH_BASE_URL
135+
base_url = self.base_url
126136
url = URL(base_url).join(URL(uri))
127137
headers = {
128138
**self.request_header,
@@ -205,8 +215,8 @@ async def authorize(self, user_credentials: PodMeUserCredentials) -> SchibstedCr
205215
response = await self._request(
206216
"oauth/authorize",
207217
params={
208-
"client_id": CLIENT_ID,
209-
"redirect_uri": f"pme.podme.{CLIENT_ID}:/login",
218+
"client_id": self.client_id,
219+
"redirect_uri": f"pme.podme.{self.client_id}:/login",
210220
"response_type": "code",
211221
"scope": "openid offline_access",
212222
"state": hashlib.sha256(os.urandom(1024)).hexdigest(),
@@ -222,15 +232,15 @@ async def authorize(self, user_credentials: PodMeUserCredentials) -> SchibstedCr
222232
# Login: step 2/4
223233
response = await self._request(
224234
"authn/api/settings/csrf",
225-
params={"client_id": CLIENT_ID},
235+
params={"client_id": self.client_id},
226236
)
227237
csrf_token = (await response.json())["data"]["attributes"]["csrfToken"]
228238

229239
# Login: step 3/4
230240
response = await self._request(
231241
"authn/api/identity/email-status",
232242
method=METH_POST,
233-
params={"client_id": CLIENT_ID},
243+
params={"client_id": self.client_id},
234244
headers={
235245
"X-CSRF-Token": csrf_token,
236246
"Accept": "application/json",
@@ -247,7 +257,7 @@ async def authorize(self, user_credentials: PodMeUserCredentials) -> SchibstedCr
247257
response = await self._request(
248258
"authn/api/identity/login/",
249259
method=METH_POST,
250-
params={"client_id": CLIENT_ID},
260+
params={"client_id": self.client_id},
251261
headers={
252262
"X-CSRF-Token": csrf_token,
253263
"Accept": "application/json",
@@ -266,7 +276,7 @@ async def authorize(self, user_credentials: PodMeUserCredentials) -> SchibstedCr
266276
response = await self._request(
267277
"authn/identity/finish/",
268278
method=METH_POST,
269-
params={"client_id": CLIENT_ID},
279+
params={"client_id": self.client_id},
270280
headers={
271281
"Content-Type": "application/x-www-form-urlencoded",
272282
},
@@ -289,13 +299,13 @@ async def authorize(self, user_credentials: PodMeUserCredentials) -> SchibstedCr
289299
method=METH_POST,
290300
headers={
291301
"X-OIDC": "v1",
292-
"X-Region": "NO", # @TODO: Support multiple regions.
302+
"X-Region": self.region.name,
293303
},
294304
data={
295-
"client_id": CLIENT_ID,
305+
"client_id": self.client_id,
296306
"grant_type": "authorization_code",
297307
"code": code,
298-
"redirect_uri": f"pme.podme.{CLIENT_ID}:/login",
308+
"redirect_uri": f"pme.podme.{self.client_id}:/login",
299309
"code_verifier": code_verifier,
300310
},
301311
allow_redirects=False,
@@ -332,14 +342,14 @@ async def refresh_token(self, credentials: SchibstedCredentials | None = None):
332342
"oauth/token",
333343
method=METH_POST,
334344
headers={
335-
"Host": "payment.schibsted.no",
345+
"Host": self.base_url.host,
336346
"Content-Type": "application/x-www-form-urlencoded",
337347
"User-Agent": "AccountSDKAndroidWeb/6.4.0 (Linux; Android 15; API 35; Google; sdk_gphone64_arm64)",
338348
"X-OIDC": "v1",
339-
"X-Region": "NO", # @TODO: Support multiple regions.
349+
"X-Region": self.region.name,
340350
},
341351
data={
342-
"client_id": CLIENT_ID,
352+
"client_id": self.client_id,
343353
"grant_type": "refresh_token",
344354
"refresh_token": credentials.refresh_token,
345355
},

podme_api/auth/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ class PyTestHttpFixture(TypedDict):
8787

8888
no: int
8989
status: int
90-
headers: dict[str, str]
90+
headers: dict
9191
body: str
9292
method: str
9393
url: str

podme_api/client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ class PodMeClient:
100100
PodMeRegion.FI,
101101
]
102102

103+
def __post_init__(self):
104+
"""Initialize the client after initialization."""
105+
self.auth_client.region = self.region
106+
103107
def set_conf_dir(self, conf_dir: PathLike | str) -> None:
104108
"""Set the configuration directory.
105109
@@ -267,7 +271,7 @@ def request_header(self) -> dict[str, str]:
267271
"""Generate a header for HTTP requests to the server."""
268272
return {
269273
"Accept": "application/json",
270-
"X-Region": str(self.region),
274+
"X-Region": self.region.name,
271275
"User-Agent": PODME_API_USER_AGENT,
272276
}
273277

@@ -508,9 +512,10 @@ async def get_episode_download_url(self, episode: PodMeEpisodeData | int) -> tup
508512
episode_data = await self.get_episode_info(episode)
509513
if episode_data.url is not None:
510514
return episode_data.id, URL(episode_data.url)
511-
if episode_data.stream_url is None:
515+
if episode_data.stream_url is None and episode_data.smooth_streaming_url is None:
512516
raise PodMeApiStreamUrlError(f"No stream URL found for episode {episode_data.id}")
513-
info = await self.resolve_stream_url(URL(episode_data.stream_url))
517+
stream_url = episode_data.stream_url or episode_data.smooth_streaming_url
518+
info = await self.resolve_stream_url(URL(stream_url))
514519
return episode_data.id, URL(info["url"])
515520

516521
async def get_episode_download_url_bulk(

podme_api/const.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,25 @@
22

33
import logging
44

5+
from podme_api.models import PodMeRegion
6+
57
PODME_AUTH_USER_AGENT = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0"
68
PODME_API_USER_AGENT = "Podme android app/6.29.3 (Linux;Android 15) AndroidXMedia3/1.5.1"
79
PODME_API_URL = "https://api.podme.com/mobile/api"
810
PODME_BASE_URL = "https://podme.com"
9-
PODME_AUTH_BASE_URL = "https://payment.schibsted.no"
10-
PODME_AUTH_RETURN_URL = f"{PODME_BASE_URL}/no/oppdag"
11+
12+
PODME_AUTH_BASE_URL = {
13+
PodMeRegion.NO: "https://payment.schibsted.no",
14+
PodMeRegion.SE: "https://login.schibsted.com",
15+
PodMeRegion.FI: "https://login.schibsted.fi",
16+
PodMeRegion.DK: "https://login.schibsted.dk",
17+
}
18+
PODME_AUTH_CLIENT_ID = {
19+
PodMeRegion.NO: "62557b19f552881812b7431c",
20+
PodMeRegion.SE: "66fd141b3f97a8558ace8ab9", # TODO: Check
21+
PodMeRegion.FI: "62557b19f552881812b7431c", # TODO: Check
22+
PodMeRegion.DK: "62557b19f552881812b7431c", # TODO: Check
23+
}
1124

1225
DEFAULT_REQUEST_TIMEOUT = 15
1326

podme_api/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class PodMeRegion(IntEnum):
5555
SE = 1
5656
NO = 2
5757
FI = 3
58+
DK = 4
5859

5960
def __repr__(self):
6061
return self.name
@@ -418,6 +419,13 @@ class PodMeEpisodeData(PodMeEpisode):
418419
default=None, metadata=field_options(alias="playInfoUpdatedAt")
419420
)
420421

422+
@classmethod
423+
def __pre_deserialize__(cls: type[T], d: T) -> T:
424+
for key in ["url", "streamUrl", "smoothStreamingUrl", "mpegDashUrl", "hlsV3Url", "hlsV4Url"]:
425+
val = d.get(key)
426+
d[key] = None if val == "" else val
427+
return d
428+
421429

422430
@dataclass
423431
class PodMeUserPrivacySettings(BaseDataClassORJSONMixin):

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ ignore = [
105105
"SLOT000",
106106
"TD003", "TID252", "TRY003", "TRY300",
107107
"W191",
108+
"TD002",
108109
]
109110
select = ["ALL"]
110111

tests/test_auth.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,14 @@
1313
from yarl import URL
1414

1515
from podme_api import PodMeClient, PodMeDefaultAuthClient
16-
from podme_api.const import PODME_AUTH_BASE_URL, PODME_BASE_URL
16+
from podme_api.const import PODME_AUTH_BASE_URL
1717
from podme_api.exceptions import (
1818
PodMeApiAuthenticationError,
1919
PodMeApiConnectionError,
2020
PodMeApiConnectionTimeoutError,
2121
PodMeApiError,
2222
)
23+
from podme_api.models import PodMeRegion
2324

2425
from .helpers import setup_auth_mocks
2526

@@ -40,12 +41,14 @@ async def test_async_get_access_token_with_valid_credentials(podme_default_auth_
4041
async def test_async_get_access_token_with_expired_credentials(
4142
aresponses: ResponsesMockServer, podme_default_auth_client, expired_credentials, refreshed_credentials
4243
):
43-
aresponses.add(
44-
URL(PODME_AUTH_BASE_URL).host,
45-
"/oauth/token",
46-
"POST",
47-
json_response(data=refreshed_credentials.to_dict()),
48-
)
44+
for region in PodMeRegion:
45+
base_url = PODME_AUTH_BASE_URL.get(region)
46+
aresponses.add(
47+
URL(base_url).host,
48+
"/oauth/token",
49+
"POST",
50+
json_response(data=refreshed_credentials.to_dict()),
51+
)
4952
async with podme_default_auth_client(credentials=expired_credentials) as auth_client:
5053
access_token = await auth_client.async_get_access_token()
5154
assert access_token == refreshed_credentials.access_token
@@ -68,25 +71,29 @@ async def test_refresh_token_success(
6871
aresponses: ResponsesMockServer, podme_default_auth_client, default_credentials, refreshed_credentials
6972
):
7073
# Mock the refresh token endpoint
71-
aresponses.add(
72-
URL(PODME_AUTH_BASE_URL).host,
73-
"/oauth/token",
74-
"POST",
75-
json_response(data=refreshed_credentials.to_dict()),
76-
)
74+
for region in PodMeRegion:
75+
base_url = PODME_AUTH_BASE_URL.get(region)
76+
aresponses.add(
77+
URL(base_url).host,
78+
"/oauth/token",
79+
"POST",
80+
json_response(data=refreshed_credentials.to_dict()),
81+
)
7782
async with podme_default_auth_client() as auth_client:
7883
new_credentials = await auth_client.refresh_token(default_credentials)
7984
assert new_credentials == refreshed_credentials
8085

8186

8287
async def test_refresh_token_failure(aresponses: ResponsesMockServer, podme_default_auth_client):
8388
# Mock the refresh token endpoint to return an error
84-
aresponses.add(
85-
URL(PODME_BASE_URL).host,
86-
"/oauth/token",
87-
"GET",
88-
aresponses.Response(text="Unauthorized", status=401),
89-
)
89+
for region in PodMeRegion:
90+
base_url = PODME_AUTH_BASE_URL.get(region)
91+
aresponses.add(
92+
URL(base_url).host,
93+
"/oauth/token",
94+
"GET",
95+
aresponses.Response(text="Unauthorized", status=401),
96+
)
9097

9198
async with podme_default_auth_client() as auth_client:
9299
with pytest.raises(PodMeApiConnectionError):
@@ -135,12 +142,14 @@ async def response_handler(_: ClientResponse):
135142
await sleep(1)
136143
return aresponses.Response(body="Helluu") # pragma: no cover
137144

138-
aresponses.add(
139-
URL(PODME_AUTH_BASE_URL).host,
140-
"/oauth/authorize",
141-
"GET",
142-
response_handler,
143-
)
145+
for region in PodMeRegion:
146+
base_url = PODME_AUTH_BASE_URL.get(region)
147+
aresponses.add(
148+
URL(base_url).host,
149+
"/oauth/authorize",
150+
"GET",
151+
response_handler,
152+
)
144153
async with podme_default_auth_client() as auth_client:
145154
auth_client.request_timeout = 0.1
146155
with pytest.raises(PodMeApiConnectionTimeoutError):
@@ -149,12 +158,14 @@ async def response_handler(_: ClientResponse):
149158

150159
async def test_request_bad_request(aresponses: ResponsesMockServer, podme_default_auth_client):
151160
# Mock a 400 Bad Request response
152-
aresponses.add(
153-
URL(PODME_AUTH_BASE_URL).host,
154-
"/invalid/endpoint",
155-
"GET",
156-
aresponses.Response(text="Bad Request", status=400),
157-
)
161+
for region in PodMeRegion:
162+
base_url = PODME_AUTH_BASE_URL.get(region)
163+
aresponses.add(
164+
URL(base_url).host,
165+
"/invalid/endpoint",
166+
"GET",
167+
aresponses.Response(text="Bad Request", status=400),
168+
)
158169
async with podme_default_auth_client() as auth_client:
159170
with pytest.raises(PodMeApiError) as exc_info:
160171
await auth_client._request("invalid/endpoint")

0 commit comments

Comments
 (0)