use crate::{YoutubeFormat, YoutubeSorts};
use alux_ext::ext;
use ambassador::delegatable_trait;
#[delegatable_trait]
pub trait YoutubeUrlAlg {
fn throttle_challenge(&self, url: &str) -> Option<String>;
fn with_throttle(&self, url: &str, solution: &str) -> String;
fn with_signature(&self, url: &str, parameter: &str, signature: &str) -> String;
}
#[delegatable_trait]
pub trait YoutubeChallengeAlg: YoutubeSorts {
type Error;
fn solve_challenges(
&self,
program: &str,
challenges: impl IntoIterator<Item = YoutubeChallenge>,
) -> Result<Self::Solutions, Self::Error>;
}
#[delegatable_trait]
pub trait YoutubeSolutionAlg: YoutubeSorts {
fn solution_of(&self, solutions: &Self::Solutions, challenge: &YoutubeChallenge) -> Option<String>;
}
pub const DEFAULT_SIGNATURE_PARAMETER: &str = "signature";
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum YoutubeChallenge {
Signature(String),
Throttle(String),
}
impl YoutubeChallenge {
#[must_use]
pub fn value(&self) -> &str {
match self {
Self::Signature(value) | Self::Throttle(value) => value,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum YoutubeFormatSource {
Direct(String),
Signed {
url: String,
signature: String,
parameter: String,
},
}
impl YoutubeFormatSource {
#[must_use]
pub fn url(&self) -> &str {
match self {
Self::Direct(url) | Self::Signed { url, .. } => url,
}
}
#[must_use]
pub fn signature_challenge(&self) -> Option<YoutubeChallenge> {
match self {
Self::Direct(_) => None,
Self::Signed { signature, .. } => Some(YoutubeChallenge::Signature(signature.clone())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum YoutubeGrant {
Granted {
id: String,
url: String,
},
Throttled {
id: String,
url: String,
challenge: YoutubeChallenge,
},
Withheld {
id: String,
challenge: YoutubeChallenge,
},
}
impl YoutubeGrant {
#[must_use]
pub fn id(&self) -> &str {
match self {
Self::Granted { id, .. } | Self::Throttled { id, .. } | Self::Withheld { id, .. } => id,
}
}
#[must_use]
pub fn retrievable(&self) -> Option<&str> {
match self {
Self::Granted { url, .. } | Self::Throttled { url, .. } => Some(url),
Self::Withheld { .. } => None,
}
}
}
#[ext(name = YoutubeChallengeExt)]
pub impl<This, ChallengeError> This
where
This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg,
{
fn format_challenges<'a>(
&'a self,
formats: impl IntoIterator<Item = &'a YoutubeFormat> + 'a,
) -> impl Iterator<Item = YoutubeChallenge> + 'a {
let mut posed = Vec::new();
formats
.into_iter()
.flat_map(|format| {
[
self.throttle_challenge(format.source.url()).map(YoutubeChallenge::Throttle),
format.source.signature_challenge(),
]
})
.flatten()
.filter(move |challenge| {
let fresh = !posed.contains(challenge);
if fresh {
posed.push(challenge.clone());
}
fresh
})
}
fn grant_format(&self, format: &YoutubeFormat, solutions: &This::Solutions) -> YoutubeGrant {
let id = format.id.clone();
let mut url = format.source.url().to_owned();
let mut throttled = None;
if let Some(value) = self.throttle_challenge(&url) {
let challenge = YoutubeChallenge::Throttle(value);
match self.solution_of(solutions, &challenge) {
Some(solution) => url = self.with_throttle(&url, &solution),
None => throttled = Some(challenge),
}
}
if let YoutubeFormatSource::Signed { signature, parameter, .. } = &format.source {
let challenge = YoutubeChallenge::Signature(signature.clone());
match self.solution_of(solutions, &challenge) {
Some(solution) => url = self.with_signature(&url, parameter, &solution),
None => return YoutubeGrant::Withheld { id, challenge },
}
}
throttled.map_or_else(
|| YoutubeGrant::Granted { id: id.clone(), url: url.clone() },
|challenge| YoutubeGrant::Throttled { id: id.clone(), url: url.clone(), challenge },
)
}
fn grant_formats(&self, program: &str, formats: &[YoutubeFormat]) -> Result<Vec<YoutubeGrant>, ChallengeError> {
let solutions = self.solve_challenges(program, self.format_challenges(formats))?;
Ok(formats.iter().map(|format| self.grant_format(format, &solutions)).collect())
}
}