tiktok_api/endpoints/share/
video_upload.rs

1use 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
10//
11pub const URL: &str = "https://open-api.tiktok.com/share/video/upload/";
12
13//
14pub 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    //
29    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    //
37    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    //
52    let resp = client
53        .post(req_url)
54        .multipart(form)
55        .send()
56        .await
57        .map_err(VideoUploadError::RespondFailed)?;
58
59    //
60    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//
127//
128//
129#[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//
150//
151//
152#[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}