use anyhow::{anyhow, bail, Result};
use if_chain::if_chain;
use rand::seq::SliceRandom;
use serde_json::Value;
use std::time::Duration;
use ureq::{Agent, AgentBuilder};
const INVIDIOUS_INSTANCE_LIST: [&str; 7] = [
"https://vid.puffyan.us",
"https://ytprivate.com",
"https://invidio.xamh.de",
"https://youtube.076.ne.jp",
"https://y.com.cm",
"https://invidious.hub.ne.kr",
"https://invidious.namazso.eu",
];
pub struct Instance {
pub domain: Option<String>,
client: Agent,
query: Option<String>,
}
pub struct YoutubeVideo {
pub title: String,
pub length_seconds: u64,
pub video_id: String,
}
impl Default for Instance {
fn default() -> Self {
let client = Agent::new();
let domain = Some(String::new());
let query = Some(String::new());
Self {
domain,
client,
query,
}
}
}
#[allow(unused)]
impl Instance {
pub fn new(query: &str) -> Result<(Self, Vec<YoutubeVideo>)> {
let client = AgentBuilder::new().timeout(Duration::from_secs(10)).build();
let mut domain = String::new();
let mut domains = INVIDIOUS_INSTANCE_LIST;
let mut video_result: Vec<YoutubeVideo> = Vec::new();
domains.shuffle(&mut rand::thread_rng());
for v in domains {
let mut url: String = v.to_string();
url.push_str("/api/v1/search");
if_chain! {
if let Ok(result) = client.get(&url).query("q", query).query("page", "1").call();
if result.status() == 200;
if let Ok(text) = result.into_string();
if let Some(vr) = Self::parse_youtube_options(&text);
then {
video_result = vr;
domain = v.to_string();
break;
}
}
}
if domain.len() < 2 {
bail!("All 7 invidious servers are down? Please check your network connection first.");
}
let domain = Some(domain);
Ok((
Self {
domain,
client,
query: Some(query.to_string()),
},
video_result,
))
}
pub fn get_search_query(&self, page: u32) -> Result<Vec<YoutubeVideo>> {
let mut url = String::new();
if let Some(u) = &self.domain {
url.push_str(u);
}
url.push_str("/api/v1/search");
let query = match &self.query {
Some(q) => q,
None => bail!("No query string found"),
};
let result = self
.client
.get(&url)
.query("q", query)
.query("page", &page.to_string())
.call()?;
match result.status() {
200 => match result.into_string() {
Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
Err(e) => bail!("Error during search: {}", e),
},
_ => bail!("Error during search"),
}
}
pub fn get_suggestions(&self, prefix: &str) -> Result<Vec<YoutubeVideo>> {
let mut url =
"http://suggestqueries.google.com/complete/search?client=firefox&ds=yt&q=".to_string();
url.push_str(prefix);
let result = self.client.get(&url).call()?;
match result.status() {
200 => match result.into_string() {
Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
Err(e) => bail!("Error during search: {}", e),
},
_ => bail!("Error during search"),
}
}
pub fn get_trending_music(&self, region: &str) -> Result<Vec<YoutubeVideo>> {
let mut url = String::new();
if let Some(u) = &self.domain {
url.push_str(u);
}
url.push_str("/api/v1/trending?");
url.push_str("type=music®ion=");
url.push_str(region);
let result = self.client.get(&url).call()?;
match result.status() {
200 => match result.into_string() {
Ok(text) => Self::parse_youtube_options(&text).ok_or_else(|| anyhow!("None Error")),
_ => bail!("Error during search"),
},
_ => bail!("Error during search"),
}
}
fn parse_youtube_options(data: &str) -> Option<Vec<YoutubeVideo>> {
if let Ok(value) = serde_json::from_str::<Value>(data) {
let mut vec: Vec<YoutubeVideo> = Vec::new();
if let Some(array) = value.as_array() {
for v in array.iter() {
vec.push(YoutubeVideo {
title: v.get("title")?.as_str()?.to_owned(),
video_id: v.get("videoId")?.as_str()?.to_owned(),
length_seconds: v.get("lengthSeconds")?.as_u64()?,
});
}
return Some(vec);
}
}
None
}
}