Skip to main content

bpi_rs/creativecenter/
upload.rs

1//! 创作中心上传 API
2//!
3//! [参考文档](https://github.com/Yuelioi/bilibili-API-collect/tree/cfc5fddcc8a94b74d91970bb5b4eaeb349addc47/docs/creativecenter/upload.md)
4
5use std::collections::HashMap;
6
7use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
8
9use base64::{ Engine as _, engine::general_purpose };
10use serde::{ Deserialize, Serialize };
11use std::fs;
12
13/// 上传封面返回结果
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct UploadCoverData {
16    pub url: String,
17}
18
19impl BpiClient {
20    /// 上传视频封面
21    ///
22    /// 上传视频封面图片,支持多种输入格式。
23    ///
24    /// # 参数
25    /// | 名称 | 类型 | 说明 |
26    /// | ---- | ---- | ---- |
27    /// | `mime_type` | &str | 图片 MIME 类型,如 image/jpeg |
28    /// | `cover` | `AsRef<str>` | 封面数据,可以是:纯 base64、完整 data URI、文件路径 |
29    ///
30    /// # 注意
31    /// 文件不得超过 20M
32    ///
33    /// # 文档
34    /// [上传视频封面](https://github.com/Yuelioi/bilibili-API-collect/tree/cfc5fddcc8a94b74d91970bb5b4eaeb349addc47/docs/creativecenter/upload.md#上传视频封面)
35    pub async fn upload_cover(
36        &self,
37        mime_type: &str,
38        cover: impl AsRef<str>
39    ) -> Result<BpiResponse<UploadCoverData>, BpiError> {
40        let csrf = self.csrf()?;
41        let cover_str = cover.as_ref();
42
43        let final_cover = if cover_str.starts_with("data:") {
44            cover_str.to_string()
45        } else if
46            cover_str
47                .chars()
48                .all(|c| (c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '='))
49        {
50            format!("data:{};base64,{}", mime_type, cover_str)
51        } else {
52            let file_bytes = fs::read(cover_str).map_err(|e| BpiError::Parse {
53                message: format!("读取文件失败: {}", e),
54            })?;
55            let file_base64 = general_purpose::STANDARD.encode(file_bytes);
56            format!("data:{};base64,{}", mime_type, file_base64)
57        };
58
59        let mut form = HashMap::new();
60        form.insert("csrf", csrf);
61        form.insert("cover", final_cover);
62
63        self
64            .post("https://member.bilibili.com/x/vu/web/cover/up")
65            .form(&form)
66            .send_bpi("上传视频封面").await
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    #[allow(unused_imports)]
74    use base64::{ Engine as _, engine::general_purpose };
75    use std::fs;
76
77    async fn run_upload_test(
78        bpi: &BpiClient,
79        mime_type: &str,
80        cover: &str
81    ) -> Result<(), Box<BpiError>> {
82        let data = bpi.upload_cover(mime_type, cover).await?.into_data()?;
83        tracing::info!("上传成功,封面地址: {}", data.url);
84        Ok(())
85    }
86
87    #[tokio::test]
88    async fn test_cover_upload() -> Result<(), Box<BpiError>> {
89        let bpi = BpiClient::new();
90
91        // 1. 文件路径
92        // run_upload_test(&bpi, "image/jpeg", "./assets/test.jpg").await?;
93
94        // 2. 纯 Base64
95        // let img_data = fs::read("./assets/test.jpg")?;
96        // let img_base64 = general_purpose::STANDARD.encode(&img_data);
97        // run_upload_test(&bpi, "image/jpeg", &img_base64).await?;
98
99        // 3. 完整 Data URI
100        let img_data = fs::read("./assets/test.jpg").map_err(|_| BpiError::parse("读取图片失败"))?;
101        let img_base64 = general_purpose::STANDARD.encode(&img_data);
102        let img_data_uri = format!("data:image/jpeg;base64,{}", img_base64);
103        run_upload_test(&bpi, "image/jpeg", &img_data_uri).await?;
104
105        Ok(())
106    }
107}