gdelt 0.1.0

CLI for GDELT Project - optimized for agentic usage with local data caching
//! TV 2.0 API and TV AI API clients.
//!
//! The TV API provides access to television news broadcasts,
//! including caption search, clips, and coverage timelines.

use crate::api::client::{endpoints, GdeltClient};
use crate::error::Result;
use serde::{Deserialize, Serialize};
use tracing::instrument;

/// TV 2.0 API client
#[derive(Debug, Clone)]
pub struct TvApi {
    client: GdeltClient,
}

impl TvApi {
    /// Create a new TV API client
    pub fn new(client: GdeltClient) -> Self {
        Self { client }
    }

    /// Search TV captions
    ///
    /// Note: The TV API requires a station parameter for clip searches.
    /// Use `stations()` to get a list of available stations.
    #[instrument(skip(self))]
    pub async fn search(&self, params: TvSearchParams) -> Result<TvSearchResponse> {
        // TV API requires station for ClipGallery mode
        if params.station.is_none() {
            return Err(crate::error::GdeltError::Validation(
                "TV search requires a station parameter. Use --station to specify one \
                (e.g., --station CNN, --station FOXNEWS, --station MSNBC). \
                Run 'gdelt tv stations' to see available stations.".to_string()
            ));
        }
        let url = self.build_search_url(&params);
        self.client.get_json(&url).await
    }

    /// Get video clips
    #[instrument(skip(self))]
    pub async fn clips(&self, params: TvClipsParams) -> Result<TvClipsResponse> {
        let url = self.build_clips_url(&params);
        self.client.get_json(&url).await
    }

    /// Get coverage timeline
    #[instrument(skip(self))]
    pub async fn timeline(&self, params: TvTimelineParams) -> Result<TvTimelineResponse> {
        let url = self.build_timeline_url(&params);
        self.client.get_json(&url).await
    }

    /// Get list of stations
    #[instrument(skip(self))]
    pub async fn stations(&self, params: TvStationsParams) -> Result<TvStationsResponse> {
        let url = self.build_stations_url(&params);
        self.client.get_json(&url).await
    }

    fn build_search_url(&self, params: &TvSearchParams) -> String {
        let max_records = params.max_records.unwrap_or(250).to_string();
        let mut query_params = vec![
            ("query", params.query.as_str()),
            ("mode", "ClipGallery"),
            ("format", "json"),
            ("maxrecords", &max_records),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }
        if let Some(ref station) = params.station {
            query_params.push(("station", station));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }

    fn build_clips_url(&self, params: &TvClipsParams) -> String {
        let mut query_params = vec![
            ("query", params.query.as_str()),
            ("mode", "ClipGallery"),
            ("format", "json"),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }

    fn build_timeline_url(&self, params: &TvTimelineParams) -> String {
        let mode = if params.by_station {
            "TimelineVolStation"
        } else {
            "TimelineVol"
        };

        let mut query_params = vec![
            ("query", params.query.as_str()),
            ("mode", mode),
            ("format", "json"),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }

    fn build_stations_url(&self, params: &TvStationsParams) -> String {
        let mut query_params = vec![
            ("mode", "StationList"),
            ("format", "json"),
        ];

        if let Some(ref country) = params.country {
            query_params.push(("country", country));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }
}

/// TV AI API client
#[derive(Debug, Clone)]
pub struct TvAiApi {
    client: GdeltClient,
}

impl TvAiApi {
    /// Create a new TV AI API client
    pub fn new(client: GdeltClient) -> Self {
        Self { client }
    }

    /// Search AI-analyzed content
    #[instrument(skip(self))]
    pub async fn search(&self, params: TvAiSearchParams) -> Result<TvAiSearchResponse> {
        let url = self.build_search_url(&params);
        self.client.get_json(&url).await
    }

    /// Search by visual concepts
    #[instrument(skip(self))]
    pub async fn concepts(&self, params: TvAiConceptsParams) -> Result<TvAiConceptsResponse> {
        let url = self.build_concepts_url(&params);
        self.client.get_json(&url).await
    }

    /// Search by visual entities
    #[instrument(skip(self))]
    pub async fn visual(&self, params: TvAiVisualParams) -> Result<TvAiVisualResponse> {
        let url = self.build_visual_url(&params);
        self.client.get_json(&url).await
    }

    fn build_search_url(&self, params: &TvAiSearchParams) -> String {
        let mut query_params = vec![
            ("query", params.query.as_str()),
            ("mode", "ClipGallery"),
            ("format", "json"),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }
        if params.captions {
            query_params.push(("searchcaptions", "1"));
        }
        if params.ocr {
            query_params.push(("searchocr", "1"));
        }
        if params.asr {
            query_params.push(("searchasr", "1"));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }

    fn build_concepts_url(&self, params: &TvAiConceptsParams) -> String {
        let mut query_params = vec![
            ("concept", params.concept.as_str()),
            ("mode", "ClipGallery"),
            ("format", "json"),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }

    fn build_visual_url(&self, params: &TvAiVisualParams) -> String {
        let mut query_params = vec![
            ("entity", params.entity.as_str()),
            ("mode", "ClipGallery"),
            ("format", "json"),
        ];

        if let Some(ref timespan) = params.timespan {
            query_params.push(("timespan", timespan));
        }

        GdeltClient::build_url(endpoints::TV_API, &query_params)
    }
}

// TV 2.0 API types

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvSearchParams {
    pub query: String,
    pub timespan: Option<String>,
    pub station: Option<String>,
    pub max_records: Option<u32>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvClipsParams {
    pub query: String,
    pub timespan: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvTimelineParams {
    pub query: String,
    pub timespan: Option<String>,
    pub by_station: bool,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvStationsParams {
    pub country: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvSearchResponse {
    #[serde(default)]
    pub clips: Vec<TvClip>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvClip {
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub preview: Option<String>,
    #[serde(default)]
    pub station: String,
    #[serde(default)]
    pub show: Option<String>,
    #[serde(default)]
    pub date: String,
    #[serde(default)]
    pub snippet: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvClipsResponse {
    #[serde(default)]
    pub clips: Vec<TvClip>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvTimelineResponse {
    #[serde(default)]
    pub timeline: Vec<TvTimelineEntry>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvTimelineEntry {
    pub date: String,
    #[serde(default)]
    pub value: f64,
    #[serde(default)]
    pub station: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvStationsResponse {
    #[serde(default)]
    pub stations: Vec<TvStation>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvStation {
    pub id: String,
    pub name: String,
    #[serde(default)]
    pub country: Option<String>,
    #[serde(default)]
    pub market: Option<String>,
}

// TV AI API types

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvAiSearchParams {
    pub query: String,
    pub timespan: Option<String>,
    pub captions: bool,
    pub ocr: bool,
    pub asr: bool,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvAiConceptsParams {
    pub concept: String,
    pub timespan: Option<String>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct TvAiVisualParams {
    pub entity: String,
    pub timespan: Option<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvAiSearchResponse {
    #[serde(default)]
    pub clips: Vec<TvAiClip>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvAiClip {
    #[serde(default)]
    pub url: String,
    #[serde(default)]
    pub preview: Option<String>,
    #[serde(default)]
    pub station: String,
    #[serde(default)]
    pub date: String,
    #[serde(default)]
    pub snippet: Option<String>,
    #[serde(default)]
    pub concepts: Vec<String>,
    #[serde(default)]
    pub entities: Vec<String>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvAiConceptsResponse {
    #[serde(default)]
    pub clips: Vec<TvAiClip>,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TvAiVisualResponse {
    #[serde(default)]
    pub clips: Vec<TvAiClip>,
}