tail-fin-cli 0.2.0

Multi-site browser automation CLI — Twitter, Reddit, Bloomberg, Coupang, PCC, Instagram, YouTube, Grok, SeekingAlpha, Xiaohongshu, 591
use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{print_json, print_list, Ctx};

#[derive(Subcommand)]
pub enum YoutubeAction {
    /// Search for videos
    Search {
        query: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get video details
    Video {
        /// Video URL or ID
        video: String,
    },
    /// Get channel info
    Channel {
        /// Channel URL, @handle, or channel ID
        channel: String,
    },
    /// Get video comments
    Comments {
        /// Video URL or ID
        video: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get trending videos
    Trending {
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get video transcript/subtitles
    Transcript {
        /// Video URL or ID
        video: String,
        /// Output plain text without timestamps
        #[arg(long, default_value_t = false)]
        plain: bool,
    },
    /// Get subscriptions (requires login)
    Subscriptions {
        #[arg(long, default_value_t = 50)]
        count: usize,
    },
}

pub async fn run(action: YoutubeAction, ctx: &Ctx) -> Result<(), TailFinError> {
    if ctx.cookies.is_some() {
        eprintln!("Error: YouTube cookie mode is not yet supported.");
        eprintln!("  Use --connect for browser mode.");
        std::process::exit(1);
    }

    let chrome_host = ctx.connect.as_deref().unwrap_or("127.0.0.1:9222");
    let session = crate::session::browser_session(chrome_host, ctx.headed).await?;
    let client = tail_fin_youtube::YouTubeClient::new(session);

    match action {
        YoutubeAction::Search { query, count } => {
            let videos = client.search(&query, count).await?;
            print_list("videos", &videos, videos.len())?;
        }
        YoutubeAction::Video { video } => {
            let id = tail_fin_youtube::extract_video_id(&video);
            let result = client.video(&id).await?;
            match result {
                Some(v) => print_json(&v)?,
                None => {
                    eprintln!("Error: Could not fetch video details for '{}'", id);
                    std::process::exit(1);
                }
            }
        }
        YoutubeAction::Channel { channel } => {
            let id = tail_fin_youtube::extract_channel_id(&channel);
            let result = client.channel(&id).await?;
            match result {
                Some(ch) => print_json(&ch)?,
                None => {
                    eprintln!("Error: Could not fetch channel info for '{}'", id);
                    std::process::exit(1);
                }
            }
        }
        YoutubeAction::Comments { video, count } => {
            let id = tail_fin_youtube::extract_video_id(&video);
            let comments = client.comments(&id, count).await?;
            print_list("comments", &comments, comments.len())?;
        }
        YoutubeAction::Trending { count } => {
            let videos = client.trending(count).await?;
            print_list("videos", &videos, videos.len())?;
        }
        YoutubeAction::Transcript { video, plain } => {
            let id = tail_fin_youtube::extract_video_id(&video);
            let segments = client.transcript(&id).await?;
            if plain {
                let text: String = segments
                    .iter()
                    .map(|s| s.text.as_str())
                    .collect::<Vec<_>>()
                    .join(" ");
                print_json(&serde_json::json!({
                    "text": text,
                    "video_id": id,
                }))?;
            } else {
                print_json(&serde_json::json!({
                    "segments": segments,
                    "count": segments.len(),
                    "video_id": id,
                }))?;
            }
        }
        YoutubeAction::Subscriptions { count } => {
            let channels = client.subscriptions(count).await?;
            print_list("channels", &channels, channels.len())?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "youtube"
    }

    fn about(&self) -> &'static str {
        "YouTube operations"
    }

    fn command(&self) -> clap::Command {
        <YoutubeAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("youtube").about("YouTube operations"),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <YoutubeAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}