/
/
/
1"""Small shared helpers for OAuth setup flows that use the MA hosted callback bounce."""
2
3from __future__ import annotations
4
5from music_assistant.models.setup_flow import SetupFlowError
6
7# Fixed https redirect URI that OAuth providers which only allow pre-registered
8# redirect URIs (Spotify, Google, Microsoft, ...) have on file. The page hosted
9# there forwards the browser to the local (session specific) callback URL that we
10# smuggle along in the OAuth `state` parameter.
11HOSTED_CALLBACK_URL = "https://music-assistant.io/callback"
12
13# Deadline for the browser part of an OAuth flow. Bounded so the client shows a
14# countdown and a consent that never comes back (window closed, callback blocked)
15# ends the flow instead of leaving the user with a spinner.
16OAUTH_STEP_TIMEOUT = 10 * 60
17
18
19def hosted_bounce_redirect(callback_url: str) -> tuple[str, str]:
20 """
21 Return the (redirect_uri, state) pair for a hosted-bounce OAuth authorize URL.
22
23 The redirect_uri is the fixed MA callback page the provider has pre-registered;
24 it forwards the browser to the flow's local callback URL, carried in `state`.
25
26 :param callback_url: The setup session's local callback URL (session.callback_url).
27 """
28 return HOSTED_CALLBACK_URL, callback_url
29
30
31def authorization_code_from_params(params: dict[str, str]) -> str:
32 """
33 Return the authorization code from OAuth callback params, or raise SetupFlowError.
34
35 :param params: The merged callback query/body params returned by session.external().
36 """
37 code = params.get("code")
38 # an older (cached) hosted relay page forwards a literal "null" code on denied consent
39 if not code or code == "null":
40 error = params.get("error") or "no authorization code returned"
41 raise SetupFlowError(f"Authorization failed: {error}")
42 return code
43