1use crate::*;
4use alux_ext::ext;
5use anyhow::{Result, bail};
6use rsynko_download::*;
7use rsynko_media::*;
8use std::fmt::{Debug, Display};
9use std::path::PathBuf;
10
11pub trait YoutubeChallengeLawFixture {
13 fn solve_law_challenge(&mut self, challenge: YoutubeChallenge, solution: &str);
15 fn law_challenge_applications(&self) -> Vec<Vec<YoutubeChallenge>>;
17}
18
19pub trait YoutubeLawFixture: YoutubeSorts {
21 fn law_watch_url(&self) -> String;
23 fn law_video_id(&self) -> String;
25 fn law_watch_bytes(&self) -> Vec<u8>;
27 fn law_player_bytes(&self) -> Vec<u8>;
29 fn law_executed_requests(&self) -> Vec<Self::Request>;
31 fn law_retrieved_requests(&self) -> Vec<Self::Request>;
33}
34
35#[ext(name = YoutubeUrlLaws)]
37pub impl<This> This
38where
39 This: YoutubeUrlAlg,
40{
41 fn youtube_url_laws(&self) -> Result<()> {
52 let url = "https://media.example/video.mp4?n=posed";
53 let parameter = DEFAULT_SIGNATURE_PARAMETER;
54 let answered = self.with_throttle(url, "answered");
55 if self.throttle_challenge(&answered).as_deref() != Some("answered") {
56 bail!("a solved throttling parameter is not the one the answered URL poses");
57 }
58 let signed = self.with_signature(url, parameter, "solved");
59 if self.throttle_challenge(&signed) != self.throttle_challenge(url) {
60 bail!("attaching a signature disturbed the throttling challenge");
61 }
62 Ok(())
63 }
64}
65
66#[ext(name = YoutubeChallengeLaws)]
68pub impl<This, ChallengeError> This
69where
70 This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg,
71{
72 fn youtube_granting_laws(&self) -> Result<()>
85 where
86 ChallengeError: Debug,
87 {
88 let formats = [
89 law_format("18", YoutubeFormatSource::Direct("https://media.example/a.mp4".to_owned())),
90 law_format(
91 "137",
92 YoutubeFormatSource::Signed {
93 url: "https://media.example/b.mp4".to_owned(),
94 signature: "obfuscated".to_owned(),
95 parameter: DEFAULT_SIGNATURE_PARAMETER.to_owned(),
96 },
97 ),
98 law_format("22", YoutubeFormatSource::Direct("https://media.example/c.mp4?n=posed".to_owned())),
99 ];
100 let formats = &formats;
101 let grants = match self.grant_formats(law_program(), formats) {
102 Ok(grants) => grants.into_iter().collect::<Vec<_>>(),
103 Err(error) => bail!("challenge resolution failed: {error:?}"),
104 };
105 if grants.len() != formats.len() {
106 bail!("granting yielded {} outcomes for {} formats", grants.len(), formats.len());
107 }
108 for (format, grant) in formats.iter().zip(&grants) {
109 if grant.id() != format.id {
110 bail!("granting did not preserve declaration order at {}", grant.id());
111 }
112 let guarded = format.source.signature_challenge().is_some();
113 let throttled = self.throttle_challenge(format.source.url()).is_some();
114 match grant {
115 YoutubeGrant::Granted { url, .. } => {
116 if guarded || throttled {
117 bail!("a guarded source was granted outright at {url}");
118 }
119 if url != format.source.url() {
120 bail!("an unguarded source was not granted its stated URL unchanged");
121 }
122 }
123 YoutubeGrant::Throttled { challenge, .. } => {
124 if !throttled {
125 bail!("a source posing no throttling parameter was throttled: {challenge:?}");
126 }
127 if guarded {
128 bail!("a signature guarding the representation was answered by throttling");
129 }
130 }
131 YoutubeGrant::Withheld { challenge, .. } => {
132 if !guarded {
133 bail!("an unguarded source was withheld behind {challenge:?}");
134 }
135 }
136 }
137 if grant.retrievable().is_none() && !guarded {
138 bail!("an unresolved throttling parameter denied retrieval of {}", grant.id());
139 }
140 }
141 Ok(())
142 }
143}
144
145#[ext(name = YoutubeSolutionLaws)]
147pub impl<This, ChallengeError> This
148where
149 This: YoutubeChallengeAlg<Error = ChallengeError> + YoutubeSolutionAlg + YoutubeUrlAlg + YoutubeChallengeLawFixture,
150{
151 fn youtube_solution_laws(&mut self) -> Result<()>
165 where
166 ChallengeError: Debug,
167 {
168 let signed_url = "https://media.example/signed.mp4";
169 let throttled_url = "https://media.example/throttled.mp4?n=posed";
170 self.solve_law_challenge(YoutubeChallenge::Signature("guarded".to_owned()), "solved");
171 self.solve_law_challenge(YoutubeChallenge::Throttle("posed".to_owned()), "answered");
172
173 let formats = [
174 law_format(
175 "137",
176 YoutubeFormatSource::Signed {
177 url: signed_url.to_owned(),
178 signature: "guarded".to_owned(),
179 parameter: DEFAULT_SIGNATURE_PARAMETER.to_owned(),
180 },
181 ),
182 law_format("18", YoutubeFormatSource::Direct(throttled_url.to_owned())),
183 law_format("22", YoutubeFormatSource::Direct(throttled_url.to_owned())),
184 ];
185 let before = self.law_challenge_applications().len();
186 let grants = match self.grant_formats(law_program(), &formats) {
187 Ok(grants) => grants,
188 Err(error) => bail!("challenge resolution failed: {error:?}"),
189 };
190
191 match &grants[0] {
192 YoutubeGrant::Granted { url, .. }
193 if *url == self.with_signature(signed_url, DEFAULT_SIGNATURE_PARAMETER, "solved") => {}
194 other => bail!("a solved signature was not attached under its parameter: {other:?}"),
195 }
196 for grant in &grants[1..] {
197 match grant {
198 YoutubeGrant::Granted { url, .. } if self.throttle_challenge(url).as_deref() == Some("answered") => {}
199 other => {
200 bail!("a solved throttling parameter did not replace the posed one: {other:?}")
201 }
202 }
203 }
204
205 let applications = self.law_challenge_applications();
206 if applications.len() != before + 1 {
207 bail!("granting resolved in {} applications rather than one", applications.len() - before);
208 }
209 let posed = &applications[before];
210 if posed.iter().filter(|challenge| **challenge == YoutubeChallenge::Throttle("posed".to_owned())).count() != 1 {
211 bail!("a challenge two formats share was not posed exactly once: {posed:?}");
212 }
213 Ok(())
214 }
215}
216
217#[ext(name = YoutubeRequestLaws)]
219pub impl<This, Request> This
220where
221 This: YoutubeSorts<Request = Request> + YoutubeRequestAlg,
222 Request: PartialEq + Debug,
223{
224 fn youtube_request_laws(&self) -> Result<()> {
236 let url = "https://www.youtube.com/watch?v=abc123";
237 if self.watch_request(url) != self.watch_request(url) {
238 bail!("a watch request is not a function of its URL");
239 }
240 if self.watch_request(url) == self.media_request(url) {
241 bail!("retrieving a watch page denotes the same request as retrieving media");
242 }
243 let claim =
244 PlayerClaim { client: "CLIENT".to_owned(), visitor: Some("session".to_owned()), timestamp: Some(19_000) };
245 let player = self.player_request("abc123", "key", &claim);
246 if player != self.player_request("abc123", "key", &claim) {
247 bail!("a player request is not a function of the values naming it");
248 }
249 if player == self.player_request("abc123", "other", &claim) {
250 bail!("a player request does not depend on its API key");
251 }
252 if player == self.player_request("other", "key", &claim) {
253 bail!("a player request does not depend on its video identity");
254 }
255 if player == self.player_request("abc123", "key", &PlayerClaim::default()) {
256 bail!("a player request does not depend on what the client claims about itself");
257 }
258 let other = PlayerClaim { client: "OTHER".to_owned(), ..claim.clone() };
259 if player == self.player_request("abc123", "key", &other) {
260 bail!("a player request does not depend on which client makes the claim");
261 }
262 Ok(())
263 }
264}
265
266#[ext(name = YoutubeResponseLaws)]
268pub impl<This> This
269where
270 This: YoutubeResponseAlg<Error: Debug> + YoutubeLawFixture,
271{
272 fn youtube_response_laws(&self) -> Result<()> {
284 let bytes = self.law_watch_bytes();
285 let page = match self.decode_youtube_watch(&bytes) {
286 Ok(page) => page,
287 Err(error) => bail!("decoding the watch page failed: {error:?}"),
288 };
289 if !page.has_player_response {
290 bail!("a watch page describing a playable video observes no player response");
291 }
292 if page.api_key.is_none() {
293 bail!("a watch page describing a playable video observes no API key");
294 }
295 match self.decode_youtube_watch(&bytes) {
296 Ok(again) if again == page => {}
297 _ => bail!("decoding a watch page is not a function of its bytes"),
298 }
299
300 let bytes = self.law_player_bytes();
301 let player = match self.decode_youtube_player(&bytes) {
302 Ok(player) => player,
303 Err(error) => bail!("decoding the player response failed: {error:?}"),
304 };
305 if player.status.as_deref() != Some("OK") {
306 bail!("a player response describing a playable video is not observed as playable");
307 }
308 if player.formats.is_empty() {
309 bail!("a player response describing one format observes none");
310 }
311 for format in &player.formats {
312 if format.source.url().is_empty() {
313 bail!("a described format states no location for its representation");
314 }
315 }
316 match self.decode_youtube_player(&bytes) {
317 Ok(again) if again == player => {}
318 _ => bail!("decoding a player response is not a function of its bytes"),
319 }
320 Ok(())
321 }
322}
323
324#[ext(name = YoutubeExtractionLaws)]
326pub impl<This, Request> This
327where
328 This: YoutubeSorts<Request = Request>
329 + YoutubeRequestAlg
330 + YoutubeClientAlg
331 + YoutubeProgramAlg
332 + YoutubeRequestBytesAlg<Error: Display>
333 + YoutubeResponseAlg<Error: Display>
334 + YoutubeChallengeAlg<Error: Display>
335 + YoutubeSolutionAlg
336 + YoutubeUrlAlg
337 + MetadataAlg
338 + FormatAlg
339 + ExtractionAlg
340 + ExtractionViewAlg
341 + MediaViewAlg
342 + YoutubeLawFixture,
343 Request: PartialEq + Debug,
344{
345 fn youtube_extraction_laws(&self) -> Result<()> {
358 let watch_url = self.law_watch_url();
359 let id = self.law_video_id();
360 if youtube_id(&watch_url).as_deref() != Some(id.as_str()) {
361 bail!("the watch URL does not denote the identity the fixture states");
362 }
363 if youtube_id(&format!("https://youtu.be/{id}")).as_deref() != Some(id.as_str()) {
364 bail!("the short spelling of one video denotes another identity");
365 }
366 if let Some(other) = youtube_id("https://example.com/watch?v=abc123") {
367 bail!("an unsupported host denoted the video identity {other}");
368 }
369
370 let before = self.law_executed_requests();
371 match self.extract_youtube("https://example.com/watch?v=abc123") {
372 Err(YoutubeError::Url(_)) => {}
373 Ok(_) => bail!("an unsupported URL extracted"),
374 Err(error) => bail!("an unsupported URL failed as {error}"),
375 }
376 if self.law_executed_requests() != before {
377 bail!("an unsupported URL executed a request");
378 }
379
380 let extraction = match self.extract_youtube(&watch_url) {
381 Ok(extraction) => extraction,
382 Err(error) => bail!("extracting the fixture URL failed: {error}"),
383 };
384 let executed = self.law_executed_requests();
385 let performed = &executed[before.len()..];
386 if performed.len() < 2 {
387 bail!("extraction executed {} requests rather than several", performed.len());
388 }
389 if performed[0] != self.watch_request(&watch_url) {
390 bail!("extraction did not begin with the watch request: {:?}", performed[0]);
391 }
392 if performed[1..].contains(&performed[0]) {
393 bail!("extraction executed the watch request more than once");
394 }
395
396 let Some(media) = self.as_media(extraction) else {
397 bail!("extracting one video did not denote one media item");
398 };
399 if self.media_id(&media) != id {
400 bail!("the extracted media carries {} rather than {id}", self.media_id(&media));
401 }
402 if self.media_formats(&media).is_empty() {
403 bail!("the extracted media carries no format");
404 }
405 Ok(())
406 }
407}
408
409#[ext(name = YoutubeApplicationLaws)]
411pub impl<This, Request, Event, Progress> This
412where
413 This: YoutubeSorts<Request = Request>
414 + YoutubeRequestAlg
415 + YoutubeClientAlg
416 + YoutubeProgramAlg
417 + YoutubeRequestBytesAlg<Error: Display>
418 + YoutubeResponseAlg<Error: Display>
419 + YoutubeChallengeAlg<Error: Display>
420 + YoutubeSolutionAlg
421 + YoutubeUrlAlg
422 + MetadataAlg
423 + FormatAlg
424 + FormatViewAlg
425 + FormatPredicateAlg
426 + FormatSelectionAlg
427 + ExtractionAlg
428 + ExtractionViewAlg
429 + MediaViewAlg
430 + FormatSelectionApplyAlg
431 + FormatSourceAlg<Request>
432 + FetchStreamAlg<Request, Error: Display>
433 + AtomicPublishAlg<Error: Display>
434 + DownloadObservationAlg<Event = Event, Progress = Progress>
435 + DownloadProgressAlg<Progress = Progress>
436 + DownloadReportAlg<Event = Event>
437 + YoutubeLawFixture
438 + MediaProgramLawFixture,
439 Request: PartialEq + Debug,
440{
441 fn youtube_application_laws(&mut self) -> Result<()> {
453 let watch_url = self.law_watch_url();
454 let selection = self.best_format(self.any_format());
455 let published = match self.download_youtube(&watch_url, &selection, &OutputTarget::MediaId) {
456 Ok(published) => published,
457 Err(error) => bail!("the Youtube download failed: {error}"),
458 };
459 let expected = PathBuf::from(format!("{}.{}", self.law_video_id(), self.law_extension()));
460 if published != expected {
461 bail!("naming derived {} rather than {}", published.display(), expected.display());
462 }
463 let retrieved = self.law_retrieved_requests();
464 if retrieved.len() != 1 {
465 bail!("retrieval opened {} requests rather than one", retrieved.len());
466 }
467 if retrieved[0] == self.watch_request(&watch_url) {
468 bail!("retrieval reused the watch request rather than the media request");
469 }
470 if self.law_published_bytes(&published).as_deref() != Some(&self.expected_law_bytes()[..]) {
471 bail!("the granted representation did not reach generic download unchanged");
472 }
473 Ok(())
474 }
475}
476
477#[must_use]
479pub fn withheld_challenge(grant: &YoutubeGrant) -> Option<&YoutubeChallenge> {
480 match grant {
481 YoutubeGrant::Withheld { challenge, .. } => Some(challenge),
482 YoutubeGrant::Granted { .. } | YoutubeGrant::Throttled { .. } => None,
483 }
484}
485
486fn law_program() -> &'static str {
488 "law://player-program"
489}
490
491fn law_format(id: &str, source: YoutubeFormatSource) -> YoutubeFormat {
493 YoutubeFormat {
494 id: id.to_owned(),
495 source,
496 extension: Some("mp4".to_owned()),
497 has_audio: true,
498 has_video: true,
499 quality: None,
500 width: None,
501 height: None,
502 bitrate: None,
503 content_length: None,
504 codecs: None,
505 }
506}