use anyhow::{anyhow, bail, Result};
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://inv.riverside.rocks",
"https://invidious.osi.kr",
"https://youtube.076.ne.jp",
"https://y.com.sb",
"https://yt.artemislena.eu",
"https://invidious.tiekoetter.com",
];
const INVIDIOUS_DOMAINS: &str = "https://api.invidious.io/instances.json?sort_by=type,users";
#[derive(Clone)]
pub struct Instance {
pub domain: Option<String>,
client: Agent,
query: Option<String>,
}
impl PartialEq for Instance {
fn eq(&self, other: &Self) -> bool {
self.domain == other.domain
}
}
impl Eq for Instance {}
#[derive(Clone, PartialEq, Eq)]
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 = vec![];
if let Ok(domain_list) = Self::get_invidious_instance_list(&client) {
domains = domain_list;
} else {
for item in &INVIDIOUS_INSTANCE_LIST {
domains.push((*item).to_string());
}
}
domains.shuffle(&mut rand::thread_rng());
let mut video_result: Vec<YoutubeVideo> = Vec::new();
for v in domains {
let url = format!("{v}/api/v1/search");
if let Ok(result) = client
.get(&url)
.query("q", query)
.query("page", "1")
.query("type", "video")
.query("sort_by", "relevance")
.call()
{
if result.status() == 200 {
if let Ok(text) = result.into_string() {
if let Some(vr) = Self::parse_youtube_options(&text) {
video_result = vr;
domain = v;
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>> {
if self.domain.is_none() {
bail!("No server available");
}
let url = format!("{}/api/v1/search", self.domain.as_ref().unwrap());
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 url = format!(
"http://suggestqueries.google.com/complete/search?client=firefox&ds=yt&q={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>> {
if self.domain.is_none() {
bail!("No server available");
}
let url = format!(
"{}/api/v1/trending?type=music®ion={region}",
self.domain.as_ref().unwrap()
);
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() {
if let Some((title, video_id, length_seconds)) = Self::parse_youtube_item(v) {
vec.push(YoutubeVideo {
title,
length_seconds,
video_id,
});
}
}
return Some(vec);
}
}
None
}
fn parse_youtube_item(value: &Value) -> Option<(String, String, u64)> {
let title = value.get("title")?.as_str()?.to_owned();
let video_id = value.get("videoId")?.as_str()?.to_owned();
let length_seconds = value.get("lengthSeconds")?.as_u64()?;
Some((title, video_id, length_seconds))
}
fn get_invidious_instance_list(client: &Agent) -> Result<Vec<String>> {
let result = client.get(INVIDIOUS_DOMAINS).call()?.into_string()?;
if let Some(vec) = Self::parse_invidious_instance_list(&result) {
return Ok(vec);
}
bail!("no instance list fetched")
}
fn parse_invidious_instance_list(data: &str) -> Option<Vec<String>> {
if let Ok(value) = serde_json::from_str::<Value>(data) {
let mut vec = Vec::new();
if let Some(array) = value.as_array() {
for inner_value in array.iter() {
if let Some((uri, health)) = Self::parse_instance(inner_value) {
if health > 95.0 {
vec.push(uri);
}
}
}
}
if !vec.is_empty() {
return Some(vec);
}
}
None
}
fn parse_instance(value: &Value) -> Option<(String, f64)> {
let obj = value.get(1)?.as_object()?;
if obj.get("api")?.as_bool()? {
let uri = obj.get("uri")?.as_str()?.to_owned();
let health = obj
.get("monitor")?
.as_object()?
.get("30dRatio")?
.get("ratio")?
.as_str()?
.to_owned()
.parse::<f64>()
.ok();
health.map(|health| (uri, health))
} else {
None
}
}
}