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(
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 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 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#[cfg(test)]
305mod tests {
306 use super::*;
307 use tracing::info;
308
309 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 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 let delete_resp = bpi.user_space_notice_set(None).await?;
341 info!("删除空间公告结果: {:?}", delete_resp);
342 assert_eq!(delete_resp.code, 0);
343
344 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 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}