use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BrowserAuth {
pub cookie: String,
#[serde(alias = "x-goog-authuser")]
pub x_goog_authuser: String,
#[serde(default = "default_origin")]
pub origin: String,
}
fn default_origin() -> String {
"https://music.youtube.com".to_string()
}
impl BrowserAuth {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let content = std::fs::read_to_string(path)?;
Self::from_json(&content)
}
pub fn from_json(json: &str) -> Result<Self> {
let parsed: serde_json::Value = serde_json::from_str(json)?;
let cookie = parsed
.get("cookie")
.or_else(|| parsed.get("Cookie"))
.and_then(|v| v.as_str())
.ok_or_else(|| Error::InvalidAuth("missing 'cookie' field".to_string()))?
.to_string();
let x_goog_authuser = parsed
.get("x-goog-authuser")
.or_else(|| parsed.get("X-Goog-Authuser"))
.and_then(|v| v.as_str())
.unwrap_or("0")
.to_string();
let origin = parsed
.get("origin")
.or_else(|| parsed.get("Origin"))
.or_else(|| parsed.get("x-origin"))
.or_else(|| parsed.get("X-Origin"))
.and_then(|v| v.as_str())
.unwrap_or("https://music.youtube.com")
.to_string();
Ok(Self {
cookie,
x_goog_authuser,
origin,
})
}
pub fn sapisid(&self) -> Result<String> {
for part in self.cookie.split(';') {
let part = part.trim();
if let Some(value) = part.strip_prefix("__Secure-3PAPISID=") {
return Ok(value.to_string());
}
}
Err(Error::InvalidAuth(
"cookie missing __Secure-3PAPISID".to_string(),
))
}
pub fn get_authorization(&self) -> Result<String> {
let sapisid = self.sapisid()?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let auth_string = format!("{} {} {}", timestamp, sapisid, self.origin);
let mut hasher = Sha1::new();
hasher.update(auth_string.as_bytes());
let hash = hasher.finalize();
Ok(format!("SAPISIDHASH {}_{:x}", timestamp, hash))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_sapisid() {
let auth = BrowserAuth {
cookie: "other=value; __Secure-3PAPISID=abc123def; more=stuff".to_string(),
x_goog_authuser: "0".to_string(),
origin: "https://music.youtube.com".to_string(),
};
assert_eq!(auth.sapisid().unwrap(), "abc123def");
}
#[test]
fn test_from_json() {
let json = r#"{"cookie": "test=1; __Secure-3PAPISID=xyz", "x-goog-authuser": "0"}"#;
let auth = BrowserAuth::from_json(json).unwrap();
assert_eq!(auth.sapisid().unwrap(), "xyz");
}
}