tiktok_api/endpoints/share/
video_upload.rs1use chrono::Utc;
2use reqwest::{
3 multipart::{Form, Part},
4 Body, Client, Error as ReqwestError,
5};
6use serde::{Deserialize, Serialize};
7use serde_json::Error as SerdeJsonError;
8use url::{ParseError as UrlParseError, Url};
9
10pub const URL: &str = "https://open-api.tiktok.com/share/video/upload/";
12
13pub async fn video_upload<T>(
15 client: &Client,
16 open_id: impl AsRef<str>,
17 access_token: impl AsRef<str>,
18 stream: T,
19 stream_length: Option<u64>,
20 file_name: Option<String>,
21) -> Result<VideoUploadResponseBody, VideoUploadError>
22where
23 T: Into<Body>,
24{
25 let open_id = open_id.as_ref();
26 let access_token = access_token.as_ref();
27
28 let mut req_url = Url::parse(URL).map_err(VideoUploadError::MakeRequestUrlFailed)?;
30
31 req_url
32 .query_pairs_mut()
33 .append_pair("open_id", open_id)
34 .append_pair("access_token", access_token);
35
36 let part = if let Some(stream_length) = stream_length {
38 Part::stream_with_length(stream, stream_length)
39 } else {
40 Part::stream(stream)
41 };
42
43 let part = if let Some(file_name) = file_name {
44 part.file_name(file_name)
45 } else {
46 part.file_name(format!("{}.mp4", Utc::now().timestamp_millis()))
47 };
48
49 let form = Form::new().part("video", part).percent_encode_noop();
50
51 let resp = client
53 .post(req_url)
54 .multipart(form)
55 .send()
56 .await
57 .map_err(VideoUploadError::RespondFailed)?;
58
59 let resp_body = resp
61 .bytes()
62 .await
63 .map_err(VideoUploadError::ReadResponseBodyFailed)?;
64 let resp_body = resp_body.as_ref();
65
66 serde_json::from_slice::<VideoUploadResponseBody>(resp_body)
67 .map_err(VideoUploadError::DeResponseBodyFailed)
68}
69
70#[cfg(feature = "with_tokio")]
71pub async fn video_upload_from_reader_stream<S>(
72 client: &Client,
73 open_id: impl AsRef<str>,
74 access_token: impl AsRef<str>,
75 stream: S,
76 stream_length: Option<u64>,
77 file_name: Option<String>,
78) -> Result<VideoUploadResponseBody, VideoUploadError>
79where
80 S: tokio::io::AsyncRead + Send + Sync + 'static,
81{
82 use tokio_util::io::ReaderStream;
83
84 video_upload(
85 client,
86 open_id,
87 access_token,
88 Body::wrap_stream(ReaderStream::new(stream)),
89 stream_length,
90 file_name,
91 )
92 .await
93}
94
95#[cfg(feature = "with_tokio_fs")]
96pub async fn video_upload_from_file(
97 client: &Client,
98 open_id: impl AsRef<str>,
99 access_token: impl AsRef<str>,
100 file_path: &std::path::PathBuf,
101) -> Result<VideoUploadResponseBody, VideoUploadError> {
102 use tokio::fs::File;
103
104 let crate::tokio_fs_util::Info {
105 file_size,
106 file_name,
107 } = crate::tokio_fs_util::info(file_path)
108 .await
109 .map_err(VideoUploadError::GetFileInfoFailed)?;
110
111 let file = File::open(&file_path)
112 .await
113 .map_err(VideoUploadError::OpenFileFailed)?;
114
115 video_upload_from_reader_stream(
116 client,
117 open_id,
118 access_token,
119 file,
120 Some(file_size),
121 file_name,
122 )
123 .await
124}
125
126#[derive(Deserialize, Serialize, Debug, Clone)]
130pub struct VideoUploadResponseBody {
131 pub data: VideoUploadResponseBodyData,
132 pub extra: VideoUploadResponseBodyExtra,
133}
134
135#[derive(Deserialize, Serialize, Debug, Clone)]
136pub struct VideoUploadResponseBodyData {
137 pub err_code: i64,
138 pub error_code: i64,
139 pub share_id: Option<String>,
140 pub error_msg: Option<String>,
141}
142
143#[derive(Deserialize, Serialize, Debug, Clone)]
144pub struct VideoUploadResponseBodyExtra {
145 pub error_detail: String,
146 pub logid: String,
147}
148
149#[derive(Debug)]
153pub enum VideoUploadError {
154 MakeRequestUrlFailed(UrlParseError),
155 RespondFailed(ReqwestError),
156 ReadResponseBodyFailed(ReqwestError),
157 DeResponseBodyFailed(SerdeJsonError),
158 #[cfg(feature = "with_tokio_fs")]
159 GetFileInfoFailed(std::io::Error),
160 #[cfg(feature = "with_tokio_fs")]
161 OpenFileFailed(std::io::Error),
162}
163impl core::fmt::Display for VideoUploadError {
164 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
165 write!(f, "{self:?}")
166 }
167}
168impl std::error::Error for VideoUploadError {}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_de_response_body() {
176 match serde_json::from_str::<VideoUploadResponseBody>(include_str!(
177 "../../../tests/response_body_files/share/video_upload.json"
178 )) {
179 Ok(ok_json) => {
180 assert_eq!(ok_json.data.err_code, 0);
181 assert_eq!(ok_json.extra.error_detail, "");
182 }
183 x => panic!("{x:?}"),
184 }
185
186 match serde_json::from_str::<VideoUploadResponseBody>(include_str!(
187 "../../../tests/response_body_files/share/video_upload__err.json"
188 )) {
189 Ok(ok_json) => {
190 assert_eq!(ok_json.data.err_code, 20000);
191 assert_eq!(
192 ok_json.extra.error_detail,
193 "access_token not found in the request query param"
194 );
195 }
196 x => panic!("{x:?}"),
197 }
198 }
199}