Skip to main content

rsynko_memory/
youtube.rs

1//! The reified Youtube request and the interpreters that tie the Youtube sorts to it.
2
3use crate::{DownloadEvent, DownloadProgress, ReferenceDownloadEnv, ReferenceFetchError};
4use ambassador::Delegate;
5use rsynko_download::*;
6use rsynko_yt::*;
7use std::cell::RefCell;
8use std::convert::Infallible;
9use std::io::Cursor;
10use std::path::Path;
11use std::str::from_utf8;
12
13/// Denotes one request required by Youtube interpretation without choosing HTTP representation.
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum YoutubeRequest {
16    /// Retrieves a watch page.
17    Watch(String),
18    /// Retrieves the player catalog for one video identity and API key.
19    Player {
20        /// Identifies the video.
21        id: String,
22        /// Authorizes the player endpoint.
23        api_key: String,
24        /// States what the client claims about itself.
25        claim: PlayerClaim,
26    },
27    /// Retrieves the player program posing a video's challenges.
28    PlayerProgram(String),
29    /// Retrieves one direct media representation.
30    Media(String),
31}
32
33/// Reifies Youtube requests as inspectable domain syntax.
34#[derive(Clone, Copy, Debug, Default)]
35pub struct YoutubeRequestSyntax;
36
37impl YoutubeSorts for YoutubeRequestSyntax {
38    type Request = YoutubeRequest;
39    type Solutions = ();
40}
41
42impl YoutubeRequestAlg for YoutubeRequestSyntax {
43    fn watch_request(&self, url: impl Into<String>) -> Self::Request {
44        YoutubeRequest::Watch(url.into())
45    }
46
47    fn player_request(
48        &self,
49        id: impl Into<String>,
50        api_key: impl Into<String>,
51        claim: &PlayerClaim,
52    ) -> Self::Request {
53        YoutubeRequest::Player {
54            id: id.into(),
55            api_key: api_key.into(),
56            claim: claim.clone(),
57        }
58    }
59
60    fn player_program_request(&self, url: impl Into<String>) -> Self::Request {
61        YoutubeRequest::PlayerProgram(url.into())
62    }
63
64    fn media_request(&self, url: impl Into<String>) -> Self::Request {
65        YoutubeRequest::Media(url.into())
66    }
67}
68
69/// Names the watch URL the reference environment answers.
70pub const REFERENCE_WATCH_URL: &str = "https://www.youtube.com/watch?v=law123";
71
72/// Names the video identity that watch URL denotes.
73pub const REFERENCE_VIDEO_ID: &str = "law123";
74
75/// Locates the representation the reference player response describes.
76pub const REFERENCE_MEDIA_URL: &str = "memory://law-media";
77
78/// Names the container the reference player response states.
79pub const REFERENCE_MEDIA_EXTENSION: &str = "mp4";
80
81/// Locates the player program the reference watch page states.
82pub const REFERENCE_PLAYER_PROGRAM_URL: &str = "memory://law-player.js";
83
84/// States the player program the reference environment resolves challenges under.
85pub const REFERENCE_PLAYER_PROGRAM: &str = "reference player program\ntimestamp=19000\n";
86
87/// Interprets the Youtube query surface and resolves only the challenges it was seeded with.
88///
89/// Transport is a line-oriented encoding rather than HTML and JSON: decoding still reads the bytes
90/// it is handed, which is all the response laws constrain.
91#[derive(Debug, Default)]
92pub struct ReferenceYoutubeEnv {
93    solutions: Vec<(YoutubeChallenge, String)>,
94    applications: RefCell<Vec<Vec<YoutubeChallenge>>>,
95    executed: RefCell<Vec<YoutubeRequest>>,
96}
97
98impl ReferenceYoutubeEnv {
99    /// Seeds one challenge with the value the player program would resolve it to.
100    pub fn solve(&mut self, challenge: YoutubeChallenge, solution: impl Into<String>) {
101        self.solutions.push((challenge, solution.into()));
102    }
103
104    /// Observes the requests executed so far, in execution order.
105    #[must_use]
106    pub fn executed(&self) -> Vec<YoutubeRequest> {
107        self.executed.borrow().clone()
108    }
109
110    /// Observes the bulk challenge applications received, in application order.
111    #[must_use]
112    pub fn applications(&self) -> Vec<Vec<YoutubeChallenge>> {
113        self.applications.borrow().clone()
114    }
115
116    /// States the watch-page bytes the reference watch URL answers with.
117    #[must_use]
118    pub fn watch_bytes() -> Vec<u8> {
119        format!(
120            "player=yes\napi_key=law-key\ntitle=Law Video\nid={REFERENCE_VIDEO_ID}\njs={REFERENCE_PLAYER_PROGRAM_URL}\nvisitor=law-session\n"
121        )
122        .into_bytes()
123    }
124
125    /// States the player bytes the reference player request answers with.
126    #[must_use]
127    pub fn player_bytes() -> Vec<u8> {
128        format!(
129            "status=OK\ntitle=Law Video\nformat=18;{REFERENCE_MEDIA_EXTENSION};{REFERENCE_MEDIA_URL}\n"
130        )
131        .into_bytes()
132    }
133}
134
135impl YoutubeSorts for ReferenceYoutubeEnv {
136    type Request = YoutubeRequest;
137    type Solutions = Vec<(YoutubeChallenge, String)>;
138}
139
140impl YoutubeRequestAlg for ReferenceYoutubeEnv {
141    fn watch_request(&self, url: impl Into<String>) -> Self::Request {
142        YoutubeRequestSyntax.watch_request(url)
143    }
144
145    fn player_request(
146        &self,
147        id: impl Into<String>,
148        api_key: impl Into<String>,
149        claim: &PlayerClaim,
150    ) -> Self::Request {
151        YoutubeRequestSyntax.player_request(id, api_key, claim)
152    }
153
154    fn player_program_request(&self, url: impl Into<String>) -> Self::Request {
155        YoutubeRequestSyntax.player_program_request(url)
156    }
157
158    fn media_request(&self, url: impl Into<String>) -> Self::Request {
159        YoutubeRequestSyntax.media_request(url)
160    }
161}
162
163impl YoutubeClientAlg for ReferenceYoutubeEnv {
164    fn player_clients(&self) -> impl Iterator<Item = &str> {
165        ["REFERENCE"].into_iter()
166    }
167}
168
169impl YoutubeProgramAlg for ReferenceYoutubeEnv {
170    fn program_timestamp(&self, program: &str) -> Option<i64> {
171        program
172            .lines()
173            .find_map(|line| line.strip_prefix("timestamp="))?
174            .parse()
175            .ok()
176    }
177}
178
179impl YoutubeRequestBytesAlg for ReferenceYoutubeEnv {
180    type Error = ReferenceFetchError;
181
182    fn youtube_request_bytes(&self, request: &Self::Request) -> Result<Vec<u8>, Self::Error> {
183        self.executed.borrow_mut().push(request.clone());
184        match request {
185            YoutubeRequest::Watch(url) if url == REFERENCE_WATCH_URL => Ok(Self::watch_bytes()),
186            YoutubeRequest::PlayerProgram(url) if url == REFERENCE_PLAYER_PROGRAM_URL => {
187                Ok(REFERENCE_PLAYER_PROGRAM.as_bytes().to_vec())
188            }
189            YoutubeRequest::Player { id, .. } if id == REFERENCE_VIDEO_ID => {
190                Ok(Self::player_bytes())
191            }
192            YoutubeRequest::Watch(url)
193            | YoutubeRequest::PlayerProgram(url)
194            | YoutubeRequest::Media(url) => Err(ReferenceFetchError::UnknownResource(url.clone())),
195            YoutubeRequest::Player { id, .. } => {
196                Err(ReferenceFetchError::UnknownResource(id.clone()))
197            }
198        }
199    }
200}
201
202impl YoutubeResponseAlg for ReferenceYoutubeEnv {
203    type Error = ReferenceFetchError;
204
205    fn decode_youtube_watch(&self, bytes: &[u8]) -> Result<YoutubeWatchPage, Self::Error> {
206        let fields = decode_fields(bytes)?;
207        Ok(YoutubeWatchPage {
208            has_player_response: field(&fields, "player") == Some("yes"),
209            api_key: field(&fields, "api_key").map(str::to_owned),
210            player_url: field(&fields, "js").map(str::to_owned),
211            visitor_data: field(&fields, "visitor").map(str::to_owned),
212            title: field(&fields, "title").map(str::to_owned),
213        })
214    }
215
216    fn decode_youtube_player(&self, bytes: &[u8]) -> Result<YoutubePlayer, Self::Error> {
217        let fields = decode_fields(bytes)?;
218        let formats = fields
219            .iter()
220            .filter(|(key, _)| key == "format")
221            .filter_map(|(_, value)| decode_format(value))
222            .collect();
223        Ok(YoutubePlayer {
224            status: field(&fields, "status").map(str::to_owned),
225            reason: field(&fields, "reason").map(str::to_owned),
226            title: field(&fields, "title").map(str::to_owned),
227            formats,
228            unreadable: 0,
229        })
230    }
231}
232
233impl YoutubeUrlAlg for ReferenceYoutubeEnv {
234    fn throttle_challenge(&self, url: &str) -> Option<String> {
235        query_value(url, "n")
236    }
237
238    fn with_throttle(&self, url: &str, solution: &str) -> String {
239        with_query(url, "n", solution)
240    }
241
242    fn with_signature(&self, url: &str, parameter: &str, signature: &str) -> String {
243        with_query(url, parameter, signature)
244    }
245}
246
247impl YoutubeChallengeAlg for ReferenceYoutubeEnv {
248    type Error = Infallible;
249
250    fn solve_challenges(
251        &self,
252        program: &str,
253        challenges: impl IntoIterator<Item = YoutubeChallenge>,
254    ) -> Result<Self::Solutions, Self::Error> {
255        let posed: Vec<YoutubeChallenge> = challenges.into_iter().collect();
256        self.applications.borrow_mut().push(posed.clone());
257        // A challenge only has a solution under the program that poses it.
258        if program.is_empty() {
259            return Ok(Vec::new());
260        }
261        Ok(posed
262            .into_iter()
263            .filter_map(|challenge| {
264                self.solutions
265                    .iter()
266                    .find(|(known, _)| *known == challenge)
267                    .map(|(_, solution)| (challenge, solution.clone()))
268            })
269            .collect())
270    }
271}
272
273impl YoutubeSolutionAlg for ReferenceYoutubeEnv {
274    fn solution_of(
275        &self,
276        solutions: &Self::Solutions,
277        challenge: &YoutubeChallenge,
278    ) -> Option<String> {
279        solutions
280            .iter()
281            .find(|(posed, _)| posed == challenge)
282            .map(|(_, solution)| solution.clone())
283    }
284}
285
286/// Retrieves the representation one Youtube media request states.
287///
288/// Publication, progress, and reporting are the shared download interpreter's; only locating the
289/// bytes is Youtube-specific, so only that is stated here.
290#[allow(
291    clippy::duplicated_attributes,
292    reason = "one delegation per capability, not per target"
293)]
294#[derive(Debug, Default, Delegate)]
295#[delegate(AtomicPublishAlg, target = "resources")]
296#[delegate(DownloadObservationAlg, target = "resources")]
297#[delegate(DownloadReportAlg, target = "resources")]
298#[delegate(DownloadProgressAlg, target = "resources")]
299pub struct ReferenceYoutubeDownloadEnv {
300    resources: ReferenceDownloadEnv,
301    opened: RefCell<Vec<YoutubeRequest>>,
302}
303
304impl ReferenceYoutubeDownloadEnv {
305    /// Associates exact bytes with one media URL.
306    pub fn register_resource(&mut self, url: impl Into<String>, bytes: impl Into<Vec<u8>>) {
307        self.resources.register_resource(url, bytes);
308    }
309
310    /// Observes bytes published at one final path.
311    #[must_use]
312    pub fn file(&self, path: &Path) -> Option<Vec<u8>> {
313        self.resources.file(path)
314    }
315
316    /// Observes the requests retrieval opened, in retrieval order.
317    #[must_use]
318    pub fn opened(&self) -> Vec<YoutubeRequest> {
319        self.opened.borrow().clone()
320    }
321
322    /// Observes terminal events in emission order.
323    #[must_use]
324    pub fn events(&self) -> Vec<DownloadEvent> {
325        self.resources.events()
326    }
327
328    /// Observes byte-progress reports in emission order.
329    #[must_use]
330    pub fn progress(&self) -> Vec<DownloadProgress> {
331        self.resources.progress()
332    }
333}
334
335impl FetchStreamAlg<YoutubeRequest> for ReferenceYoutubeDownloadEnv {
336    type Error = ReferenceFetchError;
337    type Stream = Cursor<Vec<u8>>;
338
339    fn open_fetch(
340        &self,
341        request: &YoutubeRequest,
342    ) -> Result<FetchStream<Self::Stream>, Self::Error> {
343        self.opened.borrow_mut().push(request.clone());
344        match request {
345            YoutubeRequest::Media(url) => self.resources.open_fetch(url),
346            YoutubeRequest::Watch(url) | YoutubeRequest::PlayerProgram(url) => {
347                Err(ReferenceFetchError::UnknownResource(url.clone()))
348            }
349            YoutubeRequest::Player { id, .. } => {
350                Err(ReferenceFetchError::UnknownResource(id.clone()))
351            }
352        }
353    }
354
355    fn read_fetch(
356        &self,
357        stream: &mut Self::Stream,
358        buffer: &mut [u8],
359    ) -> Result<usize, Self::Error> {
360        self.resources.read_fetch(stream, buffer)
361    }
362}
363
364/// Reads the `key=value` lines one reference response states.
365fn decode_fields(bytes: &[u8]) -> Result<Vec<(String, String)>, ReferenceFetchError> {
366    let text = from_utf8(bytes)
367        .map_err(|_| ReferenceFetchError::UnknownResource("undecodable response".to_owned()))?;
368    Ok(text
369        .lines()
370        .filter_map(|line| line.split_once('='))
371        .map(|(key, value)| (key.to_owned(), value.to_owned()))
372        .collect())
373}
374
375/// Observes the first value stated under one field name.
376fn field<'a>(fields: &'a [(String, String)], key: &str) -> Option<&'a str> {
377    fields
378        .iter()
379        .find(|(name, _)| name == key)
380        .map(|(_, value)| value.as_str())
381}
382
383/// Reads one `itag;extension;url` format description.
384fn decode_format(value: &str) -> Option<YoutubeFormat> {
385    let mut parts = value.split(';');
386    let id = parts.next()?.to_owned();
387    let extension = parts.next()?.to_owned();
388    let url = parts.next()?.to_owned();
389    Some(YoutubeFormat {
390        id,
391        source: YoutubeFormatSource::Direct(url),
392        extension: Some(extension),
393        has_audio: true,
394        has_video: true,
395        quality: None,
396        width: None,
397        height: Some(360),
398        bitrate: Some(500_000),
399        content_length: None,
400        codecs: None,
401    })
402}
403
404/// Observes one query value without depending on a serialization library.
405fn query_value(url: &str, key: &str) -> Option<String> {
406    url.split_once('?')?.1.split('&').find_map(|pair| {
407        let (name, value) = pair.split_once('=')?;
408        (name == key).then(|| value.to_owned())
409    })
410}
411
412/// Replaces or appends one query value.
413fn with_query(url: &str, key: &str, value: &str) -> String {
414    let (base, query) = url.split_once('?').unwrap_or((url, ""));
415    let pairs: Vec<String> = query
416        .split('&')
417        .filter(|pair| !pair.is_empty() && pair.split_once('=').is_none_or(|(name, _)| name != key))
418        .map(str::to_owned)
419        .chain([format!("{key}={value}")])
420        .collect();
421    format!("{base}?{}", pairs.join("&"))
422}