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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
use crate::{
    Error,
    GetVideoResponse,
    MainPage,
};
use scraper::Html;

const DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"));

/// Client
#[derive(Clone, Debug)]
pub struct Client {
    /// The inner http client
    pub client: reqwest::Client,
}

impl Client {
    /// Makes a new [`Client`].
    ///
    /// # Panics
    /// Panics if the [`Client`] could not be created.
    pub fn new() -> Self {
        let client = reqwest::ClientBuilder::new()
            .cookie_store(true)
            .user_agent(DEFAULT_USER_AGENT)
            .build()
            .expect("failed to build client");

        Client { client }
    }

    /// Gets [`MainPage`] data.
    ///
    /// Useful only to fetch csrf token and pass it to another api call.
    ///
    /// # Errors
    /// Returns an error if the [`MainPage`] could not be fetched.
    pub async fn get_main_page(&self) -> Result<MainPage, Error> {
        let body = self
            .client
            .get("https://www.redd.tube/")
            .send()
            .await?
            .error_for_status()?
            .text()
            .await?;

        Ok(tokio::task::spawn_blocking(move || {
            let html = Html::parse_document(body.as_str());
            MainPage::from_html(&html)
        })
        .await??)
    }

    /// Get a video for a reddit url.
    ///
    /// `main_page` is exposed publicly as the same [`MainPage`] may be used for multiple [`Client::get_video`] calls as long as they are close together chronologically,
    /// most likely at least a few seconds or minutes
    ///
    /// Calling [`Client::get_main_page`] will also aquire a new session cookie if necessary,
    /// so make sure to call get_main_page to refresh the csrf token if it expires
    ///
    /// # Errors
    /// Returns an error if the video url could not be fetched.
    pub async fn get_video(
        &self,
        main_page: &MainPage,
        url: &str,
    ) -> Result<GetVideoResponse, Error> {
        Ok(self
            .client
            .post("https://www.redd.tube/services/get_video")
            .form(&[
                ("url", url),
                ("zip", ""),
                ("hash", ""),
                (&main_page.csrf_key, &main_page.csrf_value),
            ])
            .send()
            .await?
            .error_for_status()?
            .json()
            .await?)
    }
}

impl Default for Client {
    fn default() -> Client {
        Client::new()
    }
}