Skip to main content

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}