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