1use anyhow::{Result, anyhow, bail};
2use rand::seq::SliceRandom;
3use serde_json::Value;
4use reqwest::{Client, ClientBuilder, StatusCode};
7use std::time::Duration;
8
9const INVIDIOUS_INSTANCE_LIST: [&str; 5] = [
10 "https://inv.nadeko.net",
11 "https://invidious.nerdvpn.de",
12 "https://yewtu.be",
13 "https://y.com.sb",
17 "https://yt.artemislena.eu",
18 ];
28
29const INVIDIOUS_DOMAINS: &str = "https://api.invidious.io/instances.json?sort_by=type,users";
30
31#[derive(Clone, Debug)]
32pub struct Instance {
33 pub domain: Option<String>,
34 client: Client,
35 query: Option<String>,
36}
37
38impl PartialEq for Instance {
39 fn eq(&self, other: &Self) -> bool {
40 self.domain == other.domain
41 }
42}
43
44impl Eq for Instance {}
45
46#[derive(Clone, PartialEq, Eq, Debug)]
47pub struct YoutubeVideo {
48 pub title: String,
49 pub length_seconds: u64,
50 pub video_id: String,
51}
52
53impl Default for Instance {
54 fn default() -> Self {
55 let client = Client::new();
56 let domain = Some(String::new());
57 let query = Some(String::new());
58
59 Self {
60 domain,
61 client,
62 query,
63 }
64 }
65}
66
67impl Instance {
68 pub async fn new(query: &str) -> Result<(Self, Vec<YoutubeVideo>)> {
69 let client = ClientBuilder::new()
70 .timeout(Duration::from_secs(10))
71 .build()?;
72
73 let mut domain = String::new();
74 let mut domains = vec![];
75
76 if let Ok(domain_list) = Self::get_invidious_instance_list(&client).await {
78 domains = domain_list;
79 } else {
80 for item in &INVIDIOUS_INSTANCE_LIST {
81 domains.push((*item).to_string());
82 }
83 }
84
85 domains.shuffle(&mut rand::rng());
86
87 let mut video_result: Vec<YoutubeVideo> = Vec::new();
88 for v in domains {
89 let url = format!("{v}/api/v1/search");
90
91 let query_vec = vec![
92 ("q", query),
93 ("page", "1"),
94 ("type", "video"),
95 ("sort_by", "relevance"),
96 ];
97 if let Ok(result) = client.get(&url).query(&query_vec).send().await {
98 if result.status() == 200 {
99 if let Ok(text) = result.text().await {
100 if let Some(vr) = Self::parse_youtube_options(&text) {
101 video_result = vr;
102 domain = v;
103 break;
104 }
105 }
106 }
107 }
108 }
109 if domain.len() < 2 {
110 bail!("Something is wrong with your connection or all 7 invidious servers are down.");
111 }
112
113 let domain = Some(domain);
114 Ok((
115 Self {
116 domain,
117 client,
118 query: Some(query.to_string()),
119 },
120 video_result,
121 ))
122 }
123
124 pub async fn get_search_query(&self, page: u32) -> Result<Vec<YoutubeVideo>> {
126 if self.domain.is_none() {
127 bail!("No server available");
128 }
129 let url = format!(
130 "{}/api/v1/search",
131 self.domain
132 .as_ref()
133 .ok_or(anyhow!("error in domain name"))?
134 );
135
136 let Some(query) = &self.query else {
137 bail!("No query string found")
138 };
139
140 let result = self
141 .client
142 .get(url)
143 .query(&[("q", query), ("page", &page.to_string())])
144 .send()
145 .await?;
146
147 match result.status() {
148 StatusCode::OK => match result.text().await {
149 Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
150 Err(e) => bail!("Error during search: {}", e),
151 },
152 _ => bail!("Error during search"),
153 }
154 }
155
156 pub async fn get_suggestions(&self, prefix: &str) -> Result<Vec<YoutubeVideo>> {
159 let url = format!(
160 "http://suggestqueries.google.com/complete/search?client=firefox&ds=yt&q={prefix}"
161 );
162 let result = self.client.get(url).send().await?;
163 match result.status() {
164 StatusCode::OK => match result.text().await {
165 Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
166 Err(e) => bail!("Error during search: {}", e),
167 },
168 _ => bail!("Error during search"),
169 }
170 }
171
172 pub async fn get_trending_music(&self, region: &str) -> Result<Vec<YoutubeVideo>> {
175 if self.domain.is_none() {
176 bail!("No server available");
177 }
178 let url = format!(
179 "{}/api/v1/trending?type=music®ion={region}",
180 self.domain
181 .as_ref()
182 .ok_or(anyhow!("error in domain names"))?
183 );
184
185 let result = self.client.get(url).send().await?;
186
187 match result.status() {
188 StatusCode::OK => match result.text().await {
189 Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
190 _ => bail!("Error during search"),
191 },
192 _ => bail!("Error during search"),
193 }
194 }
195
196 fn parse_youtube_options(data: &str) -> Option<Vec<YoutubeVideo>> {
197 if let Ok(value) = serde_json::from_str::<Value>(data) {
198 let mut vec: Vec<YoutubeVideo> = Vec::new();
199 if let Some(array) = value.as_array() {
203 for v in array {
204 if let Some((title, video_id, length_seconds)) = Self::parse_youtube_item(v) {
205 vec.push(YoutubeVideo {
206 title,
207 length_seconds,
208 video_id,
209 });
210 }
211 }
212 return Some(vec);
213 }
214 }
215 None
216 }
217
218 fn parse_youtube_item(value: &Value) -> Option<(String, String, u64)> {
219 let title = value.get("title")?.as_str()?.to_owned();
220 let video_id = value.get("videoId")?.as_str()?.to_owned();
221 let length_seconds = value.get("lengthSeconds")?.as_u64()?;
222 Some((title, video_id, length_seconds))
223 }
224
225 async fn get_invidious_instance_list(client: &Client) -> Result<Vec<String>> {
226 let result = client.get(INVIDIOUS_DOMAINS).send().await?.text().await?;
227 if let Some(vec) = Self::parse_invidious_instance_list(&result) {
231 return Ok(vec);
232 }
233 bail!("no instance list fetched")
234 }
235
236 fn parse_invidious_instance_list(data: &str) -> Option<Vec<String>> {
237 if let Ok(value) = serde_json::from_str::<Value>(data) {
238 let mut vec = Vec::new();
239 if let Some(array) = value.as_array() {
240 for inner_value in array {
241 if let Some((uri, health)) = Self::parse_instance(inner_value) {
242 if health > 95.0 {
243 vec.push(uri);
244 }
245 }
246 }
247 }
248 if !vec.is_empty() {
249 return Some(vec);
250 }
251 }
252 None
253 }
254
255 fn parse_instance(value: &Value) -> Option<(String, f64)> {
256 let obj = value.get(1)?.as_object()?;
257 if obj.get("api")?.as_bool()? {
258 let uri = obj.get("uri")?.as_str()?.to_owned();
259 let health = obj
260 .get("monitor")?
261 .as_object()?
262 .get("30dRatio")?
263 .get("ratio")?
264 .as_str()?
265 .to_owned()
266 .parse::<f64>()
267 .ok();
268 health.map(|health| (uri, health))
269 } else {
270 None
271 }
272 }
273}