Skip to main content

bpi_rs/user/
space.rs

1//! B站用户空间相关接口
2//!
3//! [查看 API 文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
4use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
5use serde::{ Deserialize, Serialize };
6
7// --- 响应数据结构体 ---
8
9/// 用户空间公告响应数据
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct SpaceNoticeResponseData(pub String);
12
13/// 修改空间公告响应数据
14#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct SetSpaceNoticeResponseData;
16
17/// 追番/追剧列表项
18#[derive(Debug, Clone, Deserialize, Serialize)]
19pub struct BangumiFollowItem {
20    pub season_id: i64,
21    pub media_id: i64,
22    pub season_type: i64,
23    pub season_type_name: String,
24    pub title: String,
25    pub cover: String,
26    pub total_count: i64,
27    pub is_finish: i64,
28    pub is_started: i64,
29    pub is_play: i64,
30    pub badge: String,
31    pub badge_type: i64,
32    pub rights: Rights,
33    pub stat: Stat,
34    pub new_ep: NewEp,
35    pub rating: Option<Rating>,
36    pub square_cover: String,
37    pub season_status: i64,
38    pub season_title: String,
39    pub badge_ep: String,
40    pub media_attr: i64,
41    pub season_attr: i64,
42    pub evaluate: String,
43    pub areas: Vec<Area>,
44    pub subtitle: String,
45    pub first_ep: i64,
46    pub can_watch: i64,
47    pub series: Series,
48    pub publish: Publish,
49    pub mode: i64,
50    pub section: Vec<Section>,
51    pub url: String,
52    pub badge_info: BadgeInfo,
53    pub renewal_time: Option<String>,
54    pub first_ep_info: FirstEpInfo,
55    pub formal_ep_count: i64,
56    pub short_url: String,
57    pub badge_infos: BadgeInfos,
58    pub season_version: String,
59    pub subtitle_14: String,
60    pub viewable_crowd_type: i64,
61    pub summary: String,
62    pub styles: Vec<String>,
63    pub follow_status: i64,
64    pub is_new: i64,
65    pub progress: String,
66    pub both_follow: bool,
67}
68
69#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
70pub struct Rights {
71    pub allow_review: i64,
72    pub is_selection: i64,
73    pub selection_style: i64,
74    pub is_rcmd: i64,
75
76    pub demand_end_time: serde_json::Value,
77    pub allow_preview: Option<i64>,
78    pub allow_bp_rank: Option<i64>,
79}
80
81#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
82pub struct Stat {
83    pub follow: i64,
84    pub view: i64,
85    pub danmaku: i64,
86    pub reply: i64,
87    pub coin: i64,
88    pub series_follow: i64,
89    pub series_view: i64,
90    pub likes: i64,
91    pub favorite: i64,
92}
93
94#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct NewEp {
96    pub id: i64,
97    pub index_show: String,
98    pub cover: String,
99    pub title: String,
100    pub long_title: Option<String>,
101    pub pub_time: String,
102    pub duration: i64,
103}
104
105#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct Rating {
107    pub score: f64,
108    pub count: i64,
109}
110
111#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct Area {
113    pub id: i64,
114    pub name: String,
115}
116
117#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
118pub struct Series {
119    pub series_id: i64,
120    pub title: String,
121    pub season_count: i64,
122    pub new_season_id: i64,
123    pub series_ord: i64,
124}
125
126#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct Publish {
128    pub pub_time: String,
129    pub pub_time_show: String,
130    pub release_date: String,
131    pub release_date_show: String,
132}
133
134#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct Section {
136    pub section_id: i64,
137    pub season_id: i64,
138    pub limit_group: i64,
139    pub watch_platform: i64,
140    pub copyright: String,
141    pub ban_area_show: i64,
142    pub episode_ids: Vec<i64>,
143    #[serde(rename = "type")]
144    pub type_field: Option<i64>,
145    pub title: Option<String>,
146    pub attr: Option<i64>,
147}
148
149#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
150pub struct BadgeInfo {
151    pub text: String,
152    pub bg_color: String,
153    pub bg_color_night: String,
154    pub img: String,
155    pub multi_img: MultiImg,
156}
157
158#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
159pub struct MultiImg {
160    pub color: String,
161    pub medium_remind: String,
162}
163
164#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
165pub struct FirstEpInfo {
166    pub id: i64,
167    pub cover: String,
168    pub title: String,
169    pub long_title: Option<String>,
170    pub pub_time: String,
171    pub duration: i64,
172}
173
174#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
175pub struct BadgeInfos {
176    pub vip_or_pay: VipOrPay,
177}
178
179#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct VipOrPay {
181    pub text: String,
182    pub bg_color: String,
183    pub bg_color_night: String,
184    pub img: String,
185    pub multi_img: MultiImg2,
186}
187
188#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct MultiImg2 {
190    pub color: String,
191    pub medium_remind: String,
192}
193
194/// 用户追番/追剧明细响应数据
195#[derive(Debug, Clone, Deserialize, Serialize)]
196pub struct BangumiFollowListResponseData {
197    /// 追番列表
198    pub list: Vec<BangumiFollowItem>,
199    /// 当前页码
200    pub pn: u32,
201    /// 每页项数
202    pub ps: u32,
203    /// 总计追番数
204    pub total: u64,
205}
206
207// --- API 实现 ---
208
209impl BpiClient {
210    /// 获取用户空间公告
211    ///
212    /// # 文档
213    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
214    ///
215    /// # 参数
216    /// | 名称   | 类型   | 说明           |
217    /// | ------ | ------ | -------------- |
218    /// | `mid`  | u64    | 目标用户 mid   |
219    pub async fn user_space_notice(
220        &self,
221        mid: u64
222    ) -> Result<BpiResponse<SpaceNoticeResponseData>, BpiError> {
223        self
224            .get("https://api.bilibili.com/x/space/notice")
225            .query(&[("mid", &mid.to_string())])
226            .send_bpi("查看用户空间公告").await
227    }
228
229    /// 修改空间公告
230    ///
231    /// # 文档
232    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
233    ///
234    /// # 参数
235    /// | 名称    | 类型           | 说明                 |
236    /// | ------- | --------------| -------------------- |
237    /// | `notice`| `Option<&str>`  | 公告内容,少于150字  |
238    pub async fn user_space_notice_set(
239        &self,
240        notice: Option<&str>
241    ) -> Result<BpiResponse<()>, BpiError> {
242        let csrf = self.csrf()?;
243        let mut form = reqwest::multipart::Form::new().text("csrf", csrf.to_string());
244
245        if let Some(n) = notice {
246            if n.len() > 150 {
247                return Err(BpiError::parse("公告内容超出150字符限制"));
248            }
249            form = form.text("notice", n.to_string());
250        }
251
252        self
253            .post("https://api.bilibili.com/x/space/notice/set")
254            .multipart(form)
255            .send_bpi("修改空间公告").await
256    }
257
258    /// 查询用户追番/追剧明细
259    ///
260    /// # 文档
261    /// [查看API文档](https://github.com/SocialSisterYi/bilibili-API-collect/tree/master/docs/user)
262    ///
263    /// # 参数
264    /// | 名称      | 类型           | 说明                 |
265    /// | --------- | --------------| -------------------- |
266    /// | `mid`     | u64           | 目标用户 mid         |
267    /// | `pn`      | `Option<u32>`   | 页码,默认1          |
268    /// | `ps`      | `Option<u32>`   | 每页项数,默认15     |
269    /// | `list_type`| u8           | 查询类型 1:追番 2:追剧 |
270    pub async fn user_bangumi_follow_list(
271        &self,
272        mid: u64,
273        pn: Option<u32>,
274        ps: Option<u32>,
275        list_type: u8
276    ) -> Result<BpiResponse<BangumiFollowListResponseData>, BpiError> {
277        let pn_val = pn.unwrap_or(1);
278        let ps_val = ps.unwrap_or(15);
279
280        if ps_val > 30 || ps_val < 1 {
281            return Err(BpiError::parse("ps 参数超出有效范围 [1, 30]"));
282        }
283
284        let mut req = self.get("https://api.bilibili.com/x/space/bangumi/follow/list").query(
285            &[
286                ("vmid", &mid.to_string()),
287                ("type", &list_type.to_string()),
288            ]
289        );
290
291        if pn.is_some() {
292            req = req.query(&[("pn", &pn_val.to_string())]);
293        }
294        if ps.is_some() {
295            req = req.query(&[("ps", &ps_val.to_string())]);
296        }
297
298        req.send_bpi("查询用户追番/追剧明细").await
299    }
300}
301
302// --- 测试模块 ---
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307    use tracing::info;
308
309    // 请在运行测试前设置环境变量 `BPI_COOKIE`,以包含 SESSDATA 等登录信息
310    // mid 根据实际情况修改
311
312    const TEST_MID: u64 = 4279370;
313
314    #[tokio::test]
315    async fn test_user_space_notice() -> Result<(), BpiError> {
316        let bpi = BpiClient::new();
317        let resp = bpi.user_space_notice(TEST_MID).await?;
318        let data = resp.into_data()?;
319
320        info!("空间公告: {:?}", data);
321
322        Ok(())
323    }
324
325    #[tokio::test]
326    async fn test_user_space_notice_set() -> Result<(), BpiError> {
327        let bpi = BpiClient::new();
328        let notice = "这是一个通过 API 设置的测试公告。";
329        let resp = bpi.user_space_notice_set(Some(notice)).await?;
330
331        info!("设置空间公告结果: {:?}", resp);
332
333        // 验证设置后内容
334        let get_resp = bpi.user_space_notice(TEST_MID).await?;
335        let get_data = get_resp.into_data()?;
336        assert_eq!(get_data.0, notice);
337        info!("验证公告内容成功");
338
339        // 删除公告
340        let delete_resp = bpi.user_space_notice_set(None).await?;
341        info!("删除空间公告结果: {:?}", delete_resp);
342        assert_eq!(delete_resp.code, 0);
343
344        // 验证删除后内容
345        let get_resp_after_delete = bpi.user_space_notice(TEST_MID).await?;
346        let get_data_after_delete = get_resp_after_delete.into_data()?;
347        assert!(get_data_after_delete.0.is_empty());
348        info!("验证删除公告内容成功");
349
350        Ok(())
351    }
352
353    #[tokio::test]
354    async fn test_user_bangumi_follow_list() -> Result<(), BpiError> {
355        let bpi = BpiClient::new();
356        // 1: 追番, 2: 追剧
357        let resp = bpi.user_bangumi_follow_list(TEST_MID, Some(1), Some(15), 1).await?;
358        let data = resp.into_data()?;
359
360        info!("追番列表: {:?}", data);
361        assert_eq!(data.pn, 1);
362        assert_eq!(data.ps, 15);
363        assert!(!data.list.is_empty());
364
365        Ok(())
366    }
367}