1use crate::{YoutubeFormat, YoutubeSorts};
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4
5#[delegatable_trait]
7pub trait YoutubeUrlAlg {
8 fn throttle_challenge(&self, url: &str) -> Option<String>;
10
11 fn with_throttle(&self, url: &str, solution: &str) -> String;
13
14 fn with_signature(&self, url: &str, parameter: &str, signature: &str) -> String;
16}
17
18#[delegatable_trait]
20pub trait YoutubeChallengeAlg: YoutubeSorts {
21 type Error;
23
24 fn solve_challenges(
33 &self,
34 program: &str,
35 challenges: impl IntoIterator<Item = YoutubeChallenge>,
36 ) -> Result<Self::Solutions, Self::Error>;
37}
38
39#[delegatable_trait]
41pub trait YoutubeSolutionAlg: YoutubeSorts {
42 fn solution_of(&self, solutions: &Self::Solutions, challenge: &YoutubeChallenge) -> Option<String>;
44}
45
46pub const DEFAULT_SIGNATURE_PARAMETER: &str = "signature";
48
49#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
51pub enum YoutubeChallenge {
52 Signature(String),
54 Throttle(String),
56}
57
58impl YoutubeChallenge {
59 #[must_use]
61 pub fn value(&self) -> &str {
62 match self {
63 Self::Signature(value) | Self::Throttle(value) => value,
64 }
65 }
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum YoutubeFormatSource {
71 Direct(String),
73 Signed {
75 url: String,
77 signature: String,
79 parameter: String,
81 },
82}
83
84impl YoutubeFormatSource {
85 #[must_use]
87 pub fn url(&self) -> &str {
88 match self {
89 Self::Direct(url) | Self::Signed { url, .. } => url,
90 }
91 }
92
93 #[must_use]
95 pub fn signature_challenge(&self) -> Option<YoutubeChallenge> {
96 match self {
97 Self::Direct(_) => None,
98 Self::Signed { signature, .. } => Some(YoutubeChallenge::Signature(signature.clone())),
99 }
100 }
101}
102
103#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum YoutubeGrant {
111 Granted {
113 id: String,
115 url: String,
117 },
118 Throttled {
120 id: String,
122 url: String,
124 challenge: YoutubeChallenge,
126 },
127 Withheld {
129 id: String,
131 challenge: YoutubeChallenge,
133 },
134}
135
136impl YoutubeGrant {
137 #[must_use]
139 pub fn id(&self) -> &str {
140 match self {
141 Self::Granted { id, .. } | Self::Throttled { id, .. } | Self::Withheld { id, .. } => id,
142 }
143 }
144
145 #[must_use]
147 pub fn retrievable(&self) -> Option<&str> {
148 match self {
149 Self::Granted { url, .. } | Self::Throttled { url, .. } => Some(url),
150 Self::Withheld { .. } => None,
151 }
152 }
153}
154
155#[ext(name = YoutubeChallengeExt)]
157pub impl<This, ChallengeError> This
158where
159 This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg,
160{
161 fn format_challenges<'a>(
163 &'a self,
164 formats: impl IntoIterator<Item = &'a YoutubeFormat> + 'a,
165 ) -> impl Iterator<Item = YoutubeChallenge> + 'a {
166 let mut posed = Vec::new();
167 formats
168 .into_iter()
169 .flat_map(|format| {
170 [
171 self.throttle_challenge(format.source.url()).map(YoutubeChallenge::Throttle),
172 format.source.signature_challenge(),
173 ]
174 })
175 .flatten()
176 .filter(move |challenge| {
177 let fresh = !posed.contains(challenge);
178 if fresh {
179 posed.push(challenge.clone());
180 }
181 fresh
182 })
183 }
184
185 fn grant_format(&self, format: &YoutubeFormat, solutions: &This::Solutions) -> YoutubeGrant {
187 let id = format.id.clone();
188 let mut url = format.source.url().to_owned();
189 let mut throttled = None;
190 if let Some(value) = self.throttle_challenge(&url) {
191 let challenge = YoutubeChallenge::Throttle(value);
192 match self.solution_of(solutions, &challenge) {
193 Some(solution) => url = self.with_throttle(&url, &solution),
194 None => throttled = Some(challenge),
195 }
196 }
197 if let YoutubeFormatSource::Signed { signature, parameter, .. } = &format.source {
198 let challenge = YoutubeChallenge::Signature(signature.clone());
199 match self.solution_of(solutions, &challenge) {
200 Some(solution) => url = self.with_signature(&url, parameter, &solution),
201 None => return YoutubeGrant::Withheld { id, challenge },
202 }
203 }
204 throttled.map_or_else(
205 || YoutubeGrant::Granted { id: id.clone(), url: url.clone() },
206 |challenge| YoutubeGrant::Throttled { id: id.clone(), url: url.clone(), challenge },
207 )
208 }
209
210 fn grant_formats(&self, program: &str, formats: &[YoutubeFormat]) -> Result<Vec<YoutubeGrant>, ChallengeError> {
216 let solutions = self.solve_challenges(program, self.format_challenges(formats))?;
217 Ok(formats.iter().map(|format| self.grant_format(format, &solutions)).collect())
218 }
219}