modules/
network.rs

1use std::{error::Error, time::Instant, fs::File, io::prelude::*};
2use colored::*;
3use crate::utils::*;
4use serde_json::Value;
5use regex::Regex;
6use reqwest;
7use urlencoding::encode;
8use invidious::{*, hidden::AdaptiveFormat};
9
10pub struct TwitchClient;
11
12impl TwitchClient {
13    pub async fn fetch(&self, id: &str, type_: &str) -> Result<(String, String), Box<dyn Error>> {
14        match type_ {
15            "twitch-video" => self.fetch_video(id).await,
16            "twitch-clip" => self.fetch_clip(id).await,
17            _ => Err("Invalid type".into()),
18        }
19    }
20
21    async fn fetch_clip(&self, id: &str) -> Result<(String, String), Box<dyn Error>> {
22        let data = self.get_data(id).await?;
23        let title = self.get_title(id, "clip").await?;
24        let download_url = format!(
25            "{}?sig={}&token={}",
26            data["videoQualities"][0]["sourceURL"].as_str().unwrap_or_default(),
27            data["playbackAccessToken"]["signature"].as_str().unwrap_or_default(),
28            encode(data["playbackAccessToken"]["value"].as_str().unwrap_or_default())
29        );
30
31        Ok((download_url, title))
32    }
33
34    async fn fetch_video(&self, id: &str) -> Result<(String, String), Box<dyn Error>> {
35        let client = reqwest::Client::new();
36        let title = self.get_title(id, "video").await?;
37        let playlist_url = self.get_playlist_url(id, &client).await?;
38
39        Ok((playlist_url, title))
40    }
41
42    async fn send_gql_request(query: String) -> Result<Value, Box<dyn Error>> {
43        let client = reqwest::Client::new();
44        let response = client
45            .post("https://gql.twitch.tv/gql")
46            .header("Client-ID", "kimne78kx3ncx6brgo4mv6wki5h1ko")
47            .body(query)
48            .send()
49            .await?;
50
51        if !response.status().is_success() {
52            return Err("Unsuccessful response (GQL API)".into());
53        }
54
55        let json_response: Value = serde_json::from_str(&response.text().await?)?;
56        Ok(json_response)
57    }
58
59    async fn get_data(&self, id: &str) -> Result<Value, Box<dyn Error>> {
60        let query = format!(r#"{{"operationName":"VideoAccessToken_Clip","variables":{{"slug":"{}"}},"extensions":{{"persistedQuery":{{"version":1,"sha256Hash":"36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11"}}}}}}"#, id);
61        let json_response = Self::send_gql_request(query).await?;
62        let data = &json_response["data"]["clip"];
63        if data.is_null() {
64            return Err("Clip not found".into());
65        }
66
67        Ok(data.clone())
68    }
69
70    async fn get_title(&self, id: &str, type_: &str) -> Result<String, Box<dyn Error>> {
71        let query = match type_ {
72            "video" => format!(r#"{{"query":"query{{video(id:\"{}\"){{title}}}}","variables":{{}}}}"#, id),
73            "clip" => format!(r#"{{"query":"query{{clip(slug:\"{}\"){{title}}}}","variables":{{}}}}"#, id),
74            _ => return Err("Invalid type".into()),
75        };
76
77        let json_response = Self::send_gql_request(query).await?;
78        let title = json_response["data"][type_]["title"].to_string().replace("\"", "");
79
80        Ok(title)
81    }
82
83    async fn get_playlist_url(&self, id: &str, client: &reqwest::Client) -> Result<String, Box<dyn Error>> {
84        let (token, signature) = self.get_token_and_sig(id).await?;
85        let playlist_url = format!("http://usher.ttvnw.net/vod/{}?nauth={}&nauthsig={}&allow_source=true&player=twitchweb", id, token, signature);
86        let playlist_response = client.get(&playlist_url).send().await?;
87        if !playlist_response.status().is_success() {
88            return Err("Unsuccessful response (Usher API)".into());
89        }
90
91        let playlist_body = playlist_response.text().await?;
92        let playlist_url = self.get_highest_bandwidth_url(&playlist_body).await?;
93
94        Ok(playlist_url)
95    }
96
97    async fn get_token_and_sig(&self, id: &str) -> Result<(String, String), Box<dyn Error>> {
98        let query = format!(r#"{{"operationName":"PlaybackAccessToken_Template","query":"query PlaybackAccessToken_Template($vodID: ID!, $playerType: String!) {{  videoPlaybackAccessToken(id: $vodID, params: {{platform: \"web\", playerBackend: \"mediaplayer\", playerType: $playerType}}) @include(if: true) {{    value    signature    __typename  }}}}", "variables":{{"vodID":"{}","playerType":"embed"}}}}"#, id);
99        let response = Self::send_gql_request(query).await?;
100        let token = response["data"]["videoPlaybackAccessToken"]["value"].as_str().unwrap_or_default();
101        let signature = response["data"]["videoPlaybackAccessToken"]["signature"].as_str().unwrap_or_default();
102
103        Ok((token.into(), signature.into()))
104    }
105
106    async fn get_highest_bandwidth_url(&self, playlist_body: &str) -> Result<String, Box<dyn Error>> {
107        let re = Regex::new(r"#EXT-X-STREAM-INF:BANDWIDTH=(\d+),.*\n(.*)\n")?;
108
109        let mut highest_bandwidth_url = String::new();
110        let mut highest_bandwidth = 0;
111
112        for cap in re.captures_iter(playlist_body) {
113            let bandwidth: i32 = cap[1].parse()?;
114            if bandwidth > highest_bandwidth {
115                highest_bandwidth = bandwidth;
116                highest_bandwidth_url = cap[2].to_string();
117            }
118        }
119
120        Ok(highest_bandwidth_url)
121    }
122}
123
124pub struct YouTubeClient;
125
126impl YouTubeClient {
127    pub async fn fetch(&self, id: &str) -> Result<(String, String), Box<dyn Error>> {
128        let client = ClientAsync::default();
129        let video = client.video(&id, None).await?;
130        let title = video.title;
131        let url = Self::get_highest_bitrate_url(&video.adaptive_formats);
132
133        Ok((url, title))
134    }
135
136    fn get_highest_bitrate_url(formats: &Vec<AdaptiveFormat>) -> String {
137        let mut highest_bitrate = 0;
138        let mut url = String::new();
139        for format in formats {
140            let bitrate = format.bitrate.parse::<u64>().unwrap();
141            if bitrate > highest_bitrate {
142                highest_bitrate = bitrate;
143                url = format.url.clone();
144            }
145        }
146
147        url
148    }
149}
150
151pub struct TikTokClient;
152
153impl TikTokClient {
154    pub async fn fetch(&self, url: &str) -> Result<(String, String), Box<dyn Error>> {
155        // Get Video Details
156        let details = reqwest::get(&format!("https://www.tiktok.com/oembed?url={}", url)).await?;
157        if !details.status().is_success() {
158            return Err("Unsuccessful response (TikTok API)".into());
159        }
160
161        let json_response: Value = serde_json::from_str(&details.text().await?)?;
162        let title = json_response["title"].to_string().replace("\"", "");
163
164        // Get Video URL
165        // scrape the main url and look for the <video> tag, then return the src attribute
166        let embedded_video = reqwest::get(url).await?;
167        if !embedded_video.status().is_success() {
168            return Err("Unsuccessful response (Tiktok may be down)".into());
169        }
170
171        let embedded_video_body = embedded_video.text().await?;
172
173        File::create("tiktok.html")?.write_all(embedded_video_body.as_bytes())?;
174        
175        Ok((String::new(), title))
176    }
177}
178
179pub async fn fetch(type_: &str, id: &str) -> Result<(String, String), Box<dyn Error>> {
180    let start = Instant::now();
181
182    let result = match type_ {
183        "twitch-video" | "twitch-clip" => TwitchClient.fetch(id, type_).await,
184        "youtube-video" | "youtube-short" => YouTubeClient.fetch(id).await,
185        "tiktok-video" => TikTokClient.fetch(id).await,
186        _ => Err("Invalid type".into()),
187    };
188
189    println!("{} {}", "Fetched URL in:".blue(), get_elapsed_time(start));
190
191    Ok(result?)
192}