1use crate::{BilibiliRequest, BpiClient, BpiError, BpiResponse};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct SpaceNoticeResponseData(pub String);
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct SetSpaceNoticeResponseData;
16
17#[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#[derive(Debug, Clone, Deserialize, Serialize)]
196pub struct BangumiFollowListResponseData {
197 pub list: Vec<BangumiFollowItem>,
199 pub pn: u32,
201 pub ps: u32,
203 pub total: u64,
205}
206
207impl BpiClient {
210 pub async fn user_space_notice(
219 &self,
220 mid: u64,
221 ) -> Result<BpiResponse<SpaceNoticeResponseData>, BpiError> {
222 self.get("https://api.bilibili.com/x/space/notice")
223 .query(&[("mid", &mid.to_string())])
224 .send_bpi("查看用户空间公告")
225 .await
226 }
227
228 pub async fn user_space_notice_set(
237 &self,
238 notice: Option<&str>,
239 ) -> Result<BpiResponse<()>, BpiError> {
240 let csrf = self.csrf()?;
241 let mut form = reqwest::multipart::Form::new().text("csrf", csrf.to_string());
242
243 if let Some(n) = notice {
244 if n.len() > 150 {
245 return Err(BpiError::parse("公告内容超出150字符限制"));
246 }
247 form = form.text("notice", n.to_string());
248 }
249
250 self.post("https://api.bilibili.com/x/space/notice/set")
251 .multipart(form)
252 .send_bpi("修改空间公告")
253 .await
254 }
255
256 pub async fn user_bangumi_follow_list(
268 &self,
269 mid: u64,
270 pn: Option<u32>,
271 ps: Option<u32>,
272 list_type: u8,
273 ) -> Result<BpiResponse<BangumiFollowListResponseData>, BpiError> {
274 let pn_val = pn.unwrap_or(1);
275 let ps_val = ps.unwrap_or(15);
276
277 if ps_val > 30 || ps_val < 1 {
278 return Err(BpiError::parse("ps 参数超出有效范围 [1, 30]"));
279 }
280
281 let mut req = self
282 .get("https://api.bilibili.com/x/space/bangumi/follow/list")
283 .query(&[("vmid", &mid.to_string()), ("type", &list_type.to_string())]);
284
285 if pn.is_some() {
286 req = req.query(&[("pn", &pn_val.to_string())]);
287 }
288 if ps.is_some() {
289 req = req.query(&[("ps", &ps_val.to_string())]);
290 }
291
292 req.send_bpi("查询用户追番/追剧明细").await
293 }
294}
295
296#[cfg(test)]
299mod tests {
300 use super::*;
301 use tracing::info;
302
303 const TEST_MID: u64 = 4279370;
307
308 #[tokio::test]
309 async fn test_user_space_notice() -> Result<(), BpiError> {
310 let bpi = BpiClient::new();
311 let resp = bpi.user_space_notice(TEST_MID).await?;
312 let data = resp.into_data()?;
313
314 info!("空间公告: {:?}", data);
315
316 Ok(())
317 }
318
319 #[tokio::test]
320
321 async fn test_user_space_notice_set() -> Result<(), BpiError> {
322 let bpi = BpiClient::new();
323 let notice = "这是一个通过 API 设置的测试公告。";
324 let resp = bpi.user_space_notice_set(Some(notice)).await?;
325
326 info!("设置空间公告结果: {:?}", resp);
327
328 let get_resp = bpi.user_space_notice(TEST_MID).await?;
330 let get_data = get_resp.into_data()?;
331 assert_eq!(get_data.0, notice);
332 info!("验证公告内容成功");
333
334 let delete_resp = bpi.user_space_notice_set(None).await?;
336 info!("删除空间公告结果: {:?}", delete_resp);
337 assert_eq!(delete_resp.code, 0);
338
339 let get_resp_after_delete = bpi.user_space_notice(TEST_MID).await?;
341 let get_data_after_delete = get_resp_after_delete.into_data()?;
342 assert!(get_data_after_delete.0.is_empty());
343 info!("验证删除公告内容成功");
344
345 Ok(())
346 }
347
348 #[tokio::test]
349
350 async fn test_user_bangumi_follow_list() -> Result<(), BpiError> {
351 let bpi = BpiClient::new();
352 let resp = bpi
354 .user_bangumi_follow_list(TEST_MID, Some(1), Some(15), 1)
355 .await?;
356 let data = resp.into_data()?;
357
358 info!("追番列表: {:?}", data);
359 assert_eq!(data.pn, 1);
360 assert_eq!(data.ps, 15);
361 assert!(!data.list.is_empty());
362
363 Ok(())
364 }
365}