tiktok_api/endpoints/v2/
video_upload_init.rs

1use http_api_client_endpoint::{
2    http::{
3        header::{ACCEPT, AUTHORIZATION, USER_AGENT},
4        Method,
5    },
6    Body, Endpoint, Request, Response,
7};
8use serde::{Deserialize, Serialize};
9use url::Url;
10
11use super::common::{endpoint_parse_response, EndpointError, EndpointRet};
12use crate::objects::v2::Error;
13
14//
15pub const URL: &str = "https://open.tiktokapis.com/v2/post/publish/inbox/video/init/";
16
17//
18#[derive(Debug, Clone)]
19pub struct VideoUploadInitEndpoint {
20    pub access_token: String,
21    pub source_info: VideoUploadInitRequestBodySourceInfo,
22}
23impl VideoUploadInitEndpoint {
24    pub fn new(
25        access_token: impl AsRef<str>,
26        source_info: VideoUploadInitRequestBodySourceInfo,
27    ) -> Self {
28        Self {
29            access_token: access_token.as_ref().into(),
30            source_info,
31        }
32    }
33
34    #[cfg(feature = "with_tokio_fs")]
35    pub async fn with_file(
36        access_token: impl AsRef<str>,
37        file_path: &std::path::PathBuf,
38        chunk_size: Option<usize>,
39    ) -> Result<Self, EndpointError> {
40        use crate::media_transfer::{get_chunk_size_and_total_chunk_count, CHUNK_SIZE_MAX};
41
42        let crate::tokio_fs_util::Info {
43            file_size,
44            file_name: _,
45        } = crate::tokio_fs_util::info(file_path)
46            .await
47            .map_err(EndpointError::GetFileInfoFailed)?;
48
49        let video_size = file_size as usize;
50        let (chunk_size, total_chunk_count) =
51            get_chunk_size_and_total_chunk_count(video_size, chunk_size.unwrap_or(CHUNK_SIZE_MAX));
52
53        //
54        let source_info = VideoUploadInitRequestBodySourceInfo::FileUpload {
55            video_size,
56            chunk_size,
57            total_chunk_count,
58        };
59
60        Ok(Self {
61            access_token: access_token.as_ref().into(),
62            source_info,
63        })
64    }
65}
66
67impl Endpoint for VideoUploadInitEndpoint {
68    type RenderRequestError = EndpointError;
69
70    type ParseResponseOutput = EndpointRet<VideoUploadInitResponseBody>;
71    type ParseResponseError = EndpointError;
72
73    fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
74        let request_body = VideoUploadInitRequestBody {
75            source_info: self.source_info.to_owned(),
76        };
77        let request_body =
78            serde_json::to_vec(&request_body).map_err(EndpointError::SerRequestBodyFailed)?;
79
80        let request = Request::builder()
81            .method(Method::POST)
82            .uri(URL)
83            .header(AUTHORIZATION, format!("Bearer {}", &self.access_token))
84            .header(USER_AGENT, "tiktok-api")
85            .header(ACCEPT, "application/json; charset=UTF-8")
86            .body(request_body)
87            .map_err(EndpointError::MakeRequestFailed)?;
88
89        Ok(request)
90    }
91
92    fn parse_response(
93        &self,
94        response: Response<Body>,
95    ) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
96        endpoint_parse_response(response)
97    }
98}
99
100//
101#[derive(Serialize, Deserialize, Debug, Clone)]
102pub struct VideoUploadInitRequestBody {
103    pub source_info: VideoUploadInitRequestBodySourceInfo,
104}
105
106#[derive(Serialize, Deserialize, Debug, Clone)]
107#[serde(tag = "source")]
108pub enum VideoUploadInitRequestBodySourceInfo {
109    #[serde(rename = "FILE_UPLOAD")]
110    FileUpload {
111        video_size: usize,
112        chunk_size: usize,
113        total_chunk_count: usize,
114    },
115    #[serde(rename = "PULL_FROM_URL")]
116    PullFromUrl { video_url: Url },
117}
118
119#[derive(Serialize, Deserialize, Debug, Clone)]
120pub struct VideoUploadInitResponseBody {
121    pub data: VideoUploadInitResponseBodyData,
122    pub error: Error,
123}
124
125#[derive(Serialize, Deserialize, Debug, Clone)]
126pub struct VideoUploadInitResponseBodyData {
127    pub publish_id: String,
128    pub upload_url: Option<Url>,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    use crate::objects::v2::ErrorCode;
136
137    #[test]
138    fn test_render_request() {
139        let req = VideoUploadInitEndpoint::new(
140            "TOKEN",
141            VideoUploadInitRequestBodySourceInfo::FileUpload {
142                video_size: 30567100,
143                chunk_size: 30567100,
144                total_chunk_count: 1,
145            },
146        )
147        .render_request()
148        .unwrap();
149        assert_eq!(req.method(), Method::POST);
150        assert_eq!(req.uri(), URL);
151        assert_eq!(
152            serde_json::from_slice::<serde_json::Value>(req.body()).unwrap(),
153            serde_json::json!({
154                "source_info": {
155                    "source": "FILE_UPLOAD",
156                    "video_size": 30567100,
157                    "chunk_size" : 30567100,
158                    "total_chunk_count": 1
159                }
160            })
161        );
162
163        let req = VideoUploadInitEndpoint::new(
164            "TOKEN",
165            VideoUploadInitRequestBodySourceInfo::PullFromUrl {
166                video_url: "https://example.verified.domain.com/example_video.mp4"
167                    .parse()
168                    .unwrap(),
169            },
170        )
171        .render_request()
172        .unwrap();
173        assert_eq!(req.method(), Method::POST);
174        assert_eq!(req.uri(), URL);
175        assert_eq!(
176            serde_json::from_slice::<serde_json::Value>(req.body()).unwrap(),
177            serde_json::json!({
178                "source_info": {
179                    "source": "PULL_FROM_URL",
180                    "video_url": "https://example.verified.domain.com/example_video.mp4",
181                }
182            })
183        );
184    }
185
186    #[test]
187    fn test_de_response_body() {
188        match serde_json::from_str::<VideoUploadInitResponseBody>(include_str!(
189            "../../../tests/response_body_files/v2/video_upload_init.json"
190        )) {
191            Ok(ok_json) => {
192                assert_eq!(ok_json.data.publish_id, "v_inbox_file~v2.123456789");
193                assert_eq!(
194                    ok_json.data.upload_url,
195                    Some("https://open-upload.tiktokapis.com/video/?upload_id=67890&upload_token=Xza123".parse().unwrap())
196                );
197                assert_eq!(ok_json.error.code, ErrorCode::Ok);
198            }
199            x => panic!("{x:?}"),
200        }
201
202        //
203        match serde_json::from_str::<VideoUploadInitResponseBody>(include_str!(
204            "../../../tests/response_body_files/v2/video_upload_init__without_upload_url.json"
205        )) {
206            Ok(ok_json) => {
207                assert_eq!(ok_json.data.publish_id, "v_inbox_file~v2.123456789");
208                assert!(ok_json.data.upload_url.is_none());
209                assert_eq!(ok_json.error.code, ErrorCode::Ok);
210            }
211            x => panic!("{x:?}"),
212        }
213    }
214}