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