use std::collections::HashMap;
use std::io::{Read, Write};
use std::path::Path;
use std::thread;
use std::time::Duration;
use super::request::{ApiClient, MultipartOptions, RequestOptions, RequestTarget};
use super::response::types::{ApiResponse, MediaUploadResponse, deserialize_response};
use crate::error::{Result, XurlError};
use crate::output::OutputConfig;
pub const MEDIA_ENDPOINT: &str = "/2/media/upload";
#[allow(clippy::too_many_arguments)]
pub fn execute_media_upload(
file_path: &str,
media_type: &str,
media_category: &str,
auth_type: &str,
username: &str,
verbose: bool,
trace: bool,
wait_for_processing: bool,
headers: &[String],
client: &mut ApiClient,
out: &OutputConfig,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<()> {
let metadata = std::fs::metadata(file_path)
.map_err(|e| XurlError::Io(format!("error accessing file: {e}")))?;
if !metadata.is_file() {
return Err(XurlError::Io(format!("{file_path} is not a regular file")));
}
let file_size = metadata.len();
let base_opts = RequestOptions {
auth_type: auth_type.to_string(),
username: username.to_string(),
verbose,
trace,
headers: headers.to_vec(),
..Default::default()
};
out.status(stderr, "Initializing media upload...");
let init_body = serde_json::json!({
"total_bytes": file_size,
"media_type": media_type,
"media_category": media_category,
});
let mut init_opts = base_opts.clone();
init_opts.method = "POST".to_string();
init_opts.target = RequestTarget::Template {
path: "/2/media/upload/initialize".to_string(),
path_params: HashMap::new(),
query: Vec::new(),
};
init_opts.data = init_body.to_string();
let init_response: ApiResponse<MediaUploadResponse> =
deserialize_response(client.send_request(&init_opts)?)?;
let media_id = init_response.data.id.clone();
if media_id.is_empty() {
return Err(XurlError::Json(
"failed to parse media ID from init response".to_string(),
));
}
if verbose {
let value = serde_json::to_value(&init_response)?;
out.print_response(stdout, &value);
}
upload_chunks(
file_path, &media_id, &base_opts, verbose, file_size, client, out, stderr,
)?;
out.status(stderr, "Finalizing media upload...");
let mut finalize_opts = base_opts.clone();
finalize_opts.method = "POST".to_string();
finalize_opts.target = RequestTarget::Template {
path: "/2/media/upload/{id}/finalize".to_string(),
path_params: HashMap::from([("id".to_string(), media_id.clone())]),
query: Vec::new(),
};
finalize_opts.data.clear();
let finalize_response: ApiResponse<MediaUploadResponse> =
deserialize_response(client.send_request(&finalize_opts)?)?;
let finalize_value = serde_json::to_value(&finalize_response)?;
out.print_response(stdout, &finalize_value);
if wait_for_processing && media_category.contains("video") {
out.status(stderr, "Waiting for media processing to complete...");
let processing_response =
wait_for_media_processing(&media_id, &base_opts, verbose, client, out, stderr)?;
let processing_value = serde_json::to_value(&processing_response)?;
out.print_response(stdout, &processing_value);
}
out.status(
stderr,
&format!("Media uploaded successfully! Media ID: {media_id}"),
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn upload_chunks(
file_path: &str,
media_id: &str,
base_opts: &RequestOptions,
verbose: bool,
file_size: u64,
client: &mut ApiClient,
out: &OutputConfig,
stderr: &mut dyn Write,
) -> Result<()> {
out.status(stderr, "Uploading media in chunks...");
let mut file = std::fs::File::open(file_path)?;
let chunk_size = 4 * 1024 * 1024;
let mut buffer = vec![0u8; chunk_size];
let mut segment_index = 0;
let mut bytes_uploaded: u64 = 0;
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
let file_name = Path::new(file_path)
.file_name()
.map_or_else(|| "file".to_string(), |n| n.to_string_lossy().to_string());
let mut form_fields = HashMap::new();
form_fields.insert("segment_index".to_string(), segment_index.to_string());
let multipart_opts = MultipartOptions {
request: RequestOptions {
method: "POST".to_string(),
target: RequestTarget::Template {
path: "/2/media/upload/{id}/append".to_string(),
path_params: HashMap::from([("id".to_string(), media_id.to_string())]),
query: Vec::new(),
},
headers: base_opts.headers.clone(),
auth_type: base_opts.auth_type.clone(),
username: base_opts.username.clone(),
verbose,
trace: base_opts.trace,
..Default::default()
},
form_fields,
file_field: "media".to_string(),
file_path: String::new(),
file_name,
file_data: buffer[..bytes_read].to_vec(),
};
client.send_multipart_request(&multipart_opts)?;
bytes_uploaded += bytes_read as u64;
segment_index += 1;
if verbose {
#[allow(clippy::cast_precision_loss)]
let pct = (bytes_uploaded as f64 / file_size as f64) * 100.0;
out.info(
stderr,
&format!("Uploaded {bytes_uploaded} of {file_size} bytes ({pct:.2}%)"),
);
}
}
out.status(stderr, "Upload complete!");
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn execute_media_status(
media_id: &str,
auth_type: &str,
username: &str,
verbose: bool,
wait: bool,
trace: bool,
headers: &[String],
client: &mut ApiClient,
out: &OutputConfig,
stdout: &mut dyn Write,
stderr: &mut dyn Write,
) -> Result<()> {
let base_opts = RequestOptions {
auth_type: auth_type.to_string(),
username: username.to_string(),
verbose,
trace,
headers: headers.to_vec(),
..Default::default()
};
if wait {
let response =
wait_for_media_processing(media_id, &base_opts, verbose, client, out, stderr)?;
let value = serde_json::to_value(&response)?;
out.print_response(stdout, &value);
} else {
let response = check_media_status(media_id, &base_opts, client)?;
let value = serde_json::to_value(&response)?;
out.print_response(stdout, &value);
}
Ok(())
}
fn check_media_status(
media_id: &str,
base_opts: &RequestOptions,
client: &mut ApiClient,
) -> Result<ApiResponse<MediaUploadResponse>> {
let mut opts = base_opts.clone();
opts.method = "GET".to_string();
opts.target = RequestTarget::Template {
path: "/2/media/upload".to_string(),
path_params: HashMap::new(),
query: vec![
("command".to_string(), "STATUS".to_string()),
("media_id".to_string(), media_id.to_string()),
],
};
opts.data.clear();
deserialize_response(client.send_request(&opts)?)
}
fn wait_for_media_processing(
media_id: &str,
base_opts: &RequestOptions,
verbose: bool,
client: &mut ApiClient,
out: &OutputConfig,
stderr: &mut dyn Write,
) -> Result<ApiResponse<MediaUploadResponse>> {
loop {
let response = check_media_status(media_id, base_opts, client)?;
let state = response
.data
.processing_info
.as_ref()
.map_or("", |p| p.state.as_str());
if state == "succeeded" {
out.status(stderr, "Media processing complete!");
return Ok(response);
} else if state == "failed" {
return Err(XurlError::validation("media processing failed"));
}
let check_after = response
.data
.processing_info
.as_ref()
.and_then(|p| p.check_after_secs)
.unwrap_or(1)
.max(1);
if verbose {
let pct = response
.data
.processing_info
.as_ref()
.and_then(|p| p.progress_percent)
.unwrap_or(0);
out.info(
stderr,
&format!(
"Media processing in progress ({pct}%), checking again in {check_after} seconds..."
),
);
}
thread::sleep(Duration::from_secs(check_after));
}
}
pub fn handle_media_append_request(
options: &RequestOptions,
media_file: &str,
client: &mut ApiClient,
) -> Result<serde_json::Value> {
let url_for_id = match &options.target {
RequestTarget::RawUrl(u) => u.clone(),
RequestTarget::Template { path, .. } => {
return Err(XurlError::validation(format!(
"handle_media_append_request requires a RawUrl target; got Template {{ path: {path:?} }} — call this only from the raw-mode path"
)));
}
};
let media_id = extract_media_id(&url_for_id);
if media_id.is_empty() {
return Err(XurlError::validation(
"media_id is required for append endpoint",
));
}
let segment_index = if options.data.is_empty() {
"0".to_string()
} else {
extract_segment_index(&options.data)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "0".to_string())
};
let file_name = Path::new(media_file)
.file_name()
.map_or_else(|| "file".to_string(), |n| n.to_string_lossy().to_string());
let mut form_fields = HashMap::new();
form_fields.insert("segment_index".to_string(), segment_index);
let multipart_opts = MultipartOptions {
request: options.clone(),
form_fields,
file_field: "media".to_string(),
file_path: media_file.to_string(),
file_name,
file_data: Vec::new(),
};
client.send_multipart_request(&multipart_opts)
}
#[must_use]
pub fn extract_media_id(url: &str) -> String {
if url.is_empty() || !url.contains("/2/media/upload") {
return String::new();
}
if url.ends_with("/2/media/upload/initialize") {
return String::new();
}
if let Some(rest) = url.split("/2/media/upload/").nth(1) {
for suffix in &["/append", "/finalize"] {
if let Some(idx) = rest.find(suffix) {
return rest[..idx].to_string();
}
}
}
if let Some(query) = url.split('?').nth(1) {
for param in query.split('&') {
if let Some(value) = param.strip_prefix("media_id=") {
return value.to_string();
}
}
}
String::new()
}
#[must_use]
pub fn extract_segment_index(data: &str) -> Option<String> {
let json: serde_json::Value = serde_json::from_str(data).ok()?;
json.get("segment_index").and_then(|v| {
v.as_str()
.map(std::string::ToString::to_string)
.or_else(|| Some(v.to_string()))
})
}
#[must_use]
pub fn is_media_append_request(url: &str, media_file: &str) -> bool {
url.contains("/2/media/upload") && url.contains("append") && !media_file.is_empty()
}