audible_api 0.1.0

Unofficial API for Audible.
Documentation
use std::collections::HashMap;

use chrono::{Duration, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};

use crate::auth::oauth::build_client_id;
use crate::Result;

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Registration {
    pub device_serial: String,
    pub client_id: String,
    pub adp_token: String,
    pub device_private_key: String,
    pub access_token: String,
    pub refresh_token: String,
    pub expires: i64,
    pub website_cookies: HashMap<String, String>,
    pub store_authentication_cookie: String,
    pub device_info: Value,
    pub customer_info: Value,
}

pub async fn register(
    authorization_code: &str,
    code_verifier: &str,
    domain: &str,
    device_serial: &str,
    with_username: bool,
) -> Result<Registration> {
    let client_id = build_client_id(device_serial);

    let body = json!({
        "requested_token_type": [
            "bearer",
            "mac_dms",
            "website_cookies",
            "store_authentication_cookie"
        ],
        "cookies": {
            "website_cookies": [],
            "domain": format!(".amazon.{}", domain)
        },
        "registration_data": {
            "domain": "Device",
            "app_version": "3.56.2",
            "device_serial": device_serial,
            "device_type": "A2CZJZGLK2JJVM",
            "device_name": "%FIRST_NAME%%FIRST_NAME_POSSESSIVE_STRING%%DUPE_STRATEGY_1ST%Audible for iPhone",
            "os_version": "15.0.0",
            "software_version": "35602678",
            "device_model": "iPhone",
            "app_name": "Audible"
        },
        "auth_data": {
            "client_id": client_id,
            "authorization_code": authorization_code,
            "code_verifier": code_verifier,
            "code_algorithm": "SHA-256",
            "client_domain": "DeviceLegacy"
        },
        "requested_extensions": [
            "device_info",
            "customer_info"
        ]
    });

    let target_domain = if with_username { "audible" } else { "amazon" };

    let resp = reqwest::Client::new()
        .post(&format!(
            "https://api.{}.{}/auth/register",
            target_domain, domain
        ))
        .json(&body)
        .send()
        .await?;
    if !resp.status().is_success() {
        return Err(format!("Failed to register: {}", resp.text().await?).into());
    }

    let resp_json: Value = resp.json().await?;
    let success_response = &resp_json["response"]["success"];

    let tokens = &success_response["tokens"];
    let adp_token = tokens["mac_dms"]["adp_token"]
        .as_str()
        .ok_or("Missing adp_token")?
        .to_string();
    let device_private_key = tokens["mac_dms"]["device_private_key"]
        .as_str()
        .ok_or("Missing device_private_key")?
        .to_string();
    let store_authentication_cookie = tokens["store_authentication_cookie"]["cookie"]
        .as_str()
        .ok_or("Missing store_authentication_cookie")?
        .to_string();
    let access_token = tokens["bearer"]["access_token"]
        .as_str()
        .ok_or("Missing access_token")?
        .to_string();
    let refresh_token = tokens["bearer"]["refresh_token"]
        .as_str()
        .ok_or("Missing refresh_token")?
        .to_string();
    let expires_s: i64 = tokens["bearer"]["expires_in"]
        .as_str()
        .ok_or("Missing expires_in")?
        .parse()?;
    let expires = (Utc::now() + Duration::seconds(expires_s)).timestamp();

    let extensions = &success_response["extensions"];
    let device_info = extensions["device_info"].clone();
    let customer_info = extensions["customer_info"].clone();

    let mut website_cookies = HashMap::new();
    if let Some(cookies) = tokens["website_cookies"].as_array() {
        for cookie in cookies {
            if let (Some(name), Some(value)) = (cookie["Name"].as_str(), cookie["Value"].as_str()) {
                website_cookies.insert(name.to_string(), value.replace('"', ""));
            }
        }
    }

    Ok(Registration {
        device_serial: device_serial.to_string(),
        client_id,
        adp_token,
        device_private_key,
        access_token,
        refresh_token,
        expires,
        website_cookies,
        store_authentication_cookie,
        device_info,
        customer_info,
    })
}

pub async fn deregister(
    access_token: &str,
    domain: &str,
    deregister_all_devices: bool,
    with_username: bool,
) -> Result<Value> {
    let body = json!({
        "deregister_all_existing_accounts": deregister_all_devices
    });

    let headers = {
        let mut headers = reqwest::header::HeaderMap::new();
        headers.insert(
            reqwest::header::AUTHORIZATION,
            format!("Bearer {}", access_token).parse()?,
        );
        headers
    };

    let target_domain = if with_username { "audible" } else { "amazon" };

    let resp = reqwest::Client::new()
        .post(&format!(
            "https://api.{}.{}/auth/deregister",
            target_domain, domain
        ))
        .json(&body)
        .headers(headers)
        .send()
        .await?;

    if !resp.status().is_success() {
        return Err(format!("Failed to deregister: {}", resp.text().await?).into());
    }
    let json = resp.json().await?;

    Ok(json)
}