biliget 0.6.8

简单的B站视频下载工具 支持免登录下载B站高清视频
use crate::util::http::default_http_headers;
use http::HeaderMap;
use reqwest;
use serde_json::Value;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum FetchError {
    #[error("网络请求失败: {0}")]
    Request(#[from] reqwest::Error),

    #[error("请求头构造失败: {0}")]
    InvalidHeader(#[from] reqwest::header::InvalidHeaderValue),

    #[error("数据解析失败: 缺少字段 {0}")]
    ParseError(&'static str),
}

const BILIBILI_DOWNLOAD_URL_API_URL: &str =
    "https://api.bilibili.com/x/player/playurl?qn=80&fnval=4048&fourk=1&try_look=1";
const BILIBILI_INFO_API_URL: &str = "https://api.bilibili.com/x/player/pagelist";

async fn get_info(client: &reqwest::Client, bvid: &str) -> Result<(i64, String), FetchError> {
    let json: Value = client
        .get(BILIBILI_INFO_API_URL)
        .query(&[("bvid", bvid)])
        .send()
        .await?
        .json()
        .await?;

    let cid = if let Some(cid) = json["data"][0]["cid"].as_i64() {
        cid
    } else {
        return Err(FetchError::ParseError("cid"));
    };

    let title = if let Some(title) = json["data"][0]["part"].as_str() {
        title.to_string()
    } else {
        return Err(FetchError::ParseError("title"));
    };

    Ok((cid, title))
}

pub async fn get_download_url(
    bvid: &str,
) -> Result<(String, String, String, HeaderMap), FetchError> {
    let referrer = format!("https://www.bilibili.com/video/{bvid}");
    let mut headers = default_http_headers();
    headers.insert("referer", referrer.parse()?);
    headers.insert("origin", "https://www.bilibili.com".parse()?);

    let client = reqwest::Client::builder()
        .default_headers(headers.clone())
        .build()?;

    let (cid, title) = get_info(&client, bvid).await?;

    let json: Value = client
        .get(BILIBILI_DOWNLOAD_URL_API_URL)
        .query(&[("bvid", bvid)])
        .query(&[("cid", cid)])
        .send()
        .await?
        .json()
        .await?;

    let video_url = if let Some(video_url) = json["data"]["dash"]["video"][0]["baseUrl"].as_str() {
        video_url.to_string()
    } else {
        return Err(FetchError::ParseError("video_url"));
    };

    let audio_url = if let Some(audio_url) = json["data"]["dash"]["audio"][0]["baseUrl"].as_str() {
        audio_url.to_string()
    } else {
        return Err(FetchError::ParseError("audio_url"));
    };

    Ok((video_url, audio_url, title, headers))
}