use std::sync::OnceLock;
use tokio::sync::{mpsc, oneshot};
#[cfg(not(target_os = "android"))]
mod runtime;
pub struct MintRequest {
pub video_id: String,
pub reply: oneshot::Sender<Result<String, String>>,
}
static MINTER: OnceLock<mpsc::UnboundedSender<MintRequest>> = OnceLock::new();
pub fn set_minter(
tx: mpsc::UnboundedSender<MintRequest>,
) -> Result<(), mpsc::UnboundedSender<MintRequest>> {
MINTER.set(tx)
}
pub fn is_available() -> bool {
MINTER.get().is_some()
}
#[cfg(not(target_os = "android"))]
pub fn ensure_started() {
use std::sync::Once;
static START: Once = Once::new();
START.call_once(|| {
crate::ytmusic::ensure_v8_platform();
let (tx, rx) = mpsc::unbounded_channel::<MintRequest>();
let _ = set_minter(tx);
if let Err(e) = std::thread::Builder::new()
.name("botguard".into())
.spawn(move || runtime::run(rx))
{
tracing::error!(error = %e, "failed to spawn BotGuard runtime thread");
}
});
}
#[cfg(target_os = "android")]
pub fn ensure_started() {}
#[cfg(all(test, not(target_os = "android")))]
mod tests {
#[tokio::test]
#[ignore = "hits live YouTube BotGuard"]
async fn mints_a_content_pot() {
let pot = super::mint_content_pot("dQw4w9WgXcQ")
.await
.expect("mint should succeed");
assert!(!pot.is_empty(), "pot must be non-empty");
let pot2 = super::mint_content_pot("9bZkp7q19f0")
.await
.expect("second mint should succeed");
assert!(!pot2.is_empty());
}
}
#[tracing::instrument(name = "yt.mint_pot", fields(video_id = %video_id))]
pub async fn mint_content_pot(video_id: &str) -> Result<String, String> {
ensure_started();
let tx = MINTER
.get()
.ok_or_else(|| "PO token minter unavailable on this platform".to_string())?;
let (reply, rx) = oneshot::channel();
tx.send(MintRequest {
video_id: video_id.to_string(),
reply,
})
.map_err(|_| "PO token minter channel closed".to_string())?;
match tokio::time::timeout(std::time::Duration::from_secs(20), rx).await {
Ok(Ok(result)) => result,
Ok(Err(_)) => Err("PO token minter dropped the reply".to_string()),
Err(_) => Err("PO token mint timed out".to_string()),
}
}