use std::time::Duration;
use async_trait::async_trait;
use reqwest::Client;
use serde::Deserialize;
use crate::engine::models::{Endpoints, Server};
use crate::engine::network::client::build_client;
use crate::errors::{EngineError, EngineResult};
use super::Provider;
const API_TOKEN: &str = "YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm";
const TARGET_COUNT: u8 = 5;
const DISCOVERY_TIMEOUT: Duration = Duration::from_secs(6);
const DOWNLOAD_RANGE_BYTES: u64 = 26_214_400;
pub struct FastProvider {
client: Client,
}
impl FastProvider {
pub fn new() -> EngineResult<Self> {
Ok(Self {
client: build_client(DISCOVERY_TIMEOUT, 1)?,
})
}
}
#[async_trait]
impl Provider for FastProvider {
fn name(&self) -> &'static str {
"Fast.com"
}
async fn fetch_servers(&self) -> EngineResult<Vec<Server>> {
let url = format!(
"https://api.fast.com/netflix/speedtest/v2?https=true&token={API_TOKEN}&urlCount={TARGET_COUNT}"
);
let response = tokio::time::timeout(DISCOVERY_TIMEOUT, self.client.get(url).send())
.await
.map_err(|_| EngineError::InvalidResponse("target request timed out".to_owned()))??;
if !response.status().is_success() {
return Err(EngineError::InvalidResponse(format!(
"target API returned HTTP {}",
response.status()
)));
}
let body = response.text().await?;
parse_targets(&body)
}
}
#[derive(Debug, Deserialize)]
struct RawResponse {
targets: Vec<RawTarget>,
}
#[derive(Debug, Deserialize)]
struct RawTarget {
url: Option<String>,
location: Option<RawLocation>,
}
#[derive(Debug, Deserialize)]
struct RawLocation {
city: Option<String>,
country: Option<String>,
}
pub fn parse_targets(json: &str) -> EngineResult<Vec<Server>> {
let raw: RawResponse = serde_json::from_str(json)
.map_err(|err| EngineError::InvalidResponse(format!("malformed target list: {err}")))?;
let servers = raw
.targets
.into_iter()
.filter_map(|target| {
let url = target.url?;
if !url.starts_with("https://") {
return None;
}
let host = url
.strip_prefix("https://")
.and_then(|rest| rest.split('/').next())
.unwrap_or_default()
.to_owned();
if host.is_empty() {
return None;
}
let name = match target.location {
Some(RawLocation {
city: Some(city),
country: Some(country),
}) => format!("Fast.com — {city}, {country}"),
_ => format!("Fast.com — {host}"),
};
Some(Server {
name,
description: host,
endpoints: Endpoints {
ping: with_range(&url, 0),
download: with_range(&url, DOWNLOAD_RANGE_BYTES),
upload: with_range(&url, DOWNLOAD_RANGE_BYTES),
},
})
})
.collect();
Ok(servers)
}
fn with_range(url: &str, bytes: u64) -> String {
match url.split_once('?') {
Some((path, query)) => format!("{path}/range/0-{bytes}?{query}"),
None => format!("{url}/range/0-{bytes}"),
}
}