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(&self, id: impl Into<String>, api_key: impl Into<String>, claim: &PlayerClaim) -> Self::Request {
48 YoutubeRequest::Player { id: id.into(), api_key: api_key.into(), claim: claim.clone() }
49 }
50
51 fn player_program_request(&self, url: impl Into<String>) -> Self::Request {
52 YoutubeRequest::PlayerProgram(url.into())
53 }
54
55 fn media_request(&self, url: impl Into<String>) -> Self::Request {
56 YoutubeRequest::Media(url.into())
57 }
58}
59
60pub const REFERENCE_WATCH_URL: &str = "https://www.youtube.com/watch?v=law123";
62
63pub const REFERENCE_VIDEO_ID: &str = "law123";
65
66pub const REFERENCE_MEDIA_URL: &str = "memory://law-media";
68
69pub const REFERENCE_MEDIA_EXTENSION: &str = "mp4";
71
72pub const REFERENCE_PLAYER_PROGRAM_URL: &str = "memory://law-player.js";
74
75pub const REFERENCE_PLAYER_PROGRAM: &str = "reference player program\ntimestamp=19000\n";
77
78#[derive(Debug, Default)]
83pub struct ReferenceYoutubeEnv {
84 solutions: Vec<(YoutubeChallenge, String)>,
85 applications: RefCell<Vec<Vec<YoutubeChallenge>>>,
86 executed: RefCell<Vec<YoutubeRequest>>,
87}
88
89impl ReferenceYoutubeEnv {
90 pub fn solve(&mut self, challenge: YoutubeChallenge, solution: impl Into<String>) {
92 self.solutions.push((challenge, solution.into()));
93 }
94
95 #[must_use]
97 pub fn executed(&self) -> Vec<YoutubeRequest> {
98 self.executed.borrow().clone()
99 }
100
101 #[must_use]
103 pub fn applications(&self) -> Vec<Vec<YoutubeChallenge>> {
104 self.applications.borrow().clone()
105 }
106
107 #[must_use]
109 pub fn watch_bytes() -> Vec<u8> {
110 format!(
111 "player=yes\napi_key=law-key\ntitle=Law Video\nid={REFERENCE_VIDEO_ID}\njs={REFERENCE_PLAYER_PROGRAM_URL}\nvisitor=law-session\n"
112 )
113 .into_bytes()
114 }
115
116 #[must_use]
118 pub fn player_bytes() -> Vec<u8> {
119 format!("status=OK\ntitle=Law Video\nformat=18;{REFERENCE_MEDIA_EXTENSION};{REFERENCE_MEDIA_URL}\n")
120 .into_bytes()
121 }
122}
123
124impl YoutubeSorts for ReferenceYoutubeEnv {
125 type Request = YoutubeRequest;
126 type Solutions = Vec<(YoutubeChallenge, String)>;
127}
128
129impl YoutubeRequestAlg for ReferenceYoutubeEnv {
130 fn watch_request(&self, url: impl Into<String>) -> Self::Request {
131 YoutubeRequestSyntax.watch_request(url)
132 }
133
134 fn player_request(&self, id: impl Into<String>, api_key: impl Into<String>, claim: &PlayerClaim) -> Self::Request {
135 YoutubeRequestSyntax.player_request(id, api_key, claim)
136 }
137
138 fn player_program_request(&self, url: impl Into<String>) -> Self::Request {
139 YoutubeRequestSyntax.player_program_request(url)
140 }
141
142 fn media_request(&self, url: impl Into<String>) -> Self::Request {
143 YoutubeRequestSyntax.media_request(url)
144 }
145}
146
147impl YoutubeClientAlg for ReferenceYoutubeEnv {
148 fn player_clients(&self) -> impl Iterator<Item = &str> {
149 ["REFERENCE"].into_iter()
150 }
151}
152
153impl YoutubeProgramAlg for ReferenceYoutubeEnv {
154 fn program_timestamp(&self, program: &str) -> Option<i64> {
155 program.lines().find_map(|line| line.strip_prefix("timestamp="))?.parse().ok()
156 }
157}
158
159impl YoutubeRequestBytesAlg for ReferenceYoutubeEnv {
160 type Error = ReferenceFetchError;
161
162 fn youtube_request_bytes(&self, request: &Self::Request) -> Result<Vec<u8>, Self::Error> {
163 self.executed.borrow_mut().push(request.clone());
164 match request {
165 YoutubeRequest::Watch(url) if url == REFERENCE_WATCH_URL => Ok(Self::watch_bytes()),
166 YoutubeRequest::PlayerProgram(url) if url == REFERENCE_PLAYER_PROGRAM_URL => {
167 Ok(REFERENCE_PLAYER_PROGRAM.as_bytes().to_vec())
168 }
169 YoutubeRequest::Player { id, .. } if id == REFERENCE_VIDEO_ID => Ok(Self::player_bytes()),
170 YoutubeRequest::Watch(url) | YoutubeRequest::PlayerProgram(url) | YoutubeRequest::Media(url) => {
171 Err(ReferenceFetchError::UnknownResource(url.clone()))
172 }
173 YoutubeRequest::Player { id, .. } => Err(ReferenceFetchError::UnknownResource(id.clone())),
174 }
175 }
176}
177
178impl YoutubeResponseAlg for ReferenceYoutubeEnv {
179 type Error = ReferenceFetchError;
180
181 fn decode_youtube_watch(&self, bytes: &[u8]) -> Result<YoutubeWatchPage, Self::Error> {
182 let fields = decode_fields(bytes)?;
183 Ok(YoutubeWatchPage {
184 has_player_response: field(&fields, "player") == Some("yes"),
185 api_key: field(&fields, "api_key").map(str::to_owned),
186 player_url: field(&fields, "js").map(str::to_owned),
187 visitor_data: field(&fields, "visitor").map(str::to_owned),
188 title: field(&fields, "title").map(str::to_owned),
189 })
190 }
191
192 fn decode_youtube_player(&self, bytes: &[u8]) -> Result<YoutubePlayer, Self::Error> {
193 let fields = decode_fields(bytes)?;
194 let formats =
195 fields.iter().filter(|(key, _)| key == "format").filter_map(|(_, value)| decode_format(value)).collect();
196 Ok(YoutubePlayer {
197 status: field(&fields, "status").map(str::to_owned),
198 reason: field(&fields, "reason").map(str::to_owned),
199 title: field(&fields, "title").map(str::to_owned),
200 formats,
201 unreadable: 0,
202 })
203 }
204}
205
206impl YoutubeUrlAlg for ReferenceYoutubeEnv {
207 fn throttle_challenge(&self, url: &str) -> Option<String> {
208 query_value(url, "n")
209 }
210
211 fn with_throttle(&self, url: &str, solution: &str) -> String {
212 with_query(url, "n", solution)
213 }
214
215 fn with_signature(&self, url: &str, parameter: &str, signature: &str) -> String {
216 with_query(url, parameter, signature)
217 }
218}
219
220impl YoutubeChallengeAlg for ReferenceYoutubeEnv {
221 type Error = Infallible;
222
223 fn solve_challenges(
224 &self,
225 program: &str,
226 challenges: impl IntoIterator<Item = YoutubeChallenge>,
227 ) -> Result<Self::Solutions, Self::Error> {
228 let posed: Vec<YoutubeChallenge> = challenges.into_iter().collect();
229 self.applications.borrow_mut().push(posed.clone());
230 if program.is_empty() {
232 return Ok(Vec::new());
233 }
234 Ok(posed
235 .into_iter()
236 .filter_map(|challenge| {
237 self.solutions
238 .iter()
239 .find(|(known, _)| *known == challenge)
240 .map(|(_, solution)| (challenge, solution.clone()))
241 })
242 .collect())
243 }
244}
245
246impl YoutubeSolutionAlg for ReferenceYoutubeEnv {
247 fn solution_of(&self, solutions: &Self::Solutions, challenge: &YoutubeChallenge) -> Option<String> {
248 solutions.iter().find(|(posed, _)| posed == challenge).map(|(_, solution)| solution.clone())
249 }
250}
251
252#[allow(clippy::duplicated_attributes, reason = "one delegation per capability, not per target")]
257#[derive(Debug, Default, Delegate)]
258#[delegate(AtomicPublishAlg, target = "resources")]
259#[delegate(DownloadObservationAlg, target = "resources")]
260#[delegate(DownloadReportAlg, target = "resources")]
261#[delegate(DownloadProgressAlg, target = "resources")]
262pub struct ReferenceYoutubeDownloadEnv {
263 resources: ReferenceDownloadEnv,
264 opened: RefCell<Vec<YoutubeRequest>>,
265}
266
267impl ReferenceYoutubeDownloadEnv {
268 pub fn register_resource(&mut self, url: impl Into<String>, bytes: impl Into<Vec<u8>>) {
270 self.resources.register_resource(url, bytes);
271 }
272
273 #[must_use]
275 pub fn file(&self, path: &Path) -> Option<Vec<u8>> {
276 self.resources.file(path)
277 }
278
279 #[must_use]
281 pub fn opened(&self) -> Vec<YoutubeRequest> {
282 self.opened.borrow().clone()
283 }
284
285 #[must_use]
287 pub fn events(&self) -> Vec<DownloadEvent> {
288 self.resources.events()
289 }
290
291 #[must_use]
293 pub fn progress(&self) -> Vec<DownloadProgress> {
294 self.resources.progress()
295 }
296}
297
298impl FetchStreamAlg<YoutubeRequest> for ReferenceYoutubeDownloadEnv {
299 type Error = ReferenceFetchError;
300 type Stream = Cursor<Vec<u8>>;
301
302 fn open_fetch(&self, request: &YoutubeRequest) -> Result<FetchStream<Self::Stream>, Self::Error> {
303 self.opened.borrow_mut().push(request.clone());
304 match request {
305 YoutubeRequest::Media(url) => self.resources.open_fetch(url),
306 YoutubeRequest::Watch(url) | YoutubeRequest::PlayerProgram(url) => {
307 Err(ReferenceFetchError::UnknownResource(url.clone()))
308 }
309 YoutubeRequest::Player { id, .. } => Err(ReferenceFetchError::UnknownResource(id.clone())),
310 }
311 }
312
313 fn read_fetch(&self, stream: &mut Self::Stream, buffer: &mut [u8]) -> Result<usize, Self::Error> {
314 self.resources.read_fetch(stream, buffer)
315 }
316}
317
318fn decode_fields(bytes: &[u8]) -> Result<Vec<(String, String)>, ReferenceFetchError> {
320 let text = from_utf8(bytes).map_err(|_| ReferenceFetchError::UnknownResource("undecodable response".to_owned()))?;
321 Ok(text
322 .lines()
323 .filter_map(|line| line.split_once('='))
324 .map(|(key, value)| (key.to_owned(), value.to_owned()))
325 .collect())
326}
327
328fn field<'a>(fields: &'a [(String, String)], key: &str) -> Option<&'a str> {
330 fields.iter().find(|(name, _)| name == key).map(|(_, value)| value.as_str())
331}
332
333fn decode_format(value: &str) -> Option<YoutubeFormat> {
335 let mut parts = value.split(';');
336 let id = parts.next()?.to_owned();
337 let extension = parts.next()?.to_owned();
338 let url = parts.next()?.to_owned();
339 Some(YoutubeFormat {
340 id,
341 source: YoutubeFormatSource::Direct(url),
342 extension: Some(extension),
343 has_audio: true,
344 has_video: true,
345 quality: None,
346 width: None,
347 height: Some(360),
348 bitrate: Some(500_000),
349 content_length: None,
350 codecs: None,
351 })
352}
353
354fn query_value(url: &str, key: &str) -> Option<String> {
356 url.split_once('?')?.1.split('&').find_map(|pair| {
357 let (name, value) = pair.split_once('=')?;
358 (name == key).then(|| value.to_owned())
359 })
360}
361
362fn with_query(url: &str, key: &str, value: &str) -> String {
364 let (base, query) = url.split_once('?').unwrap_or((url, ""));
365 let pairs: Vec<String> = query
366 .split('&')
367 .filter(|pair| !pair.is_empty() && pair.split_once('=').is_none_or(|(name, _)| name != key))
368 .map(str::to_owned)
369 .chain([format!("{key}={value}")])
370 .collect();
371 format!("{base}?{}", pairs.join("&"))
372}