1use crate::BilibiliRequest;
8use crate::BpiError;
9use crate::response::BpiResult;
10use crate::video::VideoClient;
11
12const WATCH_PROGRESS_ENDPOINT: &str = "https://api.bilibili.com/x/v2/history/report";
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct VideoWatchProgressParams {
17 aid: u64,
18 cid: u64,
19 progress: u64,
20}
21
22impl VideoWatchProgressParams {
23 pub fn new(aid: u64, cid: u64) -> BpiResult<Self> {
24 if aid == 0 {
25 return Err(BpiError::invalid_parameter("aid", "id must be non-zero"));
26 }
27 if cid == 0 {
28 return Err(BpiError::invalid_parameter("cid", "id must be non-zero"));
29 }
30
31 Ok(Self {
32 aid,
33 cid,
34 progress: 0,
35 })
36 }
37
38 pub fn progress(mut self, progress: u64) -> Self {
39 self.progress = progress;
40 self
41 }
42
43 fn into_multipart(self, csrf: &str) -> reqwest::multipart::Form {
44 reqwest::multipart::Form::new()
45 .text("aid", self.aid.to_string())
46 .text("cid", self.cid.to_string())
47 .text("csrf", csrf.to_string())
48 .text("progress", self.progress.to_string())
49 }
50}
51
52impl<'a> VideoClient<'a> {
53 pub async fn report_watch_progress(
55 &self,
56 params: VideoWatchProgressParams,
57 ) -> BpiResult<Option<serde_json::Value>> {
58 let csrf = self.client.csrf()?;
59 let form = params.into_multipart(&csrf);
60
61 self.client
62 .post(WATCH_PROGRESS_ENDPOINT)
63 .multipart(form)
64 .send_bpi_optional_payload("video.watch_progress.report")
65 .await
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn watch_progress_params_rejects_zero_aid() {
75 let err = VideoWatchProgressParams::new(0, 100).unwrap_err();
76
77 assert!(matches!(
78 err,
79 BpiError::InvalidParameter { field: "aid", .. }
80 ));
81 }
82}