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)
4
5// --- 响应数据结构体 ---
6
7use crate::BilibiliRequest;
8use crate::BpiError;
9use crate::BpiResult;
10use crate::user::UserClient;
11use serde::{Deserialize, Serialize};
12
13/// 用户空间公告响应数据
14
15#[derive(Debug, Clone, Deserialize, Serialize)]
16pub struct SpaceNoticeResponseData(pub String);
17
18/// 修改空间公告响应数据
19#[derive(Debug, Clone, Deserialize, Serialize)]
20pub struct SetSpaceNoticeResponseData;
21
22/// 追番/追剧列表项
23#[derive(Debug, Clone, Deserialize, Serialize)]
24pub struct BangumiFollowItem {
25    pub season_id: i64,
26    pub media_id: i64,
27    pub season_type: i64,
28    pub season_type_name: String,
29    pub title: String,
30    pub cover: String,
31    pub total_count: i64,
32    pub is_finish: i64,
33    pub is_started: i64,
34    pub is_play: i64,
35    pub badge: String,
36    pub badge_type: i64,
37    pub rights: Rights,
38    pub stat: Stat,
39    pub new_ep: NewEp,
40    pub rating: Option<Rating>,
41    pub square_cover: String,
42    pub season_status: i64,
43    pub season_title: String,
44    pub badge_ep: String,
45    pub media_attr: i64,
46    pub season_attr: i64,
47    pub evaluate: String,
48    pub areas: Vec<Area>,
49    pub subtitle: String,
50    pub first_ep: i64,
51    pub can_watch: i64,
52    pub series: Series,
53    pub publish: Publish,
54    pub mode: i64,
55    pub section: Vec<Section>,
56    pub url: String,
57    pub badge_info: BadgeInfo,
58    pub renewal_time: Option<String>,
59    pub first_ep_info: FirstEpInfo,
60    pub formal_ep_count: i64,
61    pub short_url: String,
62    pub badge_infos: BadgeInfos,
63    pub season_version: String,
64    pub subtitle_14: String,
65    pub viewable_crowd_type: i64,
66    pub summary: String,
67    pub styles: Vec<String>,
68    pub follow_status: i64,
69    pub is_new: i64,
70    pub progress: String,
71    pub both_follow: bool,
72}
73
74#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
75pub struct Rights {
76    pub allow_review: i64,
77    pub is_selection: i64,
78    pub selection_style: i64,
79    pub is_rcmd: i64,
80
81    pub demand_end_time: serde_json::Value,
82    pub allow_preview: Option<i64>,
83    pub allow_bp_rank: Option<i64>,
84}
85
86#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct Stat {
88    pub follow: i64,
89    pub view: i64,
90    pub danmaku: i64,
91    pub reply: i64,
92    pub coin: i64,
93    pub series_follow: i64,
94    pub series_view: i64,
95    pub likes: i64,
96    pub favorite: i64,
97}
98
99#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
100pub struct NewEp {
101    pub id: i64,
102    pub index_show: String,
103    pub cover: String,
104    pub title: String,
105    pub long_title: Option<String>,
106    pub pub_time: String,
107    pub duration: i64,
108}
109
110#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
111pub struct Rating {
112    pub score: f64,
113    pub count: i64,
114}
115
116#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct Area {
118    pub id: i64,
119    pub name: String,
120}
121
122#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
123pub struct Series {
124    pub series_id: i64,
125    pub title: String,
126    pub season_count: i64,
127    pub new_season_id: i64,
128    pub series_ord: i64,
129}
130
131#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct Publish {
133    pub pub_time: String,
134    pub pub_time_show: String,
135    pub release_date: String,
136    pub release_date_show: String,
137}
138
139#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Section {
141    pub section_id: i64,
142    pub season_id: i64,
143    pub limit_group: i64,
144    pub watch_platform: i64,
145    pub copyright: String,
146    pub ban_area_show: i64,
147    pub episode_ids: Vec<i64>,
148    #[serde(rename = "type")]
149    pub type_field: Option<i64>,
150    pub title: Option<String>,
151    pub attr: Option<i64>,
152}
153
154#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
155pub struct BadgeInfo {
156    pub text: String,
157    pub bg_color: String,
158    pub bg_color_night: String,
159    pub img: String,
160    pub multi_img: MultiImg,
161}
162
163#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
164pub struct MultiImg {
165    pub color: String,
166    pub medium_remind: String,
167}
168
169#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
170pub struct FirstEpInfo {
171    pub id: i64,
172    pub cover: String,
173    pub title: String,
174    pub long_title: Option<String>,
175    pub pub_time: String,
176    pub duration: i64,
177}
178
179#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct BadgeInfos {
181    pub vip_or_pay: VipOrPay,
182}
183
184#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
185pub struct VipOrPay {
186    pub text: String,
187    pub bg_color: String,
188    pub bg_color_night: String,
189    pub img: String,
190    pub multi_img: MultiImg2,
191}
192
193#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
194pub struct MultiImg2 {
195    pub color: String,
196    pub medium_remind: String,
197}
198
199/// 用户追番/追剧明细响应数据
200#[derive(Debug, Clone, Deserialize, Serialize)]
201pub struct BangumiFollowListResponseData {
202    /// 追番列表
203    pub list: Vec<BangumiFollowItem>,
204    /// 当前页码
205    pub pn: u32,
206    /// 每页项数
207    pub ps: u32,
208    /// 总计追番数
209    pub total: u64,
210}
211
212/// Parameters for setting the user space notice.
213#[derive(Debug, Clone, Default, PartialEq, Eq)]
214pub struct UserSpaceNoticeSetParams {
215    notice: Option<String>,
216}
217
218impl UserSpaceNoticeSetParams {
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    pub fn notice(mut self, notice: impl Into<String>) -> BpiResult<Self> {
224        let notice = notice.into();
225        if notice.len() > 150 {
226            return Err(BpiError::invalid_parameter(
227                "notice",
228                "length cannot exceed 150 bytes",
229            ));
230        }
231        self.notice = Some(notice);
232        Ok(self)
233    }
234
235    fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
236        let mut form = reqwest::multipart::Form::new().text("csrf", csrf.to_string());
237
238        if let Some(notice) = self.notice {
239            form = form.text("notice", notice);
240        }
241
242        form
243    }
244}
245
246// --- API 实现 ---
247
248// --- 测试模块 ---
249
250impl<'a> UserClient<'a> {
251    /// Sets the user space notice and returns the canonical payload result.
252    pub async fn set_space_notice(
253        &self,
254        params: UserSpaceNoticeSetParams,
255    ) -> BpiResult<Option<()>> {
256        let csrf = self.client.csrf()?;
257        let form = params.into_multipart(&csrf);
258
259        self.client
260            .post("https://api.bilibili.com/x/space/notice/set")
261            .multipart(form)
262            .send_bpi_optional_payload("user.space_notice.set")
263            .await
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    use crate::probe::contract::HttpMethod;
272    use crate::probe::endpoint_contract::EndpointContract;
273
274    use crate::{ApiEnvelope, BpiError, BpiResult};
275
276    // 请在运行测试前设置环境变量 `BPI_COOKIE`,以包含 SESSDATA 等登录信息
277    // mid 根据实际情况修改
278
279    fn public_read_contract(endpoint: &str) -> BpiResult<EndpointContract> {
280        let bytes: &[u8] = match endpoint {
281            "bangumi-follow-list" => include_bytes!(
282                "../../tests/contracts/user/public-read/bangumi-follow-list/contract.json"
283            ),
284            "space-notice" => {
285                include_bytes!("../../tests/contracts/user/public-read/space-notice/contract.json")
286            }
287            _ => {
288                return Err(BpiError::invalid_parameter(
289                    "endpoint",
290                    "unknown user space contract",
291                ));
292            }
293        };
294
295        EndpointContract::from_slice(bytes)
296    }
297
298    #[test]
299    fn legacy_user_space_contracts_match_endpoint_requests() -> BpiResult<()> {
300        let notice = public_read_contract("space-notice")?;
301        assert_eq!(notice.name, "user.space_notice");
302        assert_eq!(notice.request.method, HttpMethod::Get);
303        assert_eq!(
304            notice.request.url.as_str(),
305            "https://api.bilibili.com/x/space/notice"
306        );
307        assert_eq!(
308            notice.request.query.get("mid").map(String::as_str),
309            Some("2")
310        );
311
312        let bangumi = public_read_contract("bangumi-follow-list")?;
313        assert_eq!(bangumi.name, "user.bangumi_follow_list");
314        assert_eq!(
315            bangumi.request.url.as_str(),
316            "https://api.bilibili.com/x/space/bangumi/follow/list"
317        );
318        assert_eq!(
319            bangumi.request.query.get("vmid").map(String::as_str),
320            Some("4279370")
321        );
322        assert_eq!(
323            bangumi.request.query.get("type").map(String::as_str),
324            Some("1")
325        );
326        Ok(())
327    }
328
329    #[test]
330    fn legacy_user_space_fixtures_parse_promoted_contract_models() -> BpiResult<()> {
331        let notice = ApiEnvelope::<SpaceNoticeResponseData>::from_slice(include_bytes!(
332            "../../tests/contracts/user/public-read/space-notice/responses/success.json"
333        ))?
334        .into_payload()?;
335        let _notice_text = notice.0;
336
337        let follow_list =
338            ApiEnvelope::<BangumiFollowListResponseData>::from_slice(include_bytes!(
339                "../../tests/contracts/user/public-read/bangumi-follow-list/responses/success.json"
340            ))?
341            .into_payload()?;
342        assert_eq!(follow_list.pn, 1);
343        Ok(())
344    }
345}