1use crate::{ BilibiliRequest, BpiClient, BpiError, BpiResponse };
6use serde::{ Deserialize, Serialize };
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct CourseInfo {
14 pub brief: CourseBrief,
15 pub coupon: CourseCoupon,
16 pub cover: String,
17 pub episode_page: CourseEpisodePage,
18 pub episode_sort: i32,
19 pub episodes: Vec<CourseEpisode>,
20 pub faq: CourseFaq,
21 pub faq1: CourseFaq1,
22 pub payment: CoursePayment,
23 pub purchase_note: CoursePurchaseNote,
24 pub purchase_protocol: CoursePurchaseProtocol,
25 pub release_bottom_info: String,
26 pub release_info: String,
27 pub release_info2: String,
28 pub release_status: String,
29 pub season_id: u64,
30 pub share_url: String,
31 pub short_link: String,
32 pub stat: CourseStat,
33 pub status: i32,
34 pub subtitle: String,
35 pub title: String,
36 pub up_info: CourseUpInfo,
37 pub user_status: CourseUserStatus,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CourseBrief {
42 pub content: String,
43 pub img: Vec<CourseBriefImg>,
44 pub title: String,
45 pub r#type: i32,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct CourseBriefImg {
50 pub aspect_ratio: f64,
51 pub url: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct CourseCoupon {
56 pub amount: f64,
57 pub expire_time: String, pub start_time: String, pub status: i32,
60 pub title: String,
61 pub token: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CourseEpisodePage {
66 pub next: bool,
67 pub num: u32,
68 pub size: u32,
69 pub total: u32,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct CourseEpisode {
74 pub aid: u64, pub cid: u64, pub duration: u64, pub from: String, pub id: u64, pub index: u32, pub page: u32, pub play: u64, pub release_date: u64, pub status: i32, pub title: String, pub watched: bool, #[serde(rename = "watchedHistory")] pub watched_history: u64,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct CourseFaq {
92 pub content: String,
93 pub link: String,
94 pub title: String,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct CourseFaq1 {
99 pub items: Vec<CourseFaqItem>,
100 pub title: String,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct CourseFaqItem {
105 pub answer: String,
106 pub question: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct CoursePayment {
111 pub desc: String,
112 pub discount_desc: String,
113 pub discount_prefix: String,
114 pub pay_shade: String,
115 pub price: f64,
116 pub price_format: String,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct CoursePurchaseNote {
121 pub content: String,
122 pub link: String,
123 pub title: String,
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct CoursePurchaseProtocol {
128 pub link: String,
129 pub title: String,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CourseStat {
134 pub play: u64,
135 pub play_desc: String,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct CourseUpInfo {
140 pub avatar: String,
141 pub brief: String,
142 pub follower: u64,
143 pub is_follow: i32, pub link: String,
145 pub mid: u64,
146 pub pendant: CoursePendant,
147 pub uname: String,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct CoursePendant {
152 pub image: String,
153 pub name: String,
154 pub pid: u64,
155 }
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
159pub struct CourseUserStatus {
160 pub favored: i32, pub favored_count: u64,
162 pub payed: i32, pub progress: CourseProgress,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct CourseProgress {
168 pub last_ep_id: u64,
169 pub last_ep_index: String,
170 pub last_time: u64, }
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct CourseEpList {
179 pub items: Vec<CourseEpisode>, pub page: CourseEpPage,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct CourseEpPage {
185 pub next: bool, pub num: u32, pub size: u32, pub total: u32, }
190
191impl BpiClient {
196 pub async fn cheese_info(
213 &self,
214 season_id: Option<u64>,
215 ep_id: Option<u64>
216 ) -> Result<BpiResponse<CourseInfo>, BpiError> {
217 if season_id.is_none() && ep_id.is_none() {
218 return Err(
219 BpiError::parse("cheese_info: season_id 与 ep_id 必须至少提供一个".to_string())
220 );
221 }
222 let mut req = self
224 .get("https://api.bilibili.com/pugv/view/web/season")
225 .with_bilibili_headers();
226
227 if let Some(sid) = season_id {
228 req = req.query(&[("season_id", sid)]);
229 }
230 if let Some(eid) = ep_id {
231 req = req.query(&[("ep_id", eid)]);
232 }
233
234 req.send_bpi("获取课程基本信息").await
235 }
236
237 pub async fn cheese_info_by_season_id(
244 &self,
245 season_id: u64
246 ) -> Result<BpiResponse<CourseInfo>, BpiError> {
247 self.cheese_info(Some(season_id), None).await
248 }
249
250 pub async fn cheese_info_by_ep_id(
257 &self,
258 ep_id: u64
259 ) -> Result<BpiResponse<CourseInfo>, BpiError> {
260 self.cheese_info(None, Some(ep_id)).await
261 }
262
263 pub async fn cheese_ep_list(
277 &self,
278 season_id: u64,
279 ps: Option<u32>,
280 pn: Option<u32>
281 ) -> Result<BpiResponse<CourseEpList>, BpiError> {
282 let mut req = self
283 .get("https://api.bilibili.com/pugv/view/web/ep/list")
284 .query(&[("season_id", season_id)]);
285
286 if let Some(ps) = ps {
287 req = req.query(&[("ps", ps)]);
288 }
289 if let Some(pn) = pn {
290 req = req.query(&[("pn", pn)]);
291 }
292
293 req.send_bpi("获取课程分集列表").await
294 }
295}
296
297#[cfg(test)]
302mod tests {
303 use super::*;
304
305 const TEST_SEASON_ID: u64 = 556;
306 const TEST_EP_ID: u64 = 20767;
307
308 #[tokio::test]
309 async fn test_cheese_info_by_season_id() -> Result<(), Box<BpiError>> {
310 let bpi = BpiClient::new();
311 let data = bpi.cheese_info_by_season_id(TEST_SEASON_ID).await?.into_data()?;
312
313 assert_eq!(data.season_id, TEST_SEASON_ID);
314 tracing::info!("{:#?}", data);
315 Ok(())
316 }
317
318 #[tokio::test]
319 async fn test_cheese_info_by_ep_id() -> Result<(), Box<BpiError>> {
320 let bpi = BpiClient::new();
321 let data = bpi.cheese_info_by_ep_id(TEST_EP_ID).await?.into_data()?;
322 assert_eq!(data.season_id, TEST_SEASON_ID);
323
324 tracing::info!("课程标题: {:?}", data.title);
325 tracing::info!("课程 ssid: {:?}", data.season_id);
326 Ok(())
327 }
328
329 #[tokio::test]
330 async fn test_cheese_ep_list() -> Result<(), Box<BpiError>> {
331 let bpi = BpiClient::new();
332 let data = bpi.cheese_ep_list(TEST_SEASON_ID, Some(50), Some(1)).await?.into_data()?;
333 assert_eq!(data.items.first().unwrap().id, TEST_SEASON_ID);
334
335 tracing::info!("课程标题: {:?}", data.items.first().unwrap().title);
336 tracing::info!("课程 ssid: {:?}", data.items.first().unwrap());
337 Ok(())
338 }
339}