fast-down 4.0.6

Download everything fast
Documentation
use crate::{
    UrlInfo,
    http::{
        ContentDisposition, GetResponse, HttpClient, HttpError, HttpHeaders, HttpRequestBuilder,
        HttpResponse,
    },
    url_info::FileId,
};
use std::{borrow::Borrow, future::Future, time::Duration};
use url::Url;

pub type PrefetchResult<Client> =
    Result<(UrlInfo, GetResponse<Client>), (HttpError<Client>, Option<Duration>)>;

pub trait Prefetch<Client: HttpClient> {
    fn prefetch(&self, url: Url) -> impl Future<Output = PrefetchResult<Client>> + Send;
}

impl<Client, BorrowClient> Prefetch<Client> for BorrowClient
where
    Client: HttpClient,
    BorrowClient: Borrow<Client> + Sync,
{
    async fn prefetch(&self, url: Url) -> PrefetchResult<Client> {
        prefetch(self.borrow(), url).await
    }
}

fn get_filename(headers: &impl HttpHeaders, url: &Url) -> String {
    headers
        .get("content-disposition")
        .ok()
        .and_then(|s| ContentDisposition::parse(s.as_ref()).filename)
        .map(|s| {
            let s = urlencoding::decode_binary(s.as_bytes());
            String::from_utf8_lossy(&s).into_owned()
        })
        .filter(|s| !s.trim().is_empty())
        .or_else(|| {
            url.path_segments()
                .and_then(|mut segments| segments.next_back())
                .map(|s| {
                    let s = urlencoding::decode_binary(s.as_bytes());
                    String::from_utf8_lossy(&s).into_owned()
                })
                .filter(|s| !s.trim().is_empty())
        })
        .or_else(|| url.host_str().map(ToString::to_string))
        .unwrap_or_else(|| url.to_string())
}

async fn prefetch<Client: HttpClient>(client: &Client, url: Url) -> PrefetchResult<Client> {
    let (no_range_fut, range_fut) = (
        prefetch_no_range(client, url.clone()),
        is_support_range(client, url),
    );
    let (result_no_range, result_range) = tokio::join!(no_range_fut, range_fut);
    let mut res = result_no_range?;
    if let Ok(supports_range) = result_range
        && supports_range
    {
        res.0.supports_range = true;
        if res.0.size != 0 {
            res.0.fast_download = true;
        }
    }
    Ok(res)
}

async fn prefetch_no_range<Client: HttpClient>(
    client: &Client,
    url: Url,
) -> PrefetchResult<Client> {
    let resp = client
        .get(url, None)
        .send()
        .await
        .map_err(|(e, d)| (HttpError::Request(e), d))?;
    let headers = resp.headers();
    let size = headers
        .get("content-length")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(0);
    let final_url = resp.url();
    Ok((
        UrlInfo {
            final_url: final_url.clone(),
            raw_name: get_filename(headers, final_url),
            size,
            supports_range: false,
            fast_download: false,
            file_id: FileId::new(
                headers.get("etag").ok().as_deref(),
                headers.get("last-modified").ok().as_deref(),
            ),
            content_type: headers.get("content-type").ok().map(String::from),
        },
        resp,
    ))
}

async fn is_support_range<Client: HttpClient>(
    client: &Client,
    url: Url,
) -> Result<bool, (HttpError<Client>, Option<Duration>)> {
    let resp = client
        .get(url, Some(0..1))
        .send()
        .await
        .map_err(|(e, d)| (HttpError::Request(e), d))?;
    let headers = resp.headers();
    let supports_range = headers
        .get("content-range")
        .is_ok_and(|v| v.trim_start().to_lowercase().starts_with("bytes 0-0/"));
    Ok(supports_range)
}