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
14pub const URL: &str = "https://open.tiktokapis.com/v2/post/publish/inbox/video/init/";
16
17#[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 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#[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 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}