use std::time::{Duration, Instant};
use base64;
use serde::de::Error;
use serde::{Deserialize, Deserializer};
use crate::common::*;
use crate::{auth, error, links};
use mime;
pub mod media_types {
use mime::{self, Mime};
pub fn image_png() -> Mime {
mime::IMAGE_PNG
}
pub fn image_jpg() -> Mime {
mime::IMAGE_JPEG
}
pub fn image_webp() -> Mime {
"image/webp".parse().unwrap()
}
pub fn image_gif() -> Mime {
mime::IMAGE_GIF
}
pub fn video_mp4() -> Mime {
"video/mp4".parse().unwrap()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ProgressInfo {
Pending(u64),
InProgress(u64),
Failed(error::MediaError),
Success,
}
#[derive(Debug, Deserialize)]
enum RawProgressInfoTag {
#[serde(rename = "pending")]
Pending,
#[serde(rename = "in_progress")]
InProgress,
#[serde(rename = "failed")]
Failed,
#[serde(rename = "succeeded")]
Success,
}
#[derive(Debug, Deserialize)]
struct RawProgressInfo {
state: RawProgressInfoTag,
progress_percent: Option<f64>,
check_after_secs: Option<u64>,
error: Option<error::MediaError>,
}
impl<'de> Deserialize<'de> for ProgressInfo {
fn deserialize<D>(deser: D) -> Result<ProgressInfo, D::Error>
where
D: Deserializer<'de>,
{
use self::RawProgressInfoTag::*;
let raw = RawProgressInfo::deserialize(deser)?;
let check_after = raw
.check_after_secs
.ok_or_else(|| D::Error::custom("Missing field: check_after_secs"));
Ok(match raw.state {
Pending => ProgressInfo::Pending(check_after?),
InProgress => ProgressInfo::InProgress(check_after?),
Success => ProgressInfo::Success,
Failed => {
let err = raw
.error
.ok_or_else(|| D::Error::custom("Missing field: error"))?;
ProgressInfo::Failed(err)
}
})
}
}
#[derive(Debug, Deserialize)]
struct RawMedia {
#[serde(rename = "media_id_string")]
id: String,
#[serde(default)]
#[serde(rename = "expires_after_secs")]
expires_after: u64,
#[serde(rename = "processing_info")]
progress: Option<ProgressInfo>,
}
#[derive(Debug, Clone, derive_more::From)]
pub struct MediaId(pub(crate) String);
#[derive(Debug, Clone)]
pub struct MediaHandle {
pub id: MediaId,
pub expires_at: Instant,
pub progress: Option<ProgressInfo>,
}
impl From<RawMedia> for MediaHandle {
fn from(raw: RawMedia) -> Self {
Self {
id: raw.id.into(),
expires_at: Instant::now() + Duration::from_secs(raw.expires_after),
progress: raw.progress,
}
}
}
impl MediaHandle {
pub fn is_valid(&self) -> bool {
Instant::now() < self.expires_at
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, derive_more::Display)]
enum MediaCategory {
#[display(fmt = "tweet_image")]
Image,
#[display(fmt = "tweet_gif")]
Gif,
#[display(fmt = "tweet_video")]
Video,
}
impl From<&mime::Mime> for MediaCategory {
fn from(mime: &mime::Mime) -> Self {
if mime == &media_types::image_gif() {
MediaCategory::Gif
} else if mime == &media_types::video_mp4() {
MediaCategory::Video
} else {
MediaCategory::Image
}
}
}
pub async fn upload_media(
data: &[u8],
media_type: &mime::Mime,
token: &auth::Token,
) -> error::Result<MediaHandle> {
let media_category = MediaCategory::from(media_type);
let params = ParamList::new()
.add_param("command", "INIT")
.add_param("total_bytes", data.len().to_string())
.add_param("media_type", media_type.to_string())
.add_param("media_category", media_category.to_string());
let req = auth::post(links::media::UPLOAD, &token, Some(¶ms));
let media = request_with_json_response::<RawMedia>(req).await?.response;
for (ix, chunk) in data.chunks(1024 * 1024).enumerate() {
let params = ParamList::new()
.add_param("command", "APPEND")
.add_param("media_id", media.id.clone())
.add_param("media_data", base64::encode(chunk))
.add_param("segment_index", ix.to_string());
let req = auth::post(links::media::UPLOAD, token, Some(¶ms));
raw_request(req).await?;
}
let params = ParamList::new()
.add_param("command", "FINALIZE")
.add_param("media_id", media.id.clone());
let req = auth::post(links::media::UPLOAD, token, Some(¶ms));
Ok(request_with_json_response::<RawMedia>(req)
.await?
.response
.into())
}
pub async fn get_status(media_id: MediaId, token: &auth::Token) -> error::Result<MediaHandle> {
let params = ParamList::new()
.add_param("command", "STATUS")
.add_param("media_id", media_id.0);
let req = auth::get(links::media::UPLOAD, token, Some(¶ms));
Ok(request_with_json_response::<RawMedia>(req)
.await?
.response
.into())
}
pub async fn set_metadata(
media_id: &MediaId,
alt_text: &str,
token: &auth::Token,
) -> error::Result<()> {
let payload = serde_json::json!({
"media_id": media_id.0,
"alt_text": {
"text": alt_text
}
});
let req = auth::post_json(links::media::METADATA, &token, payload);
raw_request(req).await?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::RawMedia;
use crate::common::tests::load_file;
fn load_media(path: &str) -> RawMedia {
let content = load_file(path);
::serde_json::from_str::<RawMedia>(&content).unwrap()
}
#[test]
fn parse_media() {
let media = load_media("sample_payloads/media.json");
assert_eq!(media.id, "710511363345354753");
assert_eq!(media.expires_after, 86400);
}
#[test]
fn parse_media_pending() {
let media = load_media("sample_payloads/media_pending.json");
assert_eq!(media.id, "13");
assert_eq!(media.expires_after, 86400);
assert!(media.progress.is_some());
match media.progress {
Some(super::ProgressInfo::Pending(5)) => (),
other => assert!(false, format!("Unexpected value of progress={:?}", other)),
}
}
#[test]
fn parse_media_in_progress() {
let media = load_media("sample_payloads/media_in_progress.json");
assert_eq!(media.id, "13");
assert_eq!(media.expires_after, 3595);
assert!(media.progress.is_some());
match media.progress {
Some(super::ProgressInfo::InProgress(10)) => (),
other => assert!(false, format!("Unexpected value of progress={:?}", other)),
}
}
#[test]
fn parse_media_fail() {
let media = load_media("sample_payloads/media_fail.json");
assert_eq!(media.id, "710511363345354753");
assert_eq!(media.expires_after, 0);
assert!(media.progress.is_some());
match media.progress {
Some(super::ProgressInfo::Failed(error)) => assert_eq!(
error,
crate::error::MediaError {
code: 1,
name: "InvalidMedia".to_string(),
message: "Unsupported video format".to_string(),
}
),
other => assert!(false, format!("Unexpected value of progress={:?}", other)),
}
}
}