yt-sub-core 0.2.2

yt-sub core library
Documentation
use std::fmt::{self, Display, Formatter};

use chrono::{DateTime, Utc};
use eyre::Result;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::{user_settings::API_HOST, video::Video};

const RSS_HOST: &str = "https://www.youtube.com";

#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
pub struct Channel {
    pub handle: String,
    pub description: String,
    pub channel_id: String,
}

impl Display for Channel {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "name: {name}
handle: {handle}
channel_id: {channel_id}
channel_url: {channel_url}
RSS feed: {channel_feed}",
            name = self.description,
            handle = self.handle,
            channel_id = self.channel_id,
            channel_url = self.url(),
            channel_feed = self.rss_url()
        )
    }
}

impl Channel {
    pub fn url(&self) -> String {
        format!("https://www.youtube.com/{}", self.handle)
    }

    pub fn rss_url(&self) -> String {
        format!(
            "{}/feeds/videos.xml?channel_id={}",
            RSS_HOST, self.channel_id
        )
    }

    pub async fn validate_id(channel_id: &str, host: Option<&str>) -> Result<bool> {
        let host = host.unwrap_or(RSS_HOST);
        let client = Client::new();
        let res = client
            .get(format!(
                "{}/feeds/videos.xml?channel_id={}",
                host, channel_id,
            ))
            .send()
            .await?;

        Ok(res.status() == 200)
    }

    pub async fn get_data(handle: &str, host: Option<&str>) -> Result<(String, String)> {
        let host = host.unwrap_or(API_HOST);
        let client = Client::new();

        let res = client
            .get(format!("{}/channel_data/{}", host, handle))
            .send()
            .await?;
        if res.status() == 404 {
            eyre::bail!("Channel with handle '{handle}' not found!")
        }

        if res.status() == 503 {
            eyre::bail!(
                "It looks like YouTube API calls are currently throttled.

You can try again later or find the channel data manually:
https://github.com/pawurb/yt-sub-rs#manually-finding-an-rss-channel_id"
            );
        }

        let res_json: Value = res.json().await?;
        let channel_id = res_json["channel_id"].as_str().unwrap();
        let channel_name = res_json["channel_name"].as_str().unwrap();

        Ok((channel_id.to_string(), channel_name.to_string()))
    }

    pub async fn get_fresh_videos(&self, last_run_at: DateTime<Utc>) -> Result<Vec<Video>> {
        let rss = self.get_rss_data().await?;
        let videos = Video::parse_rss(rss)?;

        let videos: Vec<Video> = videos
            .into_iter()
            .filter(|video| video.published_at > last_run_at)
            .collect();

        Ok(videos)
    }

    async fn get_rss_data(&self) -> Result<String> {
        let client = Client::new();
        let res = client.get(self.rss_url()).send().await?;
        Ok(res.text().await?)
    }
}

#[cfg(test)]
mod tests {
    use mockito::Server;

    use super::*;
    #[tokio::test]
    async fn test_validate_channel() -> Result<()> {
        let mut server = Server::new_async().await;
        let host = server.host_with_port();
        let host = format!("http://{}", host);
        let m1 = server
            .mock(
                "GET",
                "/feeds/videos.xml?channel_id=UC_iD0xppBwwsrM9DegC5cQQ",
            )
            .with_status(200)
            .create_async()
            .await;
        let m2 = server
            .mock("GET", "/feeds/videos.xml?channel_id=UC_invalid")
            .with_status(404)
            .create_async()
            .await;

        let correct_res = Channel::validate_id("UC_iD0xppBwwsrM9DegC5cQQ", Some(&host)).await?;
        assert!(correct_res);

        let incorrect_res = Channel::validate_id("UC_invalid", Some(&host)).await?;
        assert!(!incorrect_res);

        m1.assert_async().await;
        m2.assert_async().await;

        Ok(())
    }

    #[tokio::test]
    async fn test_get_channel_data() -> Result<()> {
        let mut server = Server::new_async().await;
        let host = server.host_with_port();
        let host = format!("http://{}", host);
        let m = server
            .mock("GET", "/channel_data/@Test_handle")
            .with_body(
                r#"{
            "channel_id": "UC_iD0xppBwwsrM9DegC5cQQ",
            "channel_name": "Test Channel"
        }"#,
            )
            .create_async()
            .await;

        let (channel_id, channel_name) = Channel::get_data("@Test_handle", Some(&host)).await?;
        assert_eq!(channel_id, "UC_iD0xppBwwsrM9DegC5cQQ");
        assert_eq!(channel_name, "Test Channel");

        m.assert_async().await;

        Ok(())
    }
}