tweet2media 0.2.0

Library to get media URLs from tweet URLs
Documentation
use serde_json::Value;
use std::collections::HashSet;

pub(super) fn extract_media_urls(tweet: &Value) -> Vec<String> {
    let mut out = Vec::new();
    let mut seen = HashSet::new();

    let media_arrays = [
        tweet.pointer("/legacy/extended_entities/media"),
        tweet.pointer("/legacy/entities/media"),
        tweet.pointer("/extended_entities/media"),
        tweet.pointer("/entities/media"),
    ];

    for media_array in media_arrays.into_iter().flatten() {
        let Some(items) = media_array.as_array() else {
            continue;
        };

        for media in items {
            if media_unavailable(media) {
                continue;
            }

            if let Some(url) = best_video_url(media).or_else(|| image_url(media))
                && seen.insert(url.clone())
            {
                out.push(url);
            }
        }
    }

    out
}

fn media_unavailable(media: &Value) -> bool {
    media
        .pointer("/ext_media_availability/status")
        .and_then(Value::as_str)
        == Some("Unavailable")
}

fn best_video_url(media: &Value) -> Option<String> {
    let variants = media.pointer("/video_info/variants")?.as_array()?;

    variants
        .iter()
        .filter(|variant| {
            let url = variant
                .get("url")
                .and_then(Value::as_str)
                .unwrap_or_default();
            let content_type = variant
                .get("content_type")
                .and_then(Value::as_str)
                .unwrap_or_default();
            content_type == "video/mp4" || url.contains(".mp4")
        })
        .max_by_key(|variant| variant.get("bitrate").and_then(Value::as_u64).unwrap_or(0))
        .and_then(|variant| variant.get("url").and_then(Value::as_str))
        .or_else(|| {
            variants
                .iter()
                .max_by_key(|variant| variant.get("bitrate").and_then(Value::as_u64).unwrap_or(0))
                .and_then(|variant| variant.get("url").and_then(Value::as_str))
        })
        .map(str::to_string)
}

fn image_url(media: &Value) -> Option<String> {
    let url = media
        .get("media_url_https")
        .or_else(|| media.get("media_url"))
        .and_then(Value::as_str)?;
    Some(original_image_url(url))
}

fn original_image_url(url: &str) -> String {
    if url.contains("?format=") {
        if let Some((prefix, _)) = url.split_once("name=") {
            return format!("{prefix}name=orig");
        }
        return format!("{url}&name=orig");
    }

    let clean = url.split('?').next().unwrap_or(url);
    if let Some((base, ext)) = clean.rsplit_once('.') {
        return format!("{base}?format={ext}&name=orig");
    }

    url.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn rewrites_image_to_original() {
        assert_eq!(
            original_image_url("https://pbs.twimg.com/media/abc.jpg"),
            "https://pbs.twimg.com/media/abc?format=jpg&name=orig"
        );
        assert_eq!(
            original_image_url("https://pbs.twimg.com/media/abc?format=png&name=small"),
            "https://pbs.twimg.com/media/abc?format=png&name=orig"
        );
    }

    #[test]
    fn extracts_images_and_best_video() {
        let tweet = json!({
            "legacy": {
                "extended_entities": {
                    "media": [
                        {"media_url_https": "https://pbs.twimg.com/media/a.jpg"},
                        {
                            "video_info": {
                                "variants": [
                                    {"content_type": "application/x-mpegURL", "url": "https://video.twimg.com/a.m3u8"},
                                    {"content_type": "video/mp4", "bitrate": 256000, "url": "https://video.twimg.com/a-low.mp4"},
                                    {"content_type": "video/mp4", "bitrate": 832000, "url": "https://video.twimg.com/a-high.mp4"}
                                ]
                            },
                            "media_url_https": "https://pbs.twimg.com/media/video-thumb.jpg"
                        }
                    ]
                }
            }
        });

        assert_eq!(
            extract_media_urls(&tweet),
            vec![
                "https://pbs.twimg.com/media/a?format=jpg&name=orig".to_string(),
                "https://video.twimg.com/a-high.mp4".to_string()
            ]
        );
    }
}