use http_api_client_endpoint::{
http::{
header::{ACCEPT, AUTHORIZATION, USER_AGENT},
Method,
},
Body, Endpoint, Request, Response,
};
use serde::{Deserialize, Serialize};
use url::Url;
use super::common::{endpoint_parse_response, EndpointError, EndpointRet};
use crate::objects::v2::Error;
pub const URL: &str = "https://open.tiktokapis.com/v2/post/publish/inbox/video/init/";
#[derive(Debug, Clone)]
pub struct VideoUploadInitEndpoint {
pub access_token: String,
pub source_info: VideoUploadInitRequestBodySourceInfo,
}
impl VideoUploadInitEndpoint {
pub fn new(
access_token: impl AsRef<str>,
source_info: VideoUploadInitRequestBodySourceInfo,
) -> Self {
Self {
access_token: access_token.as_ref().into(),
source_info,
}
}
#[cfg(feature = "with_tokio_fs")]
pub async fn with_file(
access_token: impl AsRef<str>,
file_path: &std::path::PathBuf,
chunk_size: Option<usize>,
) -> Result<Self, EndpointError> {
use crate::media_transfer::{get_chunk_size_and_total_chunk_count, CHUNK_SIZE_MAX};
let crate::tokio_fs_util::Info {
file_size,
file_name: _,
} = crate::tokio_fs_util::info(file_path)
.await
.map_err(EndpointError::GetFileInfoFailed)?;
let video_size = file_size as usize;
let (chunk_size, total_chunk_count) =
get_chunk_size_and_total_chunk_count(video_size, chunk_size.unwrap_or(CHUNK_SIZE_MAX));
let source_info = VideoUploadInitRequestBodySourceInfo::FileUpload {
video_size,
chunk_size,
total_chunk_count,
};
Ok(Self {
access_token: access_token.as_ref().into(),
source_info,
})
}
}
impl Endpoint for VideoUploadInitEndpoint {
type RenderRequestError = EndpointError;
type ParseResponseOutput = EndpointRet<VideoUploadInitResponseBody>;
type ParseResponseError = EndpointError;
fn render_request(&self) -> Result<Request<Body>, Self::RenderRequestError> {
let request_body = VideoUploadInitRequestBody {
source_info: self.source_info.to_owned(),
};
let request_body =
serde_json::to_vec(&request_body).map_err(EndpointError::SerRequestBodyFailed)?;
let request = Request::builder()
.method(Method::POST)
.uri(URL)
.header(AUTHORIZATION, format!("Bearer {}", &self.access_token))
.header(USER_AGENT, "tiktok-api")
.header(ACCEPT, "application/json; charset=UTF-8")
.body(request_body)
.map_err(EndpointError::MakeRequestFailed)?;
Ok(request)
}
fn parse_response(
&self,
response: Response<Body>,
) -> Result<Self::ParseResponseOutput, Self::ParseResponseError> {
endpoint_parse_response(response)
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VideoUploadInitRequestBody {
pub source_info: VideoUploadInitRequestBodySourceInfo,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "source")]
pub enum VideoUploadInitRequestBodySourceInfo {
#[serde(rename = "FILE_UPLOAD")]
FileUpload {
video_size: usize,
chunk_size: usize,
total_chunk_count: usize,
},
#[serde(rename = "PULL_FROM_URL")]
PullFromUrl { video_url: Url },
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VideoUploadInitResponseBody {
pub data: VideoUploadInitResponseBodyData,
pub error: Error,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VideoUploadInitResponseBodyData {
pub publish_id: String,
pub upload_url: Option<Url>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::v2::ErrorCode;
#[test]
fn test_render_request() {
let req = VideoUploadInitEndpoint::new(
"TOKEN",
VideoUploadInitRequestBodySourceInfo::FileUpload {
video_size: 30567100,
chunk_size: 30567100,
total_chunk_count: 1,
},
)
.render_request()
.unwrap();
assert_eq!(req.method(), Method::POST);
assert_eq!(req.uri(), URL);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(req.body()).unwrap(),
serde_json::json!({
"source_info": {
"source": "FILE_UPLOAD",
"video_size": 30567100,
"chunk_size" : 30567100,
"total_chunk_count": 1
}
})
);
let req = VideoUploadInitEndpoint::new(
"TOKEN",
VideoUploadInitRequestBodySourceInfo::PullFromUrl {
video_url: "https://example.verified.domain.com/example_video.mp4"
.parse()
.unwrap(),
},
)
.render_request()
.unwrap();
assert_eq!(req.method(), Method::POST);
assert_eq!(req.uri(), URL);
assert_eq!(
serde_json::from_slice::<serde_json::Value>(req.body()).unwrap(),
serde_json::json!({
"source_info": {
"source": "PULL_FROM_URL",
"video_url": "https://example.verified.domain.com/example_video.mp4",
}
})
);
}
#[test]
fn test_de_response_body() {
match serde_json::from_str::<VideoUploadInitResponseBody>(include_str!(
"../../../tests/response_body_files/v2/video_upload_init.json"
)) {
Ok(ok_json) => {
assert_eq!(ok_json.data.publish_id, "v_inbox_file~v2.123456789");
assert_eq!(
ok_json.data.upload_url,
Some("https://open-upload.tiktokapis.com/video/?upload_id=67890&upload_token=Xza123".parse().unwrap())
);
assert_eq!(ok_json.error.code, ErrorCode::Ok);
}
x => panic!("{x:?}"),
}
match serde_json::from_str::<VideoUploadInitResponseBody>(include_str!(
"../../../tests/response_body_files/v2/video_upload_init__without_upload_url.json"
)) {
Ok(ok_json) => {
assert_eq!(ok_json.data.publish_id, "v_inbox_file~v2.123456789");
assert!(ok_json.data.upload_url.is_none());
assert_eq!(ok_json.error.code, ErrorCode::Ok);
}
x => panic!("{x:?}"),
}
}
}