tail-fin-youtube 0.7.8

YouTube adapter for tail-fin: search, video, channel, comments, transcript via InnerTube API
Documentation
/// Extract a YouTube video ID from a URL or bare ID.
///
/// Supports:
/// - `https://www.youtube.com/watch?v=ID`
/// - `https://youtu.be/ID`
/// - `https://www.youtube.com/shorts/ID`
/// - Bare ID string (e.g. `dQw4w9WgXcQ`)
pub fn extract_video_id(input: &str) -> String {
    let input = input.trim();

    // youtube.com/watch?v=ID
    if let Some(pos) = input.find("v=") {
        let after = &input[pos + 2..];
        return after
            .split(&['&', '#', '?'][..])
            .next()
            .unwrap_or(after)
            .to_string();
    }

    // youtu.be/ID or youtube.com/shorts/ID
    if input.contains("youtu.be/") || input.contains("/shorts/") {
        let id = input
            .rsplit('/')
            .next()
            .unwrap_or(input)
            .split(&['?', '#', '&'][..])
            .next()
            .unwrap_or(input);
        return id.to_string();
    }

    // Bare ID
    input.to_string()
}

/// Extract a YouTube channel identifier from a URL, @handle, or bare channel ID.
///
/// Supports:
/// - `https://www.youtube.com/@handle`
/// - `https://www.youtube.com/channel/UCXXXX`
/// - `@handle` (returned as-is)
/// - Bare channel ID (e.g. `UCXXXX`)
pub fn extract_channel_id(input: &str) -> String {
    let input = input.trim();

    // youtube.com/channel/UCXXXX
    if input.contains("/channel/") {
        let id = input
            .rsplit("/channel/")
            .next()
            .unwrap_or(input)
            .split(&['/', '?', '#'][..])
            .next()
            .unwrap_or(input);
        return id.to_string();
    }

    // youtube.com/@handle → extract @handle
    if input.contains("youtube.com/@") {
        let handle = input
            .rsplit("youtube.com/")
            .next()
            .unwrap_or(input)
            .split(&['/', '?', '#'][..])
            .next()
            .unwrap_or(input);
        return handle.to_string();
    }

    // Already @handle or bare ID
    input.to_string()
}

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

    #[test]
    fn test_extract_video_id_watch_url() {
        assert_eq!(
            extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ"),
            "dQw4w9WgXcQ"
        );
    }

    #[test]
    fn test_extract_video_id_watch_url_with_params() {
        assert_eq!(
            extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=42"),
            "dQw4w9WgXcQ"
        );
    }

    #[test]
    fn test_extract_video_id_short_url() {
        assert_eq!(
            extract_video_id("https://youtu.be/dQw4w9WgXcQ"),
            "dQw4w9WgXcQ"
        );
    }

    #[test]
    fn test_extract_video_id_shorts_url() {
        assert_eq!(
            extract_video_id("https://www.youtube.com/shorts/dQw4w9WgXcQ"),
            "dQw4w9WgXcQ"
        );
    }

    #[test]
    fn test_extract_video_id_bare() {
        assert_eq!(extract_video_id("dQw4w9WgXcQ"), "dQw4w9WgXcQ");
    }

    #[test]
    fn test_extract_channel_id_url() {
        assert_eq!(
            extract_channel_id("https://www.youtube.com/channel/UC1234"),
            "UC1234"
        );
    }

    #[test]
    fn test_extract_channel_id_handle_url() {
        assert_eq!(
            extract_channel_id("https://www.youtube.com/@rustlang"),
            "@rustlang"
        );
    }

    #[test]
    fn test_extract_channel_id_bare_handle() {
        assert_eq!(extract_channel_id("@rustlang"), "@rustlang");
    }

    #[test]
    fn test_extract_channel_id_bare_id() {
        assert_eq!(extract_channel_id("UC1234"), "UC1234");
    }
}