1use 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#[derive(Clone, Debug, PartialEq, Eq)]
15pub enum YoutubeRequest {
16 Watch(String),
18 Player {
20 id: String,
22 api_key: String,
24 claim: PlayerClaim,
26 },
27 PlayerProgram(String),
29 Media(String),
31}
32
33#[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
69pub const REFERENCE_WATCH_URL: &str = "https://www.youtube.com/watch?v=law123";
71
72pub const REFERENCE_VIDEO_ID: &str = "law123";
74
75pub const REFERENCE_MEDIA_URL: &str = "memory://law-media";
77
78pub const REFERENCE_MEDIA_EXTENSION: &str = "mp4";
80
81pub const REFERENCE_PLAYER_PROGRAM_URL: &str = "memory://law-player.js";
83
84pub const REFERENCE_PLAYER_PROGRAM: &str = "reference player program\ntimestamp=19000\n";
86
87#[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 pub fn solve(&mut self, challenge: YoutubeChallenge, solution: impl Into<String>) {
101 self.solutions.push((challenge, solution.into()));
102 }
103
104 #[must_use]
106 pub fn executed(&self) -> Vec<YoutubeRequest> {
107 self.executed.borrow().clone()
108 }
109
110 #[must_use]
112 pub fn applications(&self) -> Vec<Vec<YoutubeChallenge>> {
113 self.applications.borrow().clone()
114 }
115
116 #[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 #[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 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#[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 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 #[must_use]
312 pub fn file(&self, path: &Path) -> Option<Vec<u8>> {
313 self.resources.file(path)
314 }
315
316 #[must_use]
318 pub fn opened(&self) -> Vec<YoutubeRequest> {
319 self.opened.borrow().clone()
320 }
321
322 #[must_use]
324 pub fn events(&self) -> Vec<DownloadEvent> {
325 self.resources.events()
326 }
327
328 #[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
364fn 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
375fn 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
383fn 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
404fn 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
412fn 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}