1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
use regex::Regex;
use reqwest::header::{HeaderMap, HeaderValue};
use serde::export::Formatter;
use std::error::Error;
use std::fmt::Display;

use crate::api::ApiResponse;

#[derive(Debug)]
struct ClientBuildError;

impl Display for ClientBuildError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        write!(f, "Failed to build a client.")
    }
}

impl Error for ClientBuildError {}

pub(crate) struct Client {
    client: reqwest::Client,
}

impl Client {
    pub fn new(client: reqwest::Client) -> Self {
        Self { client }
    }

    pub fn create(name: &str, version: &str) -> Result<Self, Box<dyn Error>> {
        let mut builder = reqwest::Client::builder();
        let mut headers = HeaderMap::new();

        headers.insert("X-YouTube-Client-Name", HeaderValue::from_str(name)?);
        headers.insert("X-YouTube-Client-Version", HeaderValue::from_str(version)?);
        builder = builder.default_headers(headers);

        Ok(Self::new(builder.build()?))
    }

    #[allow(clippy::clone_double_ref)]
    pub async fn build() -> Result<Self, Box<dyn Error>> {
        let html = reqwest::get("https://www.youtube.com/")
            .await?
            .text()
            .await?;

        let pattern = Regex::new(r#""clientVersion":"([\d.]+)""#)?;
        let captures = pattern.captures(&html).unwrap();
        let version = captures.get(1).ok_or(ClientBuildError {})?.as_str();

        Self::create("1", version.clone())
    }

    pub async fn fetch_upcoming_live_streams(
        &self,
        channel_id: &str,
    ) -> Result<ApiResponse, Box<dyn Error>> {
        let url = format!(
            "https://www.youtube.com/channel/{}/videos?view=2&live_view=502&pbj=1",
            channel_id
        );

        Ok(self.client.get(&url).send().await?.json().await?)
    }
}