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(&self, solutions: &Self::Solutions, challenge: &YoutubeChallenge) -> Option<String>;
44}
45
46/// Names the query parameter carrying a solved signature when a format states no other.
47pub const DEFAULT_SIGNATURE_PARAMETER: &str = "signature";
48
49/// Denotes one obfuscated value that only the Youtube player program resolves.
50#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
51pub enum YoutubeChallenge {
52    /// Guards access to one format representation.
53    Signature(String),
54    /// Governs the rate at which a granted representation is served.
55    Throttle(String),
56}
57
58impl YoutubeChallenge {
59    /// Observes the obfuscated value this challenge poses.
60    #[must_use]
61    pub fn value(&self) -> &str {
62        match self {
63            Self::Signature(value) | Self::Throttle(value) => value,
64        }
65    }
66}
67
68/// Denotes how one format states the location of its representation.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub enum YoutubeFormatSource {
71    /// States a retrievable URL directly.
72    Direct(String),
73    /// States a URL granting retrieval only under a solved signature.
74    Signed {
75        /// Locates the representation before the signature is attached.
76        url: String,
77        /// Poses the signature challenge guarding the representation.
78        signature: String,
79        /// Names the query parameter carrying the solved signature.
80        parameter: String,
81    },
82}
83
84impl YoutubeFormatSource {
85    /// Locates the representation before any challenge is answered.
86    #[must_use]
87    pub fn url(&self) -> &str {
88        match self {
89            Self::Direct(url) | Self::Signed { url, .. } => url,
90        }
91    }
92
93    /// Observes the signature challenge guarding this source.
94    #[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/// Denotes the outcome of granting one format its retrievable representation.
104///
105/// The two challenges guard different things. A signature guards access: unanswered, the
106/// representation cannot be retrieved at all. A throttling parameter governs the rate at which a
107/// granted representation is served: unanswered, the representation is still retrievable, only
108/// slowly. Granting therefore withholds one and throttles the other.
109#[derive(Clone, Debug, PartialEq, Eq)]
110pub enum YoutubeGrant {
111    /// Grants retrieval at a URL whose every challenge is answered.
112    Granted {
113        /// Identifies the granted format by `itag`.
114        id: String,
115        /// Locates the fully answered representation.
116        url: String,
117    },
118    /// Grants retrieval at a rate the unanswered throttling parameter governs.
119    Throttled {
120        /// Identifies the throttled format by `itag`.
121        id: String,
122        /// Locates the representation, still posing its throttling parameter.
123        url: String,
124        /// Names the throttling challenge that stayed unresolved.
125        challenge: YoutubeChallenge,
126    },
127    /// Withholds retrieval because the signature guarding it remains unresolved.
128    Withheld {
129        /// Identifies the withheld format by `itag`.
130        id: String,
131        /// Names the challenge that remains unresolved.
132        challenge: YoutubeChallenge,
133    },
134}
135
136impl YoutubeGrant {
137    /// Identifies the format this outcome speaks about.
138    #[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    /// Locates the representation exactly when it is retrievable.
146    #[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/// Derives bulk challenge resolution and per-format granting from primitive capabilities.
156#[ext(name = YoutubeChallengeExt)]
157pub impl<This, ChallengeError> This
158where
159    This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg,
160{
161    /// Observes every distinct challenge the formats pose, in first-appearance order.
162    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    /// Grants one format its retrievable URL under already resolved challenges.
186    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    /// Resolves the challenges one program poses once, and grants every format in order.
211    ///
212    /// # Errors
213    ///
214    /// Returns the interpreter-specific resolution failure.
215    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}