rsynko_yt/challenge.rs
1use crate::{YoutubeFormat, YoutubeSorts};
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4
5/// Specifies the query surface through which Youtube poses and answers URL challenges.
6#[delegatable_trait]
7pub trait YoutubeUrlAlg {
8 /// Observes the throttling challenge a representation URL carries.
9 fn throttle_challenge(&self, url: &str) -> Option<String>;
10
11 /// Replaces the throttling parameter with its solution.
12 fn with_throttle(&self, url: &str, solution: &str) -> String;
13
14 /// Attaches a solved signature under the parameter naming it.
15 fn with_signature(&self, url: &str, parameter: &str, signature: &str) -> String;
16}
17
18/// Specifies resolution of every challenge one catalog poses in a single application.
19#[delegatable_trait]
20pub trait YoutubeChallengeAlg: YoutubeSorts {
21 /// Denotes resolution failure.
22 type Error;
23
24 /// Resolves the given challenges together under the program that poses them.
25 ///
26 /// A challenge is posed by one player program, so a solution is stated relative to that
27 /// program and says nothing about the challenges another program poses.
28 ///
29 /// # Errors
30 ///
31 /// Returns the interpreter-specific resolution failure.
32 fn solve_challenges(
33 &self,
34 program: &str,
35 challenges: impl IntoIterator<Item = YoutubeChallenge>,
36 ) -> Result<Self::Solutions, Self::Error>;
37}
38
39/// Specifies observation of one individually resolved challenge.
40#[delegatable_trait]
41pub trait YoutubeSolutionAlg: YoutubeSorts {
42 /// Observes the solution of one challenge exactly when that challenge was resolved.
43 fn solution_of(
44 &self,
45 solutions: &Self::Solutions,
46 challenge: &YoutubeChallenge,
47 ) -> Option<String>;
48}
49
50/// Names the query parameter carrying a solved signature when a format states no other.
51pub const DEFAULT_SIGNATURE_PARAMETER: &str = "signature";
52
53/// Denotes one obfuscated value that only the Youtube player program resolves.
54#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
55pub enum YoutubeChallenge {
56 /// Guards access to one format representation.
57 Signature(String),
58 /// Governs the rate at which a granted representation is served.
59 Throttle(String),
60}
61
62impl YoutubeChallenge {
63 /// Observes the obfuscated value this challenge poses.
64 #[must_use]
65 pub fn value(&self) -> &str {
66 match self {
67 Self::Signature(value) | Self::Throttle(value) => value,
68 }
69 }
70}
71
72/// Denotes how one format states the location of its representation.
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub enum YoutubeFormatSource {
75 /// States a retrievable URL directly.
76 Direct(String),
77 /// States a URL granting retrieval only under a solved signature.
78 Signed {
79 /// Locates the representation before the signature is attached.
80 url: String,
81 /// Poses the signature challenge guarding the representation.
82 signature: String,
83 /// Names the query parameter carrying the solved signature.
84 parameter: String,
85 },
86}
87
88impl YoutubeFormatSource {
89 /// Locates the representation before any challenge is answered.
90 #[must_use]
91 pub fn url(&self) -> &str {
92 match self {
93 Self::Direct(url) | Self::Signed { url, .. } => url,
94 }
95 }
96
97 /// Observes the signature challenge guarding this source.
98 #[must_use]
99 pub fn signature_challenge(&self) -> Option<YoutubeChallenge> {
100 match self {
101 Self::Direct(_) => None,
102 Self::Signed { signature, .. } => Some(YoutubeChallenge::Signature(signature.clone())),
103 }
104 }
105}
106
107/// Denotes the outcome of granting one format its retrievable representation.
108///
109/// The two challenges guard different things. A signature guards access: unanswered, the
110/// representation cannot be retrieved at all. A throttling parameter governs the rate at which a
111/// granted representation is served: unanswered, the representation is still retrievable, only
112/// slowly. Granting therefore withholds one and throttles the other.
113#[derive(Clone, Debug, PartialEq, Eq)]
114pub enum YoutubeGrant {
115 /// Grants retrieval at a URL whose every challenge is answered.
116 Granted {
117 /// Identifies the granted format by `itag`.
118 id: String,
119 /// Locates the fully answered representation.
120 url: String,
121 },
122 /// Grants retrieval at a rate the unanswered throttling parameter governs.
123 Throttled {
124 /// Identifies the throttled format by `itag`.
125 id: String,
126 /// Locates the representation, still posing its throttling parameter.
127 url: String,
128 /// Names the throttling challenge that stayed unresolved.
129 challenge: YoutubeChallenge,
130 },
131 /// Withholds retrieval because the signature guarding it remains unresolved.
132 Withheld {
133 /// Identifies the withheld format by `itag`.
134 id: String,
135 /// Names the challenge that remains unresolved.
136 challenge: YoutubeChallenge,
137 },
138}
139
140impl YoutubeGrant {
141 /// Identifies the format this outcome speaks about.
142 #[must_use]
143 pub fn id(&self) -> &str {
144 match self {
145 Self::Granted { id, .. } | Self::Throttled { id, .. } | Self::Withheld { id, .. } => id,
146 }
147 }
148
149 /// Locates the representation exactly when it is retrievable.
150 #[must_use]
151 pub fn retrievable(&self) -> Option<&str> {
152 match self {
153 Self::Granted { url, .. } | Self::Throttled { url, .. } => Some(url),
154 Self::Withheld { .. } => None,
155 }
156 }
157}
158
159/// Derives bulk challenge resolution and per-format granting from primitive capabilities.
160#[ext(name = YoutubeChallengeExt)]
161pub impl<This, ChallengeError> This
162where
163 This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg,
164{
165 /// Observes every distinct challenge the formats pose, in first-appearance order.
166 fn format_challenges<'a>(
167 &'a self,
168 formats: impl IntoIterator<Item = &'a YoutubeFormat> + 'a,
169 ) -> impl Iterator<Item = YoutubeChallenge> + 'a {
170 let mut posed = Vec::new();
171 formats
172 .into_iter()
173 .flat_map(|format| {
174 [
175 self.throttle_challenge(format.source.url())
176 .map(YoutubeChallenge::Throttle),
177 format.source.signature_challenge(),
178 ]
179 })
180 .flatten()
181 .filter(move |challenge| {
182 let fresh = !posed.contains(challenge);
183 if fresh {
184 posed.push(challenge.clone());
185 }
186 fresh
187 })
188 }
189
190 /// Grants one format its retrievable URL under already resolved challenges.
191 fn grant_format(&self, format: &YoutubeFormat, solutions: &This::Solutions) -> YoutubeGrant {
192 let id = format.id.clone();
193 let mut url = format.source.url().to_owned();
194 let mut throttled = None;
195 if let Some(value) = self.throttle_challenge(&url) {
196 let challenge = YoutubeChallenge::Throttle(value);
197 match self.solution_of(solutions, &challenge) {
198 Some(solution) => url = self.with_throttle(&url, &solution),
199 None => throttled = Some(challenge),
200 }
201 }
202 if let YoutubeFormatSource::Signed {
203 signature,
204 parameter,
205 ..
206 } = &format.source
207 {
208 let challenge = YoutubeChallenge::Signature(signature.clone());
209 match self.solution_of(solutions, &challenge) {
210 Some(solution) => url = self.with_signature(&url, parameter, &solution),
211 None => return YoutubeGrant::Withheld { id, challenge },
212 }
213 }
214 throttled.map_or_else(
215 || YoutubeGrant::Granted {
216 id: id.clone(),
217 url: url.clone(),
218 },
219 |challenge| YoutubeGrant::Throttled {
220 id: id.clone(),
221 url: url.clone(),
222 challenge,
223 },
224 )
225 }
226
227 /// Resolves the challenges one program poses once, and grants every format in order.
228 ///
229 /// # Errors
230 ///
231 /// Returns the interpreter-specific resolution failure.
232 fn grant_formats(
233 &self,
234 program: &str,
235 formats: &[YoutubeFormat],
236 ) -> Result<Vec<YoutubeGrant>, ChallengeError> {
237 let solutions = self.solve_challenges(program, self.format_challenges(formats))?;
238 Ok(formats
239 .iter()
240 .map(|format| self.grant_format(format, &solutions))
241 .collect())
242 }
243}