import base64
import hashlib
import pathlib
import struct
import urllib.parse
from typing import TYPE_CHECKING, Literal, overload
import httpx
from .exceptions import AuthFlowError
if TYPE_CHECKING:
import audible
from ._types import TrueFalseT
[docs]
def get_player_id() -> str:
"""Build a software player Id."""
player_id = base64.encodebytes(hashlib.sha1(b"").digest()).rstrip() # noqa: S324
return player_id.decode("ascii")
[docs]
def get_player_token(auth: "audible.Authenticator") -> str:
"""Fetches a player token for further authentication.
Args:
auth: The Authenticator.
Returns:
The player token.
Raises:
Exception: If `playerToken` not found in response url query.
"""
if auth.locale is None:
raise Exception("No locale set on `Authenticator`.")
player_id = get_player_id()
url = httpx.URL(
url=f"https://www.audible.{auth.locale.domain}/player-auth-token",
params={
"ipRedirectOverride": True,
"playerType": "software",
"bp_ua": "y",
"playerModel": "Desktop",
"playerId": player_id,
"playerManufacturer": "Audible",
"serial": "",
},
)
with httpx.Client(cookies=auth.website_cookies) as session:
resp = session.get(url, follow_redirects=True)
query = resp.url.query.decode()
parsed_query = urllib.parse.parse_qs(query)
player_token = parsed_query.get("playerToken")
if player_token is None:
raise Exception("No player token found in response url query.")
return player_token[0]
[docs]
def fetch_activation(player_token: str) -> bytes:
"""Fetches the activation blob with player token from Audible server.
Args:
player_token: A player token returned by ``get_player_token`` function.
Returns:
The activation blob.
"""
url = "https://www.audible.com/license/licenseForCustomerToken"
# register params
register_params = {"customer_token": player_token}
# deregister params
deregister_params = {"customer_token": player_token, "action": "de-register"}
headers = {"User-Agent": "Audible Download Manager"}
with httpx.Client(headers=headers) as session:
session.get(url, params=deregister_params)
try:
resp = session.get(url, params=register_params)
return resp.content
finally:
session.get(url, params=deregister_params)
[docs]
def fetch_activation_sign_auth(auth: "audible.Authenticator") -> bytes:
"""Fetches the activation blob with sign authentication from Audible server.
Args:
auth: A ``Authenticator`` instance with valid ``adp_token`` and
``device_private_cert``.
Returns:
The activation blob.
Raises:
AuthFlowError: If no valid auth method is available.
"""
if "signing" not in auth.available_auth_modes:
raise AuthFlowError
url = "https://www.audible.com/license/token"
params = {
"player_manuf": "Audible,iPhone",
"action": "register",
"player_model": "iPhone",
}
with httpx.Client(auth=auth) as client:
resp = client.get(url, params=params)
return resp.content
@overload
def get_activation_bytes(
auth: "audible.Authenticator",
filename: str | pathlib.Path | None = ...,
extract: Literal[True] = ...,
) -> str: ...
@overload
def get_activation_bytes(
auth: "audible.Authenticator",
filename: str | pathlib.Path | None = ...,
*,
extract: Literal[False],
) -> bytes: ...
[docs]
def get_activation_bytes(
auth: "audible.Authenticator",
filename: str | pathlib.Path | None = None,
extract: "TrueFalseT" = True,
) -> str | bytes:
"""Fetches the activation blob from Audible and extracts the bytes.
Args:
auth: The Authenticator.
filename: The filename to save the activation blob (Default: None).
extract: If True, returns the extracted activation bytes otherwise
the whole activation blob (Default: True).
Returns:
The activation bytes or activation blob.
Raises:
AuthFlowError: If no valid auth method is available.
"""
auth_modes = auth.available_auth_modes
if "signing" in auth_modes:
activation = fetch_activation_sign_auth(auth)
elif "cookies" in auth_modes:
player_token = get_player_token(auth)
activation = fetch_activation(player_token)
else:
raise AuthFlowError("No valid auth mode to fetch activation bytes.")
if filename:
pathlib.Path(filename).write_bytes(activation)
if extract:
return extract_activation_bytes(activation)
return activation