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 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 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}