Skip to main content

bpi_rs/note/
action.rs

1// --- 保存视频笔记 ---
2
3use crate::BilibiliRequest;
4use crate::BpiError;
5use crate::BpiResult;
6use crate::ids::Aid;
7use crate::note::NoteClient;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10
11/// 保存视频笔记的响应数据
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
14pub struct NoteAddResponseData {
15    /// 笔记ID
16    pub note_id: String,
17}
18
19/// Parameters for saving a video note.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct NoteAddParams {
22    oid: Aid,
23    title: String,
24    summary: String,
25    content: String,
26    note_id: Option<String>,
27    tags: Option<String>,
28    publish: Option<bool>,
29    auto_comment: Option<bool>,
30}
31
32/// Parameters for deleting a video note.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct NoteDeleteParams {
35    oid: Aid,
36    note_id: Option<String>,
37}
38
39impl NoteDeleteParams {
40    pub fn new(oid: Aid) -> Self {
41        Self { oid, note_id: None }
42    }
43
44    pub fn note_id(mut self, note_id: impl Into<String>) -> BpiResult<Self> {
45        self.note_id = Some(normalize_non_blank("note_id", note_id.into())?);
46        Ok(self)
47    }
48
49    fn form_pairs(&self, csrf: impl Into<String>) -> Vec<(&'static str, String)> {
50        let mut form = vec![("oid", self.oid.to_string()), ("csrf", csrf.into())];
51
52        if let Some(note_id) = &self.note_id {
53            form.push(("note_id", note_id.clone()));
54        }
55
56        form
57    }
58}
59
60impl NoteAddParams {
61    /// Creates note-save parameters for a video AV ID.
62    pub fn new(
63        oid: Aid,
64        title: impl Into<String>,
65        summary: impl Into<String>,
66        content: impl Into<String>,
67    ) -> BpiResult<Self> {
68        let params = Self {
69            oid,
70            title: title.into(),
71            summary: summary.into(),
72            content: content.into(),
73            note_id: None,
74            tags: None,
75            publish: None,
76            auto_comment: None,
77        };
78        params.validate()?;
79        Ok(params)
80    }
81
82    /// Sets the note ID when updating an existing note.
83    pub fn note_id(mut self, note_id: impl Into<String>) -> BpiResult<Self> {
84        self.note_id = Some(note_id.into());
85        self.validate()?;
86        Ok(self)
87    }
88
89    /// Sets the note jump tags.
90    pub fn tags(mut self, tags: impl Into<String>) -> BpiResult<Self> {
91        self.tags = Some(tags.into());
92        self.validate()?;
93        Ok(self)
94    }
95
96    /// Controls whether the note should be public.
97    pub fn publish(mut self, publish: bool) -> Self {
98        self.publish = Some(publish);
99        self
100    }
101
102    /// Controls whether the note should be added to comments.
103    pub fn auto_comment(mut self, auto_comment: bool) -> Self {
104        self.auto_comment = Some(auto_comment);
105        self
106    }
107
108    fn validate(&self) -> BpiResult<()> {
109        normalize_non_blank("title", self.title.clone())?;
110        normalize_non_blank("summary", self.summary.clone())?;
111        normalize_non_blank("content", self.content.clone())?;
112        if let Some(note_id) = self.note_id.as_deref() {
113            normalize_non_blank("note_id", note_id.to_string())?;
114        }
115        if let Some(tags) = self.tags.as_deref() {
116            normalize_non_blank("tags", tags.to_string())?;
117        }
118        Ok(())
119    }
120
121    fn form_pairs(&self, csrf: impl Into<String>) -> Vec<(&'static str, String)> {
122        let content = json!([{"insert": self.content}]);
123        let mut form = vec![
124            ("oid", self.oid.to_string()),
125            ("oid_type", "0".to_string()),
126            ("title", self.title.clone()),
127            ("summary", self.summary.clone()),
128            ("content", content.to_string()),
129            ("cls", "1".to_string()),
130            ("from", "save".to_string()),
131            ("platform", "web".to_string()),
132            ("csrf", csrf.into()),
133        ];
134
135        if let Some(tags) = self.tags.as_ref() {
136            form.push(("tags", tags.clone()));
137        }
138        if let Some(note_id) = self.note_id.as_ref() {
139            form.push(("note_id", note_id.clone()));
140        }
141        if let Some(publish) = self.publish {
142            form.push(("publish", bool_flag(publish)));
143        }
144        if let Some(auto_comment) = self.auto_comment {
145            form.push(("auto_comment", bool_flag(auto_comment)));
146        }
147
148        form
149    }
150}
151
152fn normalize_non_blank(field: &'static str, value: String) -> BpiResult<String> {
153    let value = value.trim().to_string();
154    if value.is_empty() {
155        return Err(BpiError::invalid_parameter(field, "value cannot be blank"));
156    }
157
158    Ok(value)
159}
160
161fn bool_flag(value: bool) -> String {
162    if value {
163        "1".to_string()
164    } else {
165        "0".to_string()
166    }
167}
168
169// --- 删除视频笔记 ---
170
171impl<'a> NoteClient<'a> {
172    /// Saves a video note and returns the canonical payload result.
173    pub async fn add(&self, params: NoteAddParams) -> BpiResult<NoteAddResponseData> {
174        let csrf = self.client.csrf()?;
175        let form = params.form_pairs(csrf);
176
177        self.client
178            .post("https://api.bilibili.com/x/note/add")
179            .form(&form)
180            .send_bpi_payload("note.add")
181            .await
182    }
183
184    /// Deletes a video note and returns the canonical payload result.
185    pub async fn delete(&self, params: NoteDeleteParams) -> BpiResult<Option<serde_json::Value>> {
186        let csrf = self.client.csrf()?;
187        let form = params.form_pairs(csrf);
188
189        self.client
190            .post("https://api.bilibili.com/x/note/del")
191            .form(&form)
192            .send_bpi_optional_payload("note.delete")
193            .await
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn note_add_params_rejects_blank_content() {
203        let err = NoteAddParams::new(
204            Aid::new(170001).expect("test aid should be valid"),
205            "title",
206            "summary",
207            "  ",
208        )
209        .unwrap_err();
210
211        assert!(matches!(
212            err,
213            BpiError::InvalidParameter {
214                field: "content",
215                ..
216            }
217        ));
218    }
219}