diff --git a/Cargo.toml b/Cargo.toml index 2eac990..44b96ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ tracing = { version = "0.1.37", features = ["log"] } indicatif = "0.17.0" anyhow = "1.0" clap = { version = "4.0.29", features = ["derive"] } -tracing-subscriber = "0.3.17" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } serde_yaml = "0.9.19" dirs = "5.0.0" filenamify = "0.1.0" @@ -70,6 +70,7 @@ rstest = "0.21.0" tokio-test = "0.4.2" insta = { version = "1.17.1", features = ["ron", "redactions"] } path_macro = "1.0.0" +tracing-test = "0.2.5" # Included crates rustypipe = { path = ".", version = "0.2.0", default-features = false } @@ -115,3 +116,4 @@ rstest.workspace = true tokio-test.workspace = true insta.workspace = true path_macro.workspace = true +tracing-test.workspace = true diff --git a/cli/Cargo.toml b/cli/Cargo.toml index c5021aa..a4cfc9d 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -52,6 +52,7 @@ serde_json.workspace = true indicatif.workspace = true anyhow.workspace = true clap.workspace = true +tracing.workspace = true tracing-subscriber.workspace = true serde_yaml.workspace = true dirs.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 138e915..5011a13 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -1,18 +1,19 @@ #![warn(clippy::todo, clippy::dbg_macro)] -use std::{path::PathBuf, str::FromStr, time::Duration}; +use std::{path::PathBuf, str::FromStr}; -use anyhow::{Context, Result}; use clap::{Parser, Subcommand, ValueEnum}; use futures::stream::{self, StreamExt}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; -use reqwest::{Client, ClientBuilder}; use rustypipe::{ client::{ClientType, RustyPipe}, model::{UrlTarget, VideoId, YouTubeItem}, param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter}, }; +use rustypipe_downloader::{DownloadQuery, DownloaderBuilder}; use serde::Serialize; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{fmt::MakeWriter, EnvFilter}; #[derive(Parser)] #[clap(author, version, about, long_about = None)] @@ -33,6 +34,41 @@ struct Cli { country: Option, } +#[derive(Parser)] +#[group(multiple = false)] +struct DownloadTarget { + #[clap(short, long)] + output: Option, + #[clap(long)] + output_file: Option, + #[clap(long)] + template: Option, +} + +impl DownloadTarget { + fn assert_dir(&self) { + if self.output_file.is_some() { + panic!("Cannot download multiple videos to a single file") + } else if let Some(template) = &self.template { + if !template.contains("{id}") && !template.contains("{title}") { + panic!("Template must contain {{id}} or {{title}} variables") + } + } + } + + fn apply(&self, q: DownloadQuery) -> DownloadQuery { + if let Some(output_file) = &self.output_file { + q.to_file(output_file) + } else if let Some(output) = &self.output { + q.to_dir(output) + } else if let Some(template) = &self.template { + q.to_template(template) + } else { + q + } + } +} + #[derive(Subcommand)] enum Commands { /// Download a video, playlist, album or channel @@ -40,18 +76,22 @@ enum Commands { Download { /// ID or URL id: String, - /// Output path - #[clap(short, default_value = ".")] - output: PathBuf, + #[clap(flatten)] + target: DownloadTarget, /// Video resolution (e.g. 720, 1080). Set to 0 for audio-only. #[clap(short, long)] resolution: Option, /// Number of videos downloaded in parallel #[clap(short, long, default_value_t = 8)] parallel: usize, + /// Use YouTube Music for downloading playlists + #[clap(long)] + music: bool, /// Limit the number of videos to download #[clap(long, default_value_t = 1000)] limit: usize, + #[clap(long)] + player_type: Option, }, /// Extract video, playlist, album or channel data Get { @@ -116,6 +156,7 @@ enum Commands { #[clap(long)] music: Option, }, + Vdata, } #[derive(Copy, Clone, ValueEnum)] @@ -252,64 +293,6 @@ impl From for ClientType { } } -#[allow(clippy::too_many_arguments)] -async fn download_single_video( - video_id: &str, - video_title: &str, - output_dir: &str, - output_fname: Option, - resolution: Option, - ffmpeg: &str, - rp: &RustyPipe, - http: Client, - multi: MultiProgress, - main: Option, -) -> Result<()> { - let pb = multi.add(ProgressBar::new(1)); - pb.set_style(ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap() - .progress_chars("#>-")); - pb.set_message(format!("Fetching player data for {video_title}")); - - let res = async { - let player_data = rp - .query() - .player(video_id) - .await - .context(format!("Failed to fetch player data for video {video_id}"))?; - - let mut filter = StreamFilter::new(); - if let Some(res) = resolution { - if res == 0 { - filter = filter.no_video(); - } else { - filter = filter.video_max_res(res); - } - } - - rustypipe_downloader::download_video( - &player_data, - output_dir, - output_fname, - None, - &filter, - ffmpeg, - http, - pb, - ) - .await - .context(format!( - "Failed to download video '{}' [{}]", - player_data.details.name, video_id - )) - } - .await; - - if let Some(main) = main { - main.inc(1); - } - res -} - fn print_data(data: &T, format: Format, pretty: bool) { let stdout = std::io::stdout().lock(); match format { @@ -327,55 +310,59 @@ fn print_data(data: &T, format: Format, pretty: bool) { async fn download_video( rp: &RustyPipe, id: &str, - output_dir: &str, - output_fname: Option, + target: &DownloadTarget, resolution: Option, + player_type: Option, + multi: MultiProgress, ) { - let http = ClientBuilder::new() - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0") - .gzip(true) - .brotli(true) - .timeout(Duration::from_secs(10)) - .build() - .expect("unable to build the HTTP client"); - - // Indicatif setup - let multi = MultiProgress::new(); - - download_single_video( - id, - id, - output_dir, - output_fname, - resolution, - "ffmpeg", - rp, - http, - multi, - None, - ) - .await - .unwrap_or_else(|e| println!("ERROR: {e:?}")); + let mut filter = StreamFilter::new(); + if let Some(res) = resolution { + if res == 0 { + filter = filter.no_video(); + } else { + filter = filter.video_max_res(res); + } + } + let dl = DownloaderBuilder::new() + .client(rp) + .stream_filter(filter) + .progress_bar(multi) + .build(); + let mut q = target.apply(dl.download_id(id)); + if let Some(player_type) = player_type { + q = q.player_type(player_type.into()); + } + let res = q.download().await; + if let Err(e) = res { + tracing::error!("[{id}]: {e}") + } } async fn download_videos( rp: &RustyPipe, videos: &[VideoId], - output_dir: &str, - output_fname: Option, + target: &DownloadTarget, resolution: Option, parallel: usize, + player_type: Option, + multi: MultiProgress, ) { - let http = ClientBuilder::new() - .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0") - .gzip(true) - .brotli(true) - .timeout(Duration::from_secs(10)) - .build() - .expect("unable to build the HTTP client"); + let mut filter = StreamFilter::new(); + if let Some(res) = resolution { + if res == 0 { + filter = filter.no_video(); + } else { + filter = filter.video_max_res(res); + } + } + let dl = DownloaderBuilder::new() + .client(rp) + .stream_filter(filter) + .progress_bar(multi.clone()) + .path_precheck() + .build(); // Indicatif setup - let multi = MultiProgress::new(); let main = multi.add(ProgressBar::new( videos.len().try_into().unwrap_or_default(), )); @@ -389,38 +376,62 @@ async fn download_videos( main.tick(); stream::iter(videos) - .map(|video| { - download_single_video( - &video.id, - &video.name, - output_dir, - output_fname.clone(), - resolution, - "ffmpeg", - rp, - http.clone(), - multi.clone(), - Some(main.clone()), - ) - }) - .buffer_unordered(parallel) - .collect::>() - .await - .into_iter() - .for_each(|res| match res { - Ok(_) => {} - Err(e) => { - println!("ERROR: {e:?}"); + .for_each_concurrent(parallel, |video| { + let dl = dl.clone(); + let main = main.clone(); + let id = &video.id; + + let mut q = target.apply(dl.download_entity(video)); + if let Some(player_type) = player_type { + q = q.player_type(player_type.into()); } - }); + + async move { + if let Err(e) = q.download().await { + tracing::error!("[{id}]: {e}"); + } else { + main.inc(1); + } + } + }) + .await; +} + +/// Stderr writer that suspends the progress bars before printing logs +#[derive(Clone)] +struct ProgWriter(MultiProgress); + +impl<'a> MakeWriter<'a> for ProgWriter { + type Writer = ProgWriter; + + fn make_writer(&'a self) -> Self::Writer { + self.clone() + } +} + +impl std::io::Write for ProgWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.suspend(|| std::io::stderr().write(buf)) + } + + fn flush(&mut self) -> std::io::Result<()> { + std::io::stderr().flush() + } } #[tokio::main] async fn main() { - // env_logger::builder().format_timestamp_micros().init(); - tracing_subscriber::fmt::init(); - let cli = Cli::parse(); + let multi = MultiProgress::new(); + + tracing_subscriber::fmt::SubscriberBuilder::default() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .with_writer(ProgWriter(multi.clone())) + .init(); let mut rp = RustyPipe::builder().visitor_data_opt(cli.vdata); if cli.report { @@ -442,48 +453,20 @@ async fn main() { match cli.command { Commands::Download { id, - output, + target, resolution, parallel, + music, limit, + player_type, } => { - // Cases: Existing folder, non-existing file with existing parent folder, - // Error cases: non-existing parent folder, existing file - let output_path = std::fs::canonicalize(output).unwrap(); - if output_path.is_file() { - println!("Output file already exists"); - return; - } - let (output_dir, output_fname) = if output_path.is_dir() { - (output_path.to_string_lossy().to_string(), None) - } else { - let output_dir_parent = output_path.parent().unwrap(); - if !output_dir_parent.is_dir() { - println!( - "Parent folder {} does not exist", - output_dir_parent.to_string_lossy() - ); - return; - } - - ( - output_dir_parent.to_string_lossy().to_string(), - Some( - output_path - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - ), - ) - }; - - let target = rp.query().resolve_string(&id, false).await.unwrap(); - match target { + let url_target = rp.query().resolve_string(&id, false).await.unwrap(); + match url_target { UrlTarget::Video { id, .. } => { - download_video(&rp, &id, &output_dir, output_fname, resolution).await; + download_video(&rp, &id, &target, resolution, player_type, multi).await; } UrlTarget::Channel { id } => { + target.assert_dir(); let mut channel = rp.query().channel_videos(id).await.unwrap(); channel .content @@ -500,38 +483,58 @@ async fn main() { download_videos( &rp, &videos, - &output_dir, - output_fname, + &target, resolution, parallel, + player_type, + multi, ) .await; } UrlTarget::Playlist { id } => { - let mut playlist = rp.query().playlist(id).await.unwrap(); - playlist - .videos - .extend_limit(&rp.query(), limit) - .await - .unwrap(); - let videos: Vec = playlist - .videos - .items - .into_iter() - .take(limit) - .map(VideoId::from) - .collect(); + target.assert_dir(); + let videos: Vec = if music { + let mut playlist = rp.query().music_playlist(id).await.unwrap(); + playlist + .tracks + .extend_limit(&rp.query(), limit) + .await + .unwrap(); + playlist + .tracks + .items + .into_iter() + .take(limit) + .map(VideoId::from) + .collect() + } else { + let mut playlist = rp.query().playlist(id).await.unwrap(); + playlist + .videos + .extend_limit(&rp.query(), limit) + .await + .unwrap(); + playlist + .videos + .items + .into_iter() + .take(limit) + .map(VideoId::from) + .collect() + }; download_videos( &rp, &videos, - &output_dir, - output_fname, + &target, resolution, parallel, + player_type, + multi, ) .await; } UrlTarget::Album { id } => { + target.assert_dir(); let album = rp.query().music_album(id).await.unwrap(); let videos: Vec = album .tracks @@ -542,10 +545,11 @@ async fn main() { download_videos( &rp, &videos, - &output_dir, - output_fname, + &target, resolution, parallel, + player_type, + multi, ) .await; } @@ -740,5 +744,9 @@ async fn main() { print_data(&res, format, pretty); } }, + Commands::Vdata => { + let vd = rp.query().get_visitor_data().await.unwrap(); + println!("{vd}"); + } }; } diff --git a/downloader/src/lib.rs b/downloader/src/lib.rs index a706c3c..0429ef2 100644 --- a/downloader/src/lib.rs +++ b/downloader/src/lib.rs @@ -1,19 +1,31 @@ -#![warn(clippy::todo, clippy::dbg_macro)] +#![warn(missing_docs, clippy::todo, clippy::dbg_macro)] //! # YouTube audio/video downloader mod util; -use std::{borrow::Cow, cmp::Ordering, ffi::OsString, ops::Range, path::PathBuf, time::Duration}; +use std::{ + borrow::Cow, + cmp::Ordering, + ffi::OsString, + ops::Range, + path::{Path, PathBuf}, + sync::Arc, + time::Duration, +}; use futures::stream::{self, StreamExt}; -use indicatif::{ProgressBar, ProgressStyle}; +use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use once_cell::sync::Lazy; use rand::Rng; use regex::Regex; -use reqwest::{header, Client}; +use reqwest::{header, Client, StatusCode}; use rustypipe::{ - model::{traits::FileFormat, AudioCodec, VideoCodec, VideoPlayer}, + client::{ClientType, RustyPipe}, + model::{ + traits::{FileFormat, YtEntity}, + AudioCodec, VideoCodec, VideoPlayer, + }, param::StreamFilter, }; use tokio::{ @@ -21,7 +33,6 @@ use tokio::{ io::AsyncWriteExt, process::Command, }; -use tracing::{debug, info}; use util::DownloadError; @@ -30,6 +41,569 @@ type Result = core::result::Result; const CHUNK_SIZE_MIN: u64 = 9_000_000; const CHUNK_SIZE_MAX: u64 = 10_000_000; +/// RustyPipe audio/video downloader +/// +/// The downloader uses an [`Arc`] internally, so if you are using the client +/// at multiple locations, you can just clone it. +#[derive(Clone)] +pub struct Downloader { + i: Arc, +} + +/// Builder to construct a new downloader +pub struct DownloaderBuilder { + rp: Option, + ffmpeg: String, + multi: Option, + filter: StreamFilter, + video_format: DownloadVideoFormat, + n_retries: u32, + path_precheck: bool, +} + +struct DownloaderInner { + /// YT client + rp: RustyPipe, + /// Path to the ffmpeg binary + ffmpeg: String, + /// Global progress + multi: Option, + /// Default stream filter + filter: StreamFilter, + /// Default video format + video_format: DownloadVideoFormat, + /// Number of retries in case of 403 error + n_retries: u32, + /// Check if destination path exists before player is fetched + path_precheck: bool, +} + +/// Download query +pub struct DownloadQuery { + /// RustyPipe Downloader + dl: Downloader, + /// Video to download + video: DownloadVideo, + /// Destination + dest: DownloadDest, + /// Progress bar + multi: Option, + /// Stream filter + filter: Option, + /// Target video format + video_format: Option, + /// ClientType type for fetching videos + player_type: Option, +} + +#[derive(Default)] +struct DownloadVideo { + id: String, + name: Option, + channel_id: Option, + channel_name: Option, +} + +impl DownloadVideo { + fn from_video(video: &impl YtEntity) -> Self { + DownloadVideo { + id: video.id().to_owned(), + name: Some(video.name().to_owned()), + channel_id: video.channel_id().map(str::to_owned), + channel_name: video + .channel_name() + .map(|n| n.strip_suffix(" - Topic").unwrap_or(n).to_owned()), + } + } +} + +#[derive(Clone)] +enum DownloadDest { + Default, + File(PathBuf), + Dir(PathBuf), + Template(PathBuf), +} + +fn video_filename(v: &DownloadVideo) -> String { + filenamify_lim(&format!( + "{} [{}]", + v.name.as_deref().unwrap_or_default(), + v.id + )) +} + +/// Video container format for downloading +#[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +pub enum DownloadVideoFormat { + /// .mp4 + #[default] + Mp4, + /// .mkv + Mkv, + /// .webm + Webm, +} + +impl DownloadVideoFormat { + /// Get the video format file extension + pub fn extension(&self) -> &'static str { + match self { + DownloadVideoFormat::Mp4 => "mp4", + DownloadVideoFormat::Mkv => "mkv", + DownloadVideoFormat::Webm => "webm", + } + } + + /// Get the video format from the given file extension + pub fn from_extension(ext: &str) -> Option { + match ext { + "mp4" => Some(Self::Mp4), + "mkv" => Some(Self::Mkv), + "webm" => Some(Self::Webm), + _ => None, + } + } +} + +impl DownloadDest { + fn get_dest_path(&self, v: &DownloadVideo) -> PathBuf { + match self { + DownloadDest::Default => PathBuf::from(video_filename(v)), + DownloadDest::File(p) => p.clone(), + DownloadDest::Dir(p) => p.join(video_filename(v)), + DownloadDest::Template(t) => t + .iter() + .map(|part| { + let s = part.to_string_lossy(); + let mut s = s.replace("{id}", &v.id); + if let Some(name) = &v.name { + s = s.replace("{title}", name) + } + if let Some(channel) = &v.channel_name { + s = s.replace("{channel}", channel) + } + if let Some(id) = &v.channel_id { + s = s.replace("{channelId}", id); + } + filenamify_lim(&s) + }) + .collect(), + } + } +} + +impl Default for DownloaderBuilder { + fn default() -> Self { + Self { + rp: None, + ffmpeg: "ffmpeg".to_owned(), + multi: None, + filter: StreamFilter::new(), + video_format: DownloadVideoFormat::Mp4, + n_retries: 3, + path_precheck: false, + } + } +} + +impl DownloaderBuilder { + /// Create a new [`DownloaderBuilder`] + /// + /// This is the same as [`Downloader::builder`] + pub fn new() -> Self { + Self::default() + } + + /// Use a custom [`RustyPipe`] client + #[must_use] + pub fn client(mut self, rp: &RustyPipe) -> Self { + self.rp = Some(rp.clone()); + self + } + + /// Set the path to ffmpeg, used to join video and audio files + /// + /// The default system-wide `ffmpeg` binary is used by default. + #[must_use] + pub fn ffmpeg>(mut self, ffmpeg: S) -> Self { + self.ffmpeg = ffmpeg.into(); + self + } + + /// Set the indicatif [`MultiProgress`] used to show download progress + /// for all downloads + #[must_use] + pub fn progress_bar(mut self, progress: MultiProgress) -> Self { + self.multi = Some(progress); + self + } + + /// Set the default [`StreamFilter`] for all downloads. + /// + /// The filter can be overridden for individual download queries. + #[must_use] + pub fn stream_filter(mut self, filter: StreamFilter) -> Self { + self.filter = filter; + self + } + + /// Set the [`VideoFormat`] of downloaded videos + #[must_use] + pub fn video_format(mut self, video_format: DownloadVideoFormat) -> Self { + self.video_format = video_format; + self + } + + /// Set the number of retries in case a download fails with a 403 error + #[must_use] + pub fn n_retries(mut self, n_retries: u32) -> Self { + self.n_retries = n_retries; + self + } + + /// Enable path precheck + /// + /// The downloader will check if the destination path + /// (predicted from the entity to download and the StreamFilter) exists and + /// skips the download with [`DownloadError::Exists`] without fetching any player data. + /// + /// This allows fast resumption of playlist downloads. + #[must_use] + pub fn path_precheck(mut self) -> Self { + self.path_precheck = true; + self + } + + /// Create a new, configured [`Downloader`] instance + pub fn build(self) -> Downloader { + Downloader { + i: Arc::new(DownloaderInner { + rp: self.rp.unwrap_or_default(), + ffmpeg: self.ffmpeg, + multi: self.multi, + filter: self.filter, + video_format: self.video_format, + n_retries: self.n_retries, + path_precheck: self.path_precheck, + }), + } + } +} + +impl Default for Downloader { + fn default() -> Self { + DownloaderBuilder::new().build() + } +} + +impl Downloader { + /// Create a new [`Downloader`] using the given [`RustyPipe`] instance + pub fn new(rp: &RustyPipe) -> Self { + DownloaderBuilder::new().client(rp).build() + } + + /// Create a new [`DownloaderBuilder`] + /// + /// This is the same as [`DownloaderBuilder::new`] + pub fn builder() -> DownloaderBuilder { + DownloaderBuilder::default() + } + + fn query(&self, video: DownloadVideo) -> DownloadQuery { + DownloadQuery { + dl: self.clone(), + video, + dest: DownloadDest::Default, + multi: None, + filter: None, + video_format: None, + player_type: None, + } + } + + /// Download a video with the given ID + pub fn download_id>(&self, video_id: S) -> DownloadQuery { + self.query(DownloadVideo { + id: video_id.into(), + ..Default::default() + }) + } + + /// Download a video from a [`YtEntity`] object (e.g. playlist/channel video) + /// + /// Providing an entity has the advantage that the download path can be determined before the video + /// is fetched, so already downloaded videos get skipped right away. + pub fn download_entity(&self, video: &impl YtEntity) -> DownloadQuery { + self.query(DownloadVideo::from_video(video)) + } +} + +/// Output data from downloading a video +pub struct DownloadResult { + /// Download destination path + pub dest: PathBuf, + /// Fetched vvideo player data + pub player_data: VideoPlayer, +} + +impl DownloadQuery { + /// Update the video format from the given path extension + /// + /// The video format is not updated if it was already manually set + fn update_video_format(&mut self, path: &Path) { + if self.video_format.is_none() { + self.video_format = path + .extension() + .and_then(|ext| ext.to_str()) + .and_then(DownloadVideoFormat::from_extension); + } + } + + /// Download to the given file + /// + /// Note that the file extension may be changed to fit the reuested video/audio format. + /// Refer to the [`DownloadResult`] to get the actual path after downloading. + pub fn to_file>(mut self, file: P) -> Self { + let file = file.into(); + self.update_video_format(&file); + self.dest = DownloadDest::File(file); + self + } + + /// Download to the given directory + /// + /// The filename is created by this template: `{title} [{id}]`. + /// + /// You can use a custom filename template using [`DownloadQuery::to_template`] + pub fn to_dir>(mut self, dir: P) -> Self { + self.dest = DownloadDest::Dir(dir.into()); + self + } + + /// Download to the given filename template + /// + /// Templates are paths that may contain variables for video metadata. + /// + /// ## Variables + /// - `{id}` Video ID + /// - `{title}` Video title + /// - `{channel}` Channel name + /// - `{channel_id}` Channel ID + /// + /// Note that the file extension may be changed to fit the reuested video/audio format. + /// Refer to the [`DownloadResult`] to get the actual path after downloading. + pub fn to_template>(mut self, tmpl: P) -> Self { + let tmpl = tmpl.into(); + self.update_video_format(&tmpl); + self.dest = DownloadDest::Template(tmpl); + self + } + + /// Use a [`MultiProgress`] progress bar for all downloads + pub fn progress_bar(mut self, progress: MultiProgress) -> Self { + self.multi = Some(progress); + self + } + + /// Set a [`StreamFilter`] for choosing a stream to be downloaded + pub fn stream_filter(mut self, filter: StreamFilter) -> Self { + self.filter = Some(filter); + self + } + + /// Set the [`VideoFormat`] of downloaded videos + pub fn video_format(mut self, video_format: DownloadVideoFormat) -> Self { + self.video_format = Some(video_format); + self + } + + /// Set the [`ClientType`] used to fetch the YT player + pub fn player_type(mut self, player_type: ClientType) -> Self { + self.player_type = Some(player_type); + self + } + + /// Download the video + #[tracing::instrument(skip(self), fields(id = self.video.id))] + pub async fn download(&self) -> Result { + let mut last_err = None; + + // Progress bar + let multi = self.multi.clone().or_else(|| self.dl.i.multi.clone()); + let pb = multi.map(|m| { + let pb = ProgressBar::new(1); + pb.set_style(ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap() + .progress_chars("#>-")); + m.add(pb) + }); + + for n in 0..=self.dl.i.n_retries { + let err = match self.download_attempt(&pb, n).await { + Ok(res) => return Ok(res), + Err(DownloadError::Http(e)) => { + if e.status() != Some(StatusCode::FORBIDDEN) { + return Err(DownloadError::Http(e)); + } + DownloadError::Http(e) + } + Err(e) => return Err(e), + }; + + if n != self.dl.i.n_retries { + tracing::warn!("Retry attempt #{}. Error: {}", n + 1, err); + tokio::time::sleep(Duration::from_secs(1)).await; + } + last_err = Some(err); + } + Err(last_err.unwrap()) + } + + async fn download_attempt(&self, pb: &Option, n: u32) -> Result { + let filter = self.filter.as_ref().unwrap_or(&self.dl.i.filter); + let video_format = self.video_format.unwrap_or(self.dl.i.video_format); + + // Check if already downloaded + if self.video.name.is_some() && self.dl.i.path_precheck { + let op = self.dest.get_dest_path(&self.video); + + if filter.is_video_none() { + for ext in ["m4a", "opus"] { + let p = op.with_extension(ext); + if p.is_file() { + return Err(DownloadError::Exists(p)); + } + } + } else { + let p = op.with_extension(video_format.extension()); + if p.is_file() { + return Err(DownloadError::Exists(p)); + } + } + } + + let attempt_suffix = if n > 0 { + format!(" (retry #{n})") + } else { + String::new() + }; + if let Some(pb) = pb { + pb.set_message(format!( + "Fetching player data for {}{}", + self.video.name.as_deref().unwrap_or_default(), + attempt_suffix + )) + } + + let q = self.dl.i.rp.query(); + let player_data = match self.player_type { + Some(player_type) => q.player_from_client(&self.video.id, player_type).await?, + None => q.player(&self.video.id).await?, + }; + let user_agent = q.user_agent(player_data.client_type); + + // Select streams to download + let (video, audio) = player_data.select_video_audio_stream(filter); + + if video.is_none() && audio.is_none() { + return Err(DownloadError::Input("no stream found".into())); + } + + let extension = match video { + Some(_) => video_format.extension(), + None => match audio { + Some(audio) => match audio.codec { + AudioCodec::Mp4a => "m4a", + AudioCodec::Opus => "opus", + _ => return Err(DownloadError::Input("unknown audio codec".into())), + }, + None => unreachable!(), + }, + }; + + let pv = DownloadVideo::from_video(&player_data); + let output_path = self.dest.get_dest_path(&pv).with_extension(extension); + + if output_path.exists() { + return Err(DownloadError::Exists(output_path)); + } + if let Some(parent) = output_path.parent() { + std::fs::create_dir_all(parent)?; + } + + let mut downloads: Vec = Vec::new(); + + if let Some(v) = video { + downloads.push(StreamDownload { + file: output_path.with_extension(format!("video{}", v.format.extension())), + url: v.url.clone(), + video_codec: Some(v.codec), + audio_codec: None, + }); + } + if let Some(a) = audio { + downloads.push(StreamDownload { + file: output_path.with_extension(format!("audio{}", a.format.extension())), + url: a.url.clone(), + video_codec: None, + audio_codec: Some(a.codec), + }); + } + + if let Some(pb) = pb { + pb.set_message(format!( + "Downloading {}{}", + player_data.name(), + attempt_suffix + )) + } + download_streams( + &downloads, + self.dl.i.rp.http_client(), + &user_agent, + pb.clone(), + ) + .await?; + + if let Some(pb) = &pb { + pb.set_message(format!("Converting {}", player_data.name())); + pb.set_style( + ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}]") + .unwrap(), + ); + pb.enable_steady_tick(Duration::from_millis(500)); + } + + convert_streams( + &downloads, + &output_path, + &self.dl.i.ffmpeg, + player_data.name(), + ) + .await?; + if let Some(pb) = pb { + pb.disable_steady_tick(); + } + + // Delete original files + stream::iter(&downloads) + .map(|d| fs::remove_file(d.file.clone())) + .buffer_unordered(downloads.len()) + .collect::>() + .await + .into_iter() + .collect::>()?; + + if let Some(pb) = pb { + pb.finish_and_clear(); + } + Ok(DownloadResult { + dest: output_path, + player_data, + }) + } +} + fn get_download_range(offset: u64, size: Option) -> Range { let mut rng = rand::thread_rng(); let chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX); @@ -64,11 +638,26 @@ fn parse_cr_header(cr_header: &str) -> Result<(u64, u64)> { )) } +fn filenamify_lim(name: &str) -> String { + let lim = 200; + let n = filenamify::filenamify(name); + + if n.len() > lim { + n.char_indices() + .take_while(|(i, _)| i < &lim) + .map(|(_, c)| c) + .collect::() + } else { + n + } +} + async fn download_single_file>( url: &str, output: P, - http: Client, - pb: ProgressBar, + http: &Client, + user_agent: &str, + pb: Option, ) -> Result<()> { // Check if file is already downloaded let output_path: PathBuf = output.into(); @@ -99,6 +688,7 @@ async fn download_single_file>( let res = http .head(url.to_owned()) + .header(header::USER_AGENT, user_agent) .header(header::RANGE, "bytes=0-0") .send() .await? @@ -125,8 +715,10 @@ async fn download_single_file>( size = Some(original_size); offset = file_size; - pb.inc_length(original_size); - pb.inc(offset); + if let Some(pb) = &pb { + pb.inc_length(original_size); + pb.inc(offset); + } } Ordering::Equal => { // Already downloaded @@ -153,9 +745,10 @@ async fn download_single_file>( .await?; if is_gvideo && size.is_some() { - download_chunks_by_param(http, &mut file, url, size.unwrap(), offset, pb).await?; + download_chunks_by_param(http, &mut file, url, size.unwrap(), offset, user_agent, pb) + .await?; } else { - download_chunks_by_header(http, &mut file, url, size, offset, pb).await?; + download_chunks_by_header(http, &mut file, url, size, offset, user_agent, pb).await?; } fs::rename(&output_path_tmp, &output_path).await?; @@ -166,22 +759,24 @@ async fn download_single_file>( // This is the standardized method that works on all web servers, // but I have observed throttling using this method. async fn download_chunks_by_header( - http: Client, + http: &Client, file: &mut File, url: &str, size: Option, offset: u64, - pb: ProgressBar, + user_agent: &str, + pb: Option, ) -> Result<()> { let mut offset = offset; let mut size = size; loop { let range = get_download_range(offset, size); - debug!("Fetching range {}-{}", range.start, range.end); + tracing::debug!("Fetching range {}-{}", range.start, range.end); let res = http .get(url.to_owned()) + .header(header::USER_AGENT, user_agent) .header(header::ORIGIN, "https://www.youtube.com") .header(header::REFERER, "https://www.youtube.com/") .header( @@ -211,15 +806,19 @@ async fn download_chunks_by_header( offset = parsed_offset + 1; if size.is_none() { size = Some(parsed_size); - pb.inc_length(parsed_size); + if let Some(pb) = &pb { + pb.inc_length(parsed_size); + } } - debug!("Retrieving chunks..."); + tracing::debug!("Retrieving chunks..."); let mut stream = res.bytes_stream(); while let Some(item) = stream.next().await { // Retrieve chunk. let mut chunk = item?; - pb.inc(chunk.len() as u64); + if let Some(pb) = &pb { + pb.inc(chunk.len() as u64); + } file.write_all_buf(&mut chunk).await?; } @@ -234,22 +833,26 @@ async fn download_chunks_by_header( // This ist used by YouTube's web player. The file size // must be known beforehand (it is included in the stream url). async fn download_chunks_by_param( - http: Client, + http: &Client, file: &mut File, url: &str, size: u64, offset: u64, - pb: ProgressBar, + user_agent: &str, + pb: Option, ) -> Result<()> { let mut offset = offset; - pb.inc_length(size); + if let Some(pb) = &pb { + pb.inc_length(size); + } loop { let range = get_download_range(offset, Some(size)); - debug!("Fetching range {}-{}", range.start, range.end); + tracing::debug!("Fetching range {}-{}", range.start, range.end); let res = http .get(format!("{}&range={}-{}", url, range.start, range.end)) + .header(header::USER_AGENT, user_agent) .header(header::ORIGIN, "https://www.youtube.com") .header(header::REFERER, "https://www.youtube.com/") .send() @@ -258,17 +861,19 @@ async fn download_chunks_by_param( let clen = res.content_length().unwrap(); - debug!("Retrieving chunks..."); + tracing::debug!("Retrieving chunks..."); let mut stream = res.bytes_stream(); while let Some(item) = stream.next().await { // Retrieve chunk. let mut chunk = item?; - pb.inc(chunk.len() as u64); + if let Some(pb) = &pb { + pb.inc(chunk.len() as u64); + } file.write_all_buf(&mut chunk).await?; } offset += clen; - debug!("offset inc by {}, new: {}", clen, offset); + tracing::debug!("offset inc by {}, new: {}", clen, offset); if offset >= size { break; } @@ -279,146 +884,21 @@ async fn download_chunks_by_param( #[allow(dead_code)] struct StreamDownload { file: PathBuf, - // track_name: String TODO: add for multiple audio languages, url: String, audio_codec: Option, video_codec: Option, } -#[allow(clippy::too_many_arguments)] -pub async fn download_video( - player_data: &VideoPlayer, - output_dir: &str, - output_fname: Option, - output_format: Option, - filter: &StreamFilter<'_>, - ffmpeg: &str, - http: Client, - pb: ProgressBar, -) -> Result<()> { - // Download filepath - let download_dir = PathBuf::from(output_dir); - let title = player_data.details.name.clone(); - let output_fname_set = output_fname.is_some(); - let output_fname = output_fname.unwrap_or_else(|| { - filenamify::filenamify(format!("{} [{}]", title, player_data.details.id)) - }); - - // Select streams to download - let (video, audio) = player_data.select_video_audio_stream(filter); - - if video.is_none() && audio.is_none() { - return Err(DownloadError::Input("no stream found".into())); - } - - let format = output_format.unwrap_or( - match video { - Some(_) => "mp4", - None => match audio { - Some(audio) => match audio.codec { - AudioCodec::Mp4a => "m4a", - AudioCodec::Opus => "opus", - _ => return Err(DownloadError::Input("unknown audio codec".into())), - }, - None => unreachable!(), - }, - } - .to_owned(), - ); - - let output_path = download_dir.join(&output_fname).with_extension(&format); - if output_path.exists() { - // If the downloaded video already exists, only error if the download path was - // chosen explicitly. - if output_fname_set { - return Err(DownloadError::Input( - format!("File {} already exists", output_path.to_string_lossy()).into(), - ))?; - } - info!( - "Downloaded video {} already exists", - output_path.to_string_lossy() - ); - return Ok(()); - } - - match (video, audio) { - // Downloading combined video/audio stream (no conversion) - (Some(video), None) => { - pb.set_message(format!("Downloading {title}")); - download_single_file( - &video.url, - download_dir.join(output_fname).with_extension(&format), - http, - pb.clone(), - ) - .await?; - } - // Downloading split video/audio streams (requires conversion with ffmpeg) - _ => { - let mut downloads: Vec = Vec::new(); - - if let Some(v) = video { - downloads.push(StreamDownload { - file: download_dir.join(format!( - "{}.video{}", - output_fname, - v.format.extension() - )), - url: v.url.clone(), - video_codec: Some(v.codec), - audio_codec: None, - }); - } - if let Some(a) = audio { - downloads.push(StreamDownload { - file: download_dir.join(format!( - "{}.audio{}", - output_fname, - a.format.extension() - )), - url: a.url.clone(), - video_codec: None, - audio_codec: Some(a.codec), - }); - } - - pb.set_message(format!("Downloading {title}")); - download_streams(&downloads, http, pb.clone()).await?; - - pb.set_message(format!("Converting {title}")); - pb.set_style( - ProgressStyle::with_template("{msg}\n{spinner:.green} [{elapsed_precise}]") - .unwrap(), - ); - pb.enable_steady_tick(Duration::from_millis(100)); - convert_streams(&downloads, output_path, ffmpeg).await?; - pb.disable_steady_tick(); - - // Delete original files - stream::iter(&downloads) - .map(|d| fs::remove_file(d.file.clone())) - .buffer_unordered(downloads.len()) - .collect::>() - .await - .into_iter() - .collect::>()?; - } - } - - pb.finish_and_clear(); - Ok(()) -} - async fn download_streams( downloads: &Vec, - http: Client, - pb: ProgressBar, + http: &Client, + user_agent: &str, + pb: Option, ) -> Result<()> { let n = downloads.len(); stream::iter(downloads) - .map(|d| download_single_file(&d.url, d.file.clone(), http.clone(), pb.clone())) + .map(|d| download_single_file(&d.url, d.file.clone(), http, user_agent, pb.clone())) .buffer_unordered(n) .collect::>() .await @@ -432,6 +912,7 @@ async fn convert_streams>( downloads: &[StreamDownload], output: P, ffmpeg: &str, + title: &str, ) -> Result<()> { let output_path: PathBuf = output.into(); @@ -451,6 +932,9 @@ async fn convert_streams>( args.push("-c".into()); args.push("copy".into()); + args.push("-metadata".into()); + args.push(format!("title={title}").into()); + args.push(output_path.into()); let res = Command::new(ffmpeg).args(args).output().await?; diff --git a/downloader/src/util.rs b/downloader/src/util.rs index c805afd..d95b8f0 100644 --- a/downloader/src/util.rs +++ b/downloader/src/util.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use std::{borrow::Cow, collections::BTreeMap, path::PathBuf}; use reqwest::Url; @@ -6,18 +6,28 @@ use reqwest::Url; #[derive(thiserror::Error, Debug)] #[non_exhaustive] pub enum DownloadError { + /// RustyPipe error + #[error("{0}")] + RustyPipe(#[from] rustypipe::error::Error), /// Error from the HTTP client #[error("http error: {0}")] Http(#[from] reqwest::Error), /// File IO error #[error(transparent)] Io(#[from] std::io::Error), + /// FFmpeg returned an error #[error("FFmpeg error: {0}")] Ffmpeg(Cow<'static, str>), + /// Error parsing ranges for progressive download #[error("Progressive download error: {0}")] Progressive(Cow<'static, str>), + /// Video could not be downloaded because of invalid player data #[error("input error: {0}")] Input(Cow<'static, str>), + /// Download target already exists + #[error("file {0} already exists")] + Exists(PathBuf), + /// Other error #[error("error: {0}")] Other(Cow<'static, str>), } diff --git a/src/client/channel.rs b/src/client/channel.rs index 9a4ea00..254a39f 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -16,7 +16,9 @@ use crate::{ util::{self, timeago, ProtoBuilder}, }; -use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext}; +use super::{ + response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext, +}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -201,16 +203,13 @@ impl RustyPipeQuery { impl MapResponse>> for response::Channel { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>>, ExtractionError> { - let content = map_channel_content(id, self.contents, self.alerts)?; + let content = map_channel_content(ctx.id, self.contents, self.alerts)?; let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let channel_data = map_channel( MapChannelData { @@ -221,12 +220,11 @@ impl MapResponse>> for response::Channel { has_shorts: content.has_shorts, has_live: content.has_live, }, - id, - lang, + ctx, )?; let mut mapper = response::YouTubeListMapper::::with_channel( - lang, + ctx.lang, &channel_data.c, channel_data.warnings, ); @@ -249,16 +247,13 @@ impl MapResponse>> for response::Channel { impl MapResponse>> for response::Channel { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>>, ExtractionError> { - let content = map_channel_content(id, self.contents, self.alerts)?; + let content = map_channel_content(ctx.id, self.contents, self.alerts)?; let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let channel_data = map_channel( MapChannelData { @@ -269,12 +264,11 @@ impl MapResponse>> for response::Channel { has_shorts: content.has_shorts, has_live: content.has_live, }, - id, - lang, + ctx, )?; let mut mapper = response::YouTubeListMapper::::with_channel( - lang, + ctx.lang, &channel_data.c, channel_data.warnings, ); @@ -289,13 +283,7 @@ impl MapResponse>> for response::Channel { } impl MapResponse for response::ChannelAbout { - fn map_response( - self, - id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _visitor_data: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // Channel info is always fetched in English. There is no localized data there // and it allows parsing the country name. let lang = Language::En; @@ -309,7 +297,7 @@ impl MapResponse for response::ChannelAbout { .ok_or(ExtractionError::InvalidData("no received endpoint".into()))?, response::ChannelAbout::Content { contents } => { // Handle errors (e.g. age restriction) when regular channel content was returned - map_channel_content(id, contents, None)?; + map_channel_content(ctx.id, contents, None)?; return Err(ExtractionError::InvalidData( "could not extract aboutData".into(), )); @@ -388,36 +376,35 @@ struct MapChannelData { fn map_channel( d: MapChannelData, - id: &str, - lang: Language, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let header = d.header.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no header".into(), })?; let metadata = d .metadata .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no metadata".into(), })? .channel_metadata_renderer; let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no microformat".into(), })?; - if metadata.external_id != id { + if metadata.external_id != ctx.id { return Err(ExtractionError::WrongResult(format!( "got wrong channel id {}, expected {}", - metadata.external_id, id + metadata.external_id, ctx.id ))); } let vanity_url = metadata .vanity_channel_url .as_ref() - .and_then(|url| map_vanity_url(url, id)); + .and_then(|url| map_vanity_url(url, ctx.id)); let mut warnings = Vec::new(); Ok(MapResult { @@ -425,9 +412,9 @@ fn map_channel( response::channel::Header::C4TabbedHeaderRenderer(header) => Channel { id: metadata.external_id, name: metadata.title, - subscriber_count: header - .subscriber_count_text - .and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)), + subscriber_count: header.subscriber_count_text.and_then(|txt| { + util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings) + }), avatar: header.avatar.into(), verification: header.badges.into(), description: metadata.description, @@ -458,7 +445,7 @@ fn map_channel( name: metadata.title, subscriber_count: hdata.as_ref().and_then(|hdata| { hdata.0.as_ref().and_then(|txt| { - util::parse_large_numstr_or_warn(txt, lang, &mut warnings) + util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings) }) }), avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(), @@ -487,7 +474,7 @@ fn map_channel( md_rows.first().and_then(|md| md.metadata_parts.get(1)) }; let subscriber_count = sub_part.and_then(|t| { - util::parse_large_numstr_or_warn::(&t.text, lang, &mut warnings) + util::parse_large_numstr_or_warn::(&t.text, ctx.lang, &mut warnings) }); Channel { @@ -697,10 +684,10 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, error::{ExtractionError, UnavailabilityReason}, model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem}, - param::{ChannelOrder, ChannelVideoTab, Language}, + param::{ChannelOrder, ChannelVideoTab}, serializer::MapResult, util::tests::TESTFILES, }; @@ -728,7 +715,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult>> = - channel.map_response(id, Language::En, None, None).unwrap(); + channel.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -755,7 +742,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let res: Result>>, ExtractionError> = - channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None); + channel.map_response(&MapRespCtx::test("UCbfnHqxXs_K3kvaH-WlNlig")); if let Err(ExtractionError::Unavailable { reason, msg }) = res { assert_eq!(reason, UnavailabilityReason::AgeRestricted); assert!(msg.starts_with("Laphroaig Whisky: ")); @@ -772,7 +759,7 @@ mod tests { let channel: response::Channel = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult>> = channel - .map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ")) .unwrap(); assert!( @@ -791,7 +778,7 @@ mod tests { let channel: response::ChannelAbout = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = channel - .map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ")) .unwrap(); assert!( diff --git a/src/client/mod.rs b/src/client/mod.rs index c428139..e6761e8 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -225,6 +225,7 @@ struct RustyPipeRef { n_http_retries: u32, cache: CacheHolder, default_opts: RustyPipeOpts, + user_agent: Cow<'static, str>, } #[derive(Clone)] @@ -432,7 +433,7 @@ impl Default for RustyPipeBuilder { } impl RustyPipeBuilder { - /// Return a new `RustyPipeBuilder`. + /// Create a new [`RustyPipeBuilder`]. /// /// This is the same as [`RustyPipe::builder`] #[must_use] @@ -448,15 +449,20 @@ impl RustyPipeBuilder { } } - /// Return a new, configured RustyPipe instance. + /// Create a new, configured [`RustyPipe`] instance. pub fn build(self) -> Result { self.build_with_client(ClientBuilder::new()) } - /// Return a new, configured RustyPipe instance using a Reqwest client builder. + /// Create a new, configured RustyPipe instance using a Reqwest client builder. pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result { + let user_agent = self + .user_agent + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(DEFAULT_UA)); + client_builder = client_builder - .user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned())) + .user_agent(user_agent.as_ref()) .gzip(true) .brotli(true) .redirect(reqwest::redirect::Policy::none()); @@ -503,6 +509,7 @@ impl RustyPipeBuilder { deobf: RwLock::new(cdata.deobf), }, default_opts: self.default_opts, + user_agent, }), }) } @@ -710,6 +717,14 @@ impl RustyPipe { } } + /// Get the internal HTTP client + /// + /// Can be used for downloading videos or custom YT requests. + #[must_use] + pub fn http_client(&self) -> &Client { + &self.inner.http + } + /// Execute the given http request. async fn http_request(&self, request: &Request) -> Result { let mut last_resp = None; @@ -963,7 +978,14 @@ impl RustyPipe { /// visitor data is extracted from the html page. async fn get_visitor_data(&self) -> Result { tracing::debug!("getting YT visitor data"); - let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?; + let resp = self + .inner + .http + .get(YOUTUBE_MUSIC_HOME_URL) + .header(header::ORIGIN, YOUTUBE_MUSIC_HOME_URL) + .header(header::REFERER, YOUTUBE_MUSIC_HOME_URL) + .send() + .await?; let vdata = resp .headers() @@ -972,7 +994,10 @@ impl RustyPipe { .find_map(|c| { if let Ok(cookie) = c.to_str() { if let Some(after) = cookie.strip_prefix("__Secure-YEC=") { - return after.split_once(';').map(|s| s.0.to_owned()); + return after + .split_once(';') + .map(|s| s.0.to_owned()) + .filter(|s| !s.is_empty()); } } None @@ -1065,6 +1090,27 @@ impl RustyPipeQuery { self } + /// Get the user agent for the given client type + /// + /// This can be used for additional HTTP requests (e.g. downloading/streaming) + pub fn user_agent(&self, ctype: ClientType) -> Cow<'_, str> { + match ctype { + ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => { + Cow::Borrowed(&self.client.inner.user_agent) + } + ClientType::Android => format!( + "com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip", + MOBILE_CLIENT_VERSION, self.opts.country + ) + .into(), + ClientType::Ios => format!( + "com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})", + MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, self.opts.country + ) + .into(), + } + } + /// Create a new context object, which is included in every request to /// the YouTube API and contains language, country and device parameters. /// @@ -1227,13 +1273,6 @@ impl RustyPipeQuery { .post(format!( "{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}" )) - .header( - header::USER_AGENT, - format!( - "com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip", - MOBILE_CLIENT_VERSION, self.opts.country - ), - ) .header("X-Goog-Api-Format-Version", "2"), ClientType::Ios => self .client @@ -1242,15 +1281,9 @@ impl RustyPipeQuery { .post(format!( "{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}" )) - .header( - header::USER_AGENT, - format!( - "com.google.ios.youtube/{} ({}; U; CPU iOS 15_4 like Mac OS X; {})", - MOBILE_CLIENT_VERSION, IOS_DEVICE_MODEL, self.opts.country - ), - ) .header("X-Goog-Api-Format-Version", "2"), }; + r = r.header(header::USER_AGENT, self.user_agent(ctype).as_ref()); if let Some(vdata) = self.opts.visitor_data.as_deref().or(visitor_data) { r = r.header("X-Goog-EOM-Visitor-Id", vdata); } @@ -1268,9 +1301,7 @@ impl RustyPipeQuery { async fn yt_request_attempt + Debug, M>( &self, request: &Request, - id: &str, - visitor_data: Option<&str>, - deobf: Option<&DeobfData>, + ctx: &MapRespCtx<'_>, ) -> Result, Error> { let response = self .client @@ -1289,7 +1320,7 @@ impl RustyPipeQuery { Err(match status { StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: error_msg.unwrap_or("404".into()), }), StatusCode::BAD_REQUEST => { @@ -1299,12 +1330,7 @@ impl RustyPipeQuery { }) } else { match serde_json::from_str::(&body) { - Ok(deserialized) => match deserialized.map_response( - id, - self.opts.lang, - deobf, - self.opts.visitor_data.as_deref().or(visitor_data), - ) { + Ok(deserialized) => match deserialized.map_response(ctx) { Ok(mapres) => Ok(mapres), Err(e) => Err(e.into()), }, @@ -1320,15 +1346,11 @@ impl RustyPipeQuery { async fn yt_request + Debug, M>( &self, request: &Request, - id: &str, - visitor_data: Option<&str>, - deobf: Option<&DeobfData>, + ctx: &MapRespCtx<'_>, ) -> Result, Error> { let mut last_resp = None; for n in 0..=self.client.inner.n_http_retries { - let resp = self - .yt_request_attempt::(request, id, visitor_data, deobf) - .await?; + let resp = self.yt_request_attempt::(request, ctx).await?; let err = match &resp.res { Ok(_) => return Ok(resp), @@ -1394,9 +1416,15 @@ impl RustyPipeQuery { .json(body) .build()?; - let req_res = self - .yt_request::(&request, id, visitor_data, deobf) - .await?; + let ctx = MapRespCtx { + id, + lang: self.opts.lang, + deobf, + visitor_data, + client_type: ctype, + }; + + let req_res = self.yt_request::(&request, &ctx).await?; // Uncomment to debug response text // println!("{}", &req_res.body); @@ -1553,6 +1581,28 @@ impl AsRef for RustyPipeQuery { } } +struct MapRespCtx<'a> { + id: &'a str, + lang: Language, + deobf: Option<&'a DeobfData>, + visitor_data: Option<&'a str>, + client_type: ClientType, +} + +impl<'a> MapRespCtx<'a> { + /// Create a [`MapRespCtx`] for testing + #[cfg(test)] + fn test(id: &'a str) -> Self { + Self { + id, + lang: Language::En, + deobf: None, + visitor_data: None, + client_type: ClientType::Desktop, + } + } +} + /// Implement this for YouTube API response structs that need to be mapped to /// RustyPipe models. trait MapResponse { @@ -1569,13 +1619,7 @@ trait MapResponse { /// - `lang`: Language of the request. Used for mapping localized information like dates. /// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method) /// - `visitor_data`: Visitor data option of the client - fn map_response( - self, - id: &str, - lang: Language, - deobf: Option<&DeobfData>, - visitor_data: Option<&str>, - ) -> Result, ExtractionError>; + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError>; } fn validate_country(country: Country) -> Country { diff --git a/src/client/music_artist.rs b/src/client/music_artist.rs index bd1ca76..9009644 100644 --- a/src/client/music_artist.rs +++ b/src/client/music_artist.rs @@ -14,7 +14,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::PageType}, - ClientType, MapResponse, QBrowse, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, }; impl RustyPipeQuery { @@ -92,14 +92,8 @@ impl RustyPipeQuery { } impl MapResponse for response::MusicArtist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { - let mapped = map_artist_page(self, id, lang, false)?; + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { + let mapped = map_artist_page(self, ctx, false)?; Ok(MapResult { c: mapped.c.0, warnings: mapped.warnings, @@ -110,19 +104,15 @@ impl MapResponse for response::MusicArtist { impl MapResponse<(MusicArtist, bool)> for response::MusicArtist { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - map_artist_page(self, id, lang, true) + map_artist_page(self, ctx, true) } } fn map_artist_page( res: response::MusicArtist, - id: &str, - lang: crate::param::Language, + ctx: &MapRespCtx<'_>, skip_extendables: bool, ) -> Result, ExtractionError> { // dbg!(&res); @@ -138,7 +128,7 @@ fn map_artist_page( .and_then(|pb| util::string_from_pb(pb, 3)); if let Some(share_channel_id) = share_channel_id { - if share_channel_id != id { + if share_channel_id != ctx.id { return Err(ExtractionError::Redirect(share_channel_id)); } } @@ -155,9 +145,9 @@ fn map_artist_page( .unwrap_or_default(); let mut mapper = MusicListMapper::with_artist( - lang, + ctx.lang, ArtistId { - id: Some(id.to_owned()), + id: Some(ctx.id.to_owned()), name: header.title.clone(), }, ); @@ -264,7 +254,7 @@ fn map_artist_page( Ok(MapResult { c: ( MusicArtist { - id: id.to_owned(), + id: ctx.id.to_owned(), name: header.title, header_image: header.thumbnail.into(), description: header.description, @@ -272,7 +262,7 @@ fn map_artist_page( subscriber_count: header.subscription_button.and_then(|btn| { util::parse_large_numstr_or_warn( &btn.subscribe_button_renderer.subscriber_count_text, - lang, + ctx.lang, &mut mapped.warnings, ) }), @@ -293,16 +283,13 @@ fn map_artist_page( impl MapResponse> for response::MusicArtistAlbums { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { // dbg!(&self); let Some(header) = self.header else { return Err(ExtractionError::NotFound { - id: id.into(), + id: ctx.id.into(), msg: "no header".into(), }); }; @@ -320,9 +307,9 @@ impl MapResponse> for response::MusicArtistAlbums { .contents; let mut mapper = MusicListMapper::with_artist( - lang, + ctx.lang, ArtistId { - id: Some(id.to_owned()), + id: Some(ctx.id.to_owned()), name: header.music_header_renderer.title, }, ); @@ -347,7 +334,7 @@ mod tests { use path_macro::path; use rstest::rstest; - use crate::{param::Language, util::tests::TESTFILES}; + use crate::util::tests::TESTFILES; use super::*; @@ -369,7 +356,7 @@ mod tests { let resp: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult<(MusicArtist, bool)> = - resp.map_response(id, Language::En, None, None).unwrap(); + resp.map_response(&MapRespCtx::test(id)).unwrap(); let (mut artist, can_fetch_more) = map_res.c; assert!( @@ -384,7 +371,7 @@ mod tests { let resp: response::MusicArtistAlbums = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let mut map_res: MapResult> = - resp.map_response(id, Language::En, None, None).unwrap(); + resp.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -405,7 +392,7 @@ mod tests { let artist: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = artist - .map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None) + .map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ")) .unwrap(); assert!( @@ -424,7 +411,7 @@ mod tests { let artist: response::MusicArtist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let res: Result, ExtractionError> = - artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None); + artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ")); let e = res.unwrap_err(); match e { diff --git a/src/client/music_charts.rs b/src/client/music_charts.rs index 27ac005..1075913 100644 --- a/src/client/music_charts.rs +++ b/src/client/music_charts.rs @@ -11,7 +11,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType}, - ClientType, MapResponse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -56,13 +56,7 @@ impl RustyPipeQuery { } impl MapResponse for response::MusicCharts { - fn map_response( - self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, crate::error::ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let countries = self .framework_updates .map(|fwu| { @@ -77,9 +71,9 @@ impl MapResponse for response::MusicCharts { let mut top_playlist_id = None; let mut trending_playlist_id = None; - let mut mapper_top = MusicListMapper::new(lang); - let mut mapper_trending = MusicListMapper::new(lang); - let mut mapper_other = MusicListMapper::new(lang); + let mut mapper_top = MusicListMapper::new(ctx.lang); + let mut mapper_trending = MusicListMapper::new(ctx.lang); + let mut mapper_other = MusicListMapper::new(ctx.lang); self.contents .single_column_browse_results_renderer @@ -151,7 +145,6 @@ mod tests { use rstest::rstest; use super::*; - use crate::param::Language; #[rstest] #[case::default("global")] @@ -163,8 +156,7 @@ mod tests { let charts: response::MusicCharts = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = - charts.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = charts.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_details.rs b/src/client/music_details.rs index 0459389..919b07f 100644 --- a/src/client/music_details.rs +++ b/src/client/music_details.rs @@ -8,7 +8,6 @@ use crate::{ paginator::{ContinuationEndpoint, Paginator}, ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem, }, - param::Language, serializer::MapResult, }; @@ -17,7 +16,7 @@ use super::{ self, music_item::{map_queue_item, MusicListMapper}, }, - ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -170,10 +169,7 @@ impl RustyPipeQuery { impl MapResponse for response::MusicDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let tabs = self .contents @@ -211,7 +207,7 @@ impl MapResponse for response::MusicDetails { } let content = content.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })?; let track_item = content @@ -225,7 +221,7 @@ impl MapResponse for response::MusicDetails { response::music_item::PlaylistPanelVideo::None => None, }) .ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?; - let mut track = map_queue_item(track_item, lang); + let mut track = map_queue_item(track_item, ctx.lang); let mut warnings = content.contents.warnings; warnings.append(&mut track.warnings); @@ -244,10 +240,7 @@ impl MapResponse for response::MusicDetails { impl MapResponse> for response::MusicDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let tabs = self .contents @@ -260,7 +253,7 @@ impl MapResponse> for response::MusicDetails { .into_iter() .find_map(|t| t.tab_renderer.content) .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })? .music_queue_renderer @@ -275,7 +268,7 @@ impl MapResponse> for response::MusicDetails { .into_iter() .filter_map(|item| match item { response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => { - let mut track = map_queue_item(item, lang); + let mut track = map_queue_item(item, ctx.lang); warnings.append(&mut track.warnings); Some(track.c) } @@ -297,18 +290,12 @@ impl MapResponse> for response::MusicDetails { } impl MapResponse for response::MusicLyrics { - fn map_response( - self, - id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let lyrics = self .contents .into_res() .map_err(|msg| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: msg.into(), })? .into_iter() @@ -328,16 +315,13 @@ impl MapResponse for response::MusicLyrics { impl MapResponse for response::MusicRelated { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let contents = self .contents .into_res() .map_err(|msg| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: msg.into(), })?; @@ -362,10 +346,10 @@ impl MapResponse for response::MusicRelated { _ => None, }); - let mut mapper_tracks = MusicListMapper::new(lang); + let mut mapper_tracks = MusicListMapper::new(ctx.lang); let mut mapper = match artist_id { - Some(artist_id) => MusicListMapper::with_artist(lang, artist_id), - None => MusicListMapper::new(lang), + Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id), + None => MusicListMapper::new(ctx.lang), }; let mut sections = contents.into_iter(); @@ -412,7 +396,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[rstest] #[case::mv("mv", "ZeerrnuLi5E")] @@ -424,7 +408,7 @@ mod tests { let details: response::MusicDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - details.map_response(id, Language::En, None, None).unwrap(); + details.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -444,7 +428,7 @@ mod tests { let radio: response::MusicDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - radio.map_response(id, Language::En, None, None).unwrap(); + radio.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -461,7 +445,7 @@ mod tests { let lyrics: response::MusicLyrics = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = lyrics.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = lyrics.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -478,8 +462,7 @@ mod tests { let lyrics: response::MusicRelated = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = - lyrics.map_response("", Language::En, None, None).unwrap(); + let map_res: MapResult = lyrics.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_genres.rs b/src/client/music_genres.rs index 7fc2511..16f3b53 100644 --- a/src/client/music_genres.rs +++ b/src/client/music_genres.rs @@ -8,7 +8,7 @@ use crate::{ use super::{ response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint}, - ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, }; impl RustyPipeQuery { @@ -59,11 +59,8 @@ impl RustyPipeQuery { impl MapResponse> for response::MusicGenres { fn map_response( self, - _id: &str, - _lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result>, ExtractionError> { + _ctx: &MapRespCtx<'_>, + ) -> Result>, ExtractionError> { let content = self .contents .single_column_browse_results_renderer @@ -111,13 +108,7 @@ impl MapResponse> for response::MusicGenres { } impl MapResponse for response::MusicGenre { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // dbg!(&self); let content = self @@ -179,7 +170,7 @@ impl MapResponse for response::MusicGenre { _ => return None, }; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(items); let mut mapped = mapper.conv_items(); warnings.append(&mut mapped.warnings); @@ -194,7 +185,7 @@ impl MapResponse for response::MusicGenre { Ok(MapResult { c: MusicGenre { - id: id.to_owned(), + id: ctx.id.to_owned(), name: self.header.music_header_renderer.title, sections, }, @@ -211,7 +202,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[test] fn map_music_genres() { @@ -221,7 +212,7 @@ mod tests { let playlist: response::MusicGenres = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - playlist.map_response("", Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -241,7 +232,7 @@ mod tests { let playlist: response::MusicGenre = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_new.rs b/src/client/music_new.rs index c5cfc5f..68251e6 100644 --- a/src/client/music_new.rs +++ b/src/client/music_new.rs @@ -4,9 +4,10 @@ use crate::{ client::response::music_item::MusicListMapper, error::{Error, ExtractionError}, model::{traits::FromYtItem, AlbumItem, TrackItem}, + serializer::MapResult, }; -use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery}; impl RustyPipeQuery { /// Get the new albums that were released on YouTube Music @@ -49,13 +50,7 @@ impl RustyPipeQuery { } impl MapResponse> for response::MusicNew { - fn map_response( - self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result>, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result>, ExtractionError> { let items = self .contents .single_column_browse_results_renderer @@ -73,7 +68,7 @@ impl MapResponse> for response::MusicNew { .grid_renderer .items; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(items); Ok(mapper.conv_items()) @@ -88,7 +83,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES}; + use crate::{serializer::MapResult, util::tests::TESTFILES}; #[rstest] #[case::default("default")] @@ -98,9 +93,8 @@ mod tests { let new_albums: response::MusicNew = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = new_albums - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + new_albums.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -119,9 +113,8 @@ mod tests { let new_videos: response::MusicNew = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = new_videos - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + new_videos.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_playlist.rs b/src/client/music_playlist.rs index 8c2d29f..8261b2b 100644 --- a/src/client/music_playlist.rs +++ b/src/client/music_playlist.rs @@ -17,7 +17,7 @@ use super::{ self, music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper}, }, - ClientType, MapResponse, QBrowse, RustyPipeQuery, + ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, }; impl RustyPipeQuery { @@ -138,10 +138,7 @@ impl RustyPipeQuery { impl MapResponse for response::MusicPlaylist { fn map_response( self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { // dbg!(&self); @@ -186,14 +183,15 @@ impl MapResponse for response::MusicPlaylist { )))?; if let Some(playlist_id) = shelf.playlist_id { - if playlist_id != id { + if playlist_id != ctx.id { return Err(ExtractionError::WrongResult(format!( - "got wrong playlist id {playlist_id}, expected {id}" + "got wrong playlist id {}, expected {}", + playlist_id, ctx.id ))); } } - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); mapper.map_response(shelf.contents); let map_res = mapper.conv_items(); @@ -273,7 +271,7 @@ impl MapResponse for response::MusicPlaylist { Ok(MapResult { c: MusicPlaylist { - id: id.to_owned(), + id: ctx.id.to_owned(), name, thumbnail, channel, @@ -284,14 +282,14 @@ impl MapResponse for response::MusicPlaylist { track_count, map_res.c, ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicBrowse, ), related_playlists: Paginator::new_ext( None, Vec::new(), related_ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicBrowse, ), }, @@ -301,13 +299,7 @@ impl MapResponse for response::MusicPlaylist { } impl MapResponse for response::MusicPlaylist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { // dbg!(&self); let (header, sections) = match self.contents { @@ -401,7 +393,7 @@ impl MapResponse for response::MusicPlaylist { .map(|part| part.to_string()) .unwrap_or_default(); - let album_type = map_album_type(album_type_txt.as_str(), lang); + let album_type = map_album_type(album_type_txt.as_str(), ctx.lang); let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok()); fn map_playlist_id(ep: &NavigationEndpoint) -> Option { @@ -448,11 +440,11 @@ impl MapResponse for response::MusicPlaylist { let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone())); let mut mapper = MusicListMapper::with_album( - lang, + ctx.lang, artists.clone(), by_va, AlbumId { - id: id.to_owned(), + id: ctx.id.to_owned(), name: header.title.clone(), }, ); @@ -460,7 +452,7 @@ impl MapResponse for response::MusicPlaylist { let tracks_res = mapper.conv_items(); let mut warnings = tracks_res.warnings; - let mut variants_mapper = MusicListMapper::new(lang); + let mut variants_mapper = MusicListMapper::new(ctx.lang); if let Some(res) = album_variants { variants_mapper.map_response(res); } @@ -469,7 +461,7 @@ impl MapResponse for response::MusicPlaylist { Ok(MapResult { c: MusicAlbum { - id: id.to_owned(), + id: ctx.id.to_owned(), playlist_id, name: header.title, cover: header.thumbnail.into(), @@ -497,7 +489,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{model, param::Language, util::tests::TESTFILES}; + use crate::{model, util::tests::TESTFILES}; #[rstest] #[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")] @@ -512,7 +504,7 @@ mod tests { let playlist: response::MusicPlaylist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -539,7 +531,7 @@ mod tests { let playlist: response::MusicPlaylist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult = - playlist.map_response(id, Language::En, None, None).unwrap(); + playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/music_search.rs b/src/client/music_search.rs index 2216171..f443229 100644 --- a/src/client/music_search.rs +++ b/src/client/music_search.rs @@ -15,7 +15,7 @@ use crate::{ serializer::MapResult, }; -use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext}; +use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -152,10 +152,7 @@ impl RustyPipeQuery { impl MapResponse> for response::MusicSearch { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { // dbg!(&self); @@ -171,7 +168,7 @@ impl MapResponse> for response::MusicSearch let mut corrected_query = None; let mut ctoken = None; - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); sections.into_iter().for_each(|section| match section { response::music_search::ItemSection::MusicShelfRenderer(shelf) => { @@ -199,7 +196,7 @@ impl MapResponse> for response::MusicSearch None, map_res.c, ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::MusicSearch, ), corrected_query, @@ -212,12 +209,9 @@ impl MapResponse> for response::MusicSearch impl MapResponse for response::MusicSearchSuggestion { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - let mut mapper = MusicListMapper::new_search_suggest(lang); + let mut mapper = MusicListMapper::new_search_suggest(ctx.lang); let mut terms = Vec::new(); for section in self.contents { @@ -256,12 +250,11 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{ AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult, MusicSearchSuggestion, TrackItem, }, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -278,7 +271,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -301,7 +294,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -320,7 +313,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -339,7 +332,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -360,7 +353,7 @@ mod tests { let search: response::MusicSearch = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -380,9 +373,8 @@ mod tests { let suggestion: response::MusicSearchSuggestion = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult = suggestion - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult = + suggestion.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/pagination.rs b/src/client/pagination.rs index 20ce792..94c44e8 100644 --- a/src/client/pagination.rs +++ b/src/client/pagination.rs @@ -10,7 +10,7 @@ use crate::model::{ use crate::serializer::MapResult; use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo}; -use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery}; impl RustyPipeQuery { /// Get more YouTube items from the given continuation token and endpoint @@ -103,10 +103,7 @@ fn map_ytm_paginator( impl MapResponse> for response::Continuation { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .on_response_received_actions @@ -126,7 +123,7 @@ impl MapResponse> for response::Continuation { }) .unwrap_or_default(); - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -139,12 +136,9 @@ impl MapResponse> for response::Continuation { impl MapResponse> for response::MusicContinuation { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { - let mut mapper = MusicListMapper::new(lang); + let mut mapper = MusicListMapper::new(ctx.lang); let mut continuations = Vec::new(); match self.continuation_contents { @@ -173,7 +167,7 @@ impl MapResponse> for response::MusicContinuation { mapper.add_warnings(&mut panel.contents.warnings); panel.contents.c.into_iter().for_each(|item| { if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item { - let mut track = map_queue_item(item, lang); + let mut track = map_queue_item(item, ctx.lang); mapper.add_item(MusicItem::Track(track.c)); mapper.add_warnings(&mut track.warnings); } @@ -356,7 +350,6 @@ mod tests { use super::*; use crate::{ model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem}, - param::Language, util::tests::TESTFILES, }; @@ -371,7 +364,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -393,7 +386,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); @@ -416,7 +409,7 @@ mod tests { let items: response::Continuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse); @@ -439,7 +432,7 @@ mod tests { let items: response::MusicContinuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); @@ -460,7 +453,7 @@ mod tests { let items: response::MusicContinuation = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - items.map_response("", Language::En, None, None).unwrap(); + items.map_response(&MapRespCtx::test("")).unwrap(); let paginator: Paginator = map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse); diff --git a/src/client/player.rs b/src/client/player.rs index 03ac1e8..8cccc97 100644 --- a/src/client/player.rs +++ b/src/client/player.rs @@ -16,13 +16,15 @@ use crate::{ traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset, Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, }, - param::Language, util, }; use super::{ - response::{self, player}, - ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext, + response::{ + self, + player::{self, Format}, + }, + ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -62,33 +64,52 @@ struct QContentPlaybackContext<'a> { impl RustyPipeQuery { /// Get YouTube player data (video/audio streams + basic metadata) - #[tracing::instrument(skip(self))] pub async fn player + Debug>(&self, video_id: S) -> Result { + self.player_from_clients(video_id, &[ClientType::Desktop, ClientType::TvHtml5Embed]) + .await + } + + /// Get YouTube player data (video/audio streams + basic metadata) using a list of clients. + /// + /// The clients are used in the given order. If a client cannot fetch the requested video, + /// an attempt is made with the next one. + pub async fn player_from_clients + Debug>( + &self, + video_id: S, + clients: &[ClientType], + ) -> Result { let video_id = video_id.as_ref(); - let desktop_res = self.player_from_client(video_id, ClientType::Desktop).await; + let mut last_e = Error::Other("no clients".into()); + // Prefer to output age restriction error (e.g. if video cannot be played + // by Desktop because of age restriction and by TvHtml5Embed because it is non-embeddable) + let mut age_restricted_e = None; - match desktop_res { - Ok(res) => Ok(res), - Err(Error::Extraction(e)) => { - if e.switch_client() { - let tv_res = self - .player_from_client(video_id, ClientType::TvHtml5Embed) - .await; - - match tv_res { - // Output desktop client error if the tv client is unsupported - Err(Error::Extraction(ExtractionError::Unavailable { - reason: UnavailabilityReason::UnsupportedClient, - .. - })) => Err(Error::Extraction(e)), - _ => tv_res, + for client in clients { + let res = self.player_from_client(video_id, *client).await; + match res { + Ok(res) => return Ok(res), + Err(Error::Extraction(e)) => { + if e.switch_client() { + if let ExtractionError::Unavailable { + reason: UnavailabilityReason::AgeRestricted, + msg, + } = &e + { + age_restricted_e = + Some(Error::Extraction(ExtractionError::Unavailable { + reason: UnavailabilityReason::AgeRestricted, + msg: msg.to_owned(), + })); + } + last_e = Error::Extraction(e); + } else { + return Err(Error::Extraction(e)); } - } else { - Err(Error::Extraction(e)) } + Err(e) => return Err(e), } - Err(e) => Err(e), } + Err(age_restricted_e.unwrap_or(last_e)) } /// Get YouTube player data (video/audio streams + basic metadata) using the specified client @@ -149,12 +170,8 @@ impl RustyPipeQuery { impl MapResponse for response::Player { fn map_response( self, - id: &str, - _lang: Language, - deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { - let deobf = Deobfuscator::new(deobf.unwrap())?; let mut warnings = vec![]; // Check playability status @@ -224,7 +241,7 @@ impl MapResponse for response::Player { } }; - let mut streaming_data = + let streaming_data = self.streaming_data .ok_or(ExtractionError::InvalidData(Cow::Borrowed( "no streaming data", @@ -235,10 +252,10 @@ impl MapResponse for response::Player { "no video details", )))?; - if video_details.video_id != id { + if video_details.video_id != ctx.id { return Err(ExtractionError::WrongResult(format!( "video id {}, expected {}", - video_details.video_id, id + video_details.video_id, ctx.id ))); } @@ -258,54 +275,16 @@ impl MapResponse for response::Player { is_live_content: video_details.is_live_content, }; - let mut formats = streaming_data.formats.c; - formats.append(&mut streaming_data.adaptive_formats.c); - - let mut video_streams: Vec = Vec::new(); - let mut video_only_streams: Vec = Vec::new(); - let mut audio_streams: Vec = Vec::new(); - - if !is_live { - let mut last_nsig: [String; 2] = [String::new(), String::new()]; - - warnings.append(&mut streaming_data.formats.warnings); - warnings.append(&mut streaming_data.adaptive_formats.warnings); - - for f in formats { - if f.format_type == player::FormatType::FormatStreamTypeOtf { - continue; - } - - match (f.is_video(), f.is_audio()) { - (true, true) => { - let mut map_res = map_video_stream(f, &deobf, &mut last_nsig); - warnings.append(&mut map_res.warnings); - if let Some(c) = map_res.c { - video_streams.push(c); - }; - } - (true, false) => { - let mut map_res = map_video_stream(f, &deobf, &mut last_nsig); - warnings.append(&mut map_res.warnings); - if let Some(c) = map_res.c { - video_only_streams.push(c); - }; - } - (false, true) => { - let mut map_res = map_audio_stream(f, &deobf, &mut last_nsig); - warnings.append(&mut map_res.warnings); - if let Some(c) = map_res.c { - audio_streams.push(c); - }; - } - (false, false) => warnings.push(format!("invalid stream: itag {}", f.itag)), - } - } - } - - video_streams.sort_by(QualityOrd::quality_cmp); - video_only_streams.sort_by(QualityOrd::quality_cmp); - audio_streams.sort_by(QualityOrd::quality_cmp); + let streams = if !is_live { + let mut mapper = StreamsMapper::new(Deobfuscator::new(ctx.deobf.unwrap())?); + mapper.map_streams(streaming_data.formats); + mapper.map_streams(streaming_data.adaptive_formats); + let mut res = mapper.output()?; + warnings.append(&mut res.warnings); + res.c + } else { + Streams::default() + }; let subtitles = self.captions.map_or(Vec::new(), |captions| { captions @@ -374,235 +353,309 @@ impl MapResponse for response::Player { Ok(MapResult { c: VideoPlayer { details: video_info, - video_streams, - video_only_streams, - audio_streams, + video_streams: streams.video_streams, + video_only_streams: streams.video_only_streams, + audio_streams: streams.audio_streams, subtitles, expires_in_seconds: streaming_data.expires_in_seconds, hls_manifest_url: streaming_data.hls_manifest_url, dash_manifest_url: streaming_data.dash_manifest_url, preview_frames, + client_type: ctx.client_type, visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings, }) } } -fn cipher_to_url_params( - signature_cipher: &str, - deobf: &Deobfuscator, -) -> Result<(Url, BTreeMap), DeobfError> { - let params: HashMap, Cow> = - url::form_urlencoded::parse(signature_cipher.as_bytes()).collect(); - - // Parameters: - // `s`: Obfuscated signature - // `sp`: Signature parameter - // `url`: URL that is missing the signature parameter - - let sig = params.get("s").ok_or(DeobfError::Extraction("s param"))?; - let sp = params.get("sp").ok_or(DeobfError::Extraction("sp param"))?; - let raw_url = params - .get("url") - .ok_or(DeobfError::Extraction("no url param"))?; - let (url_base, mut url_params) = - util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?; - - let deobf_sig = deobf.deobfuscate_sig(sig)?; - url_params.insert(sp.to_string(), deobf_sig); - - Ok((url_base, url_params)) +struct StreamsMapper { + deobf: Deobfuscator, + streams: Streams, + warnings: Vec, + /// First stream mapping error + first_err: Option, + /// Last obfuscated nsig parameter (cache) + last_nsig: String, + /// Last deobfuscated nsig parameter + last_nsig_deobf: String, } -fn deobf_nsig( - url_params: &mut BTreeMap, - deobf: &Deobfuscator, - last_nsig: &mut [String; 2], -) -> Result<(), DeobfError> { - let nsig: String; - if let Some(n) = url_params.get("n") { - nsig = if n == &last_nsig[0] { - last_nsig[1].clone() - } else { - let nsig = deobf.deobfuscate_nsig(n)?; - last_nsig[0] = n.to_string(); - last_nsig[1].clone_from(&nsig); - nsig +#[derive(Default)] +struct Streams { + video_streams: Vec, + video_only_streams: Vec, + audio_streams: Vec, +} + +impl StreamsMapper { + fn new(deobf: Deobfuscator) -> Self { + Self { + deobf, + streams: Streams::default(), + warnings: Vec::new(), + first_err: None, + last_nsig: String::new(), + last_nsig_deobf: String::new(), + } + } + + fn map_streams(&mut self, mut streams: MapResult>) { + self.warnings.append(&mut streams.warnings); + + let map_e = |m: &mut Self, e: ExtractionError| { + m.warnings.push(e.to_string()); + if m.first_err.is_none() { + m.first_err = Some(e); + } }; - url_params.insert("n".to_owned(), nsig); - }; - Ok(()) + for f in streams.c { + if f.format_type == player::FormatType::FormatStreamTypeOtf { + continue; + } + + match (f.is_video(), f.is_audio()) { + (true, true) => match self.map_video_stream(f) { + Ok(c) => self.streams.video_streams.push(c), + Err(e) => map_e(self, e), + }, + (true, false) => match self.map_video_stream(f) { + Ok(c) => self.streams.video_only_streams.push(c), + Err(e) => map_e(self, e), + }, + (false, true) => match self.map_audio_stream(f) { + Ok(c) => self.streams.audio_streams.push(c), + Err(e) => map_e(self, e), + }, + (false, false) => self + .warnings + .push(format!("invalid stream: itag {}", f.itag)), + } + } + } + + fn output(mut self) -> Result, ExtractionError> { + // If we did not extract any streams and there were mapping errors, fail with the first error + if self.streams.video_streams.is_empty() + && (self.streams.video_only_streams.is_empty() || self.streams.audio_streams.is_empty()) + { + if let Some(e) = self.first_err { + return Err(e); + } + } + + self.streams.video_streams.sort_by(QualityOrd::quality_cmp); + self.streams + .video_only_streams + .sort_by(QualityOrd::quality_cmp); + self.streams.audio_streams.sort_by(QualityOrd::quality_cmp); + + Ok(MapResult { + c: self.streams, + warnings: self.warnings, + }) + } + + fn cipher_to_url_params( + &self, + signature_cipher: &str, + ) -> Result<(Url, BTreeMap), DeobfError> { + let params: HashMap, Cow> = + url::form_urlencoded::parse(signature_cipher.as_bytes()).collect(); + + // Parameters: + // `s`: Obfuscated signature + // `sp`: Signature parameter + // `url`: URL that is missing the signature parameter + + let sig = params.get("s").ok_or(DeobfError::Extraction("s param"))?; + let sp = params.get("sp").ok_or(DeobfError::Extraction("sp param"))?; + let raw_url = params + .get("url") + .ok_or(DeobfError::Extraction("no url param"))?; + let (url_base, mut url_params) = + util::url_to_params(raw_url).or(Err(DeobfError::Extraction("url params")))?; + + let deobf_sig = self.deobf.deobfuscate_sig(sig)?; + url_params.insert(sp.to_string(), deobf_sig); + + Ok((url_base, url_params)) + } + + fn deobf_nsig(&mut self, url_params: &mut BTreeMap) -> Result<(), DeobfError> { + if let Some(n) = url_params.get("n") { + let nsig = if n == &self.last_nsig { + self.last_nsig_deobf.to_owned() + } else { + let nsig = self.deobf.deobfuscate_nsig(n)?; + self.last_nsig.clone_from(n); + self.last_nsig_deobf.clone_from(&nsig); + nsig + }; + + url_params.insert("n".to_owned(), nsig); + }; + Ok(()) + } + + fn map_url( + &mut self, + url: &Option, + signature_cipher: &Option, + ) -> Result { + let (url_base, mut url_params) = + match url { + Some(url) => util::url_to_params(url).map_err(|_| { + ExtractionError::InvalidData(format!("Could not parse url `{url}`").into()) + }), + None => match signature_cipher { + Some(signature_cipher) => { + self.cipher_to_url_params(signature_cipher).map_err(|e| { + ExtractionError::InvalidData( + format!("Could not deobfuscate signatureCipher `{signature_cipher}`: {e}") + .into(), + ) + }) + } + None => Err(ExtractionError::InvalidData( + "stream contained neither url or cipher".into(), + )), + }, + }?; + + self.deobf_nsig(&mut url_params)?; + let url = Url::parse_with_params(url_base.as_str(), url_params.iter()) + .map_err(|_| ExtractionError::InvalidData("could not combine URL".into()))?; + + Ok(UrlMapRes { + url: url.to_string(), + xtags: url_params.get("xtags").cloned(), + }) + } + + fn map_video_stream(&mut self, f: player::Format) -> Result { + let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { + return Err(ExtractionError::InvalidData( + format!( + "Invalid mime type `{}` in video format {:?}", + &f.mime_type, &f + ) + .into(), + )); + }; + let Some(format) = get_video_format(mtype) else { + return Err(ExtractionError::InvalidData( + format!("invalid video format. itag: {}", f.itag).into(), + )); + }; + let map_res = self.map_url(&f.url, &f.signature_cipher)?; + + Ok(VideoStream { + url: map_res.url, + itag: f.itag, + bitrate: f.bitrate, + average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), + size: f.content_length, + index_range: f.index_range, + init_range: f.init_range, + duration_ms: f.approx_duration_ms, + // Note that the format has already been verified using + // is_video(), so these unwraps are safe + width: f.width.unwrap(), + height: f.height.unwrap(), + fps: f.fps.unwrap(), + quality: f.quality_label.unwrap(), + hdr: f.color_info.unwrap_or_default().primaries + == player::Primaries::ColorPrimariesBt2020, + format, + codec: get_video_codec(codecs), + mime: f.mime_type, + }) + } + + fn map_audio_stream(&mut self, f: player::Format) -> Result { + let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { + return Err(ExtractionError::InvalidData( + format!( + "Invalid mime type `{}` in video format {:?}", + &f.mime_type, &f + ) + .into(), + )); + }; + let format = get_audio_format(mtype).ok_or_else(|| { + ExtractionError::InvalidData(format!("invalid audio format. itag: {}", f.itag).into()) + })?; + let map_res = self.map_url(&f.url, &f.signature_cipher)?; + + Ok(AudioStream { + url: map_res.url, + itag: f.itag, + bitrate: f.bitrate, + average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), + size: f.content_length.unwrap(), + index_range: f.index_range, + init_range: f.init_range, + duration_ms: f.approx_duration_ms, + format, + codec: get_audio_codec(codecs), + mime: f.mime_type, + channels: f.audio_channels, + loudness_db: f.loudness_db, + track: f + .audio_track + .map(|t| self.map_audio_track(t, map_res.xtags)), + }) + } + + fn map_audio_track( + &mut self, + track: response::player::AudioTrack, + xtags: Option, + ) -> AudioTrack { + let mut lang = None; + let mut track_type = None; + + if let Some(xtags) = xtags { + xtags + .split(':') + .filter_map(|param| param.split_once('=')) + .for_each(|(k, v)| match k { + "lang" => { + lang = Some(v.to_owned()); + } + "acont" => match serde_plain::from_str(v) { + Ok(v) => { + track_type = Some(v); + } + Err(_) => { + self.warnings + .push(format!("could not parse audio track type `{v}`")); + } + }, + _ => {} + }); + } + + AudioTrack { + id: track.id, + lang, + lang_name: track.display_name, + is_default: track.audio_is_default, + track_type, + } + } } struct UrlMapRes { url: String, - throttled: bool, xtags: Option, } -fn map_url( - url: &Option, - signature_cipher: &Option, - deobf: &Deobfuscator, - last_nsig: &mut [String; 2], -) -> MapResult> { - let x = match url { - Some(url) => util::url_to_params(url).map_err(|_| format!("Could not parse url `{url}`")), - None => match signature_cipher { - Some(signature_cipher) => cipher_to_url_params(signature_cipher, deobf).map_err(|e| { - format!("Could not deobfuscate signatureCipher `{signature_cipher}`: {e}") - }), - None => Err("stream contained neither url or cipher".to_owned()), - }, - }; - - let (url_base, mut url_params) = match x { - Ok(x) => x, - Err(e) => { - return MapResult { - c: None, - warnings: vec![e], - } - } - }; - - let mut warnings = vec![]; - let mut throttled = false; - deobf_nsig(&mut url_params, deobf, last_nsig).unwrap_or_else(|e| { - warnings.push(format!( - "Could not deobfuscate nsig (params: {url_params:?}): {e}" - )); - throttled = true; - }); - - match Url::parse_with_params(url_base.as_str(), url_params.iter()) { - Ok(url) => MapResult { - c: Some(UrlMapRes { - url: url.to_string(), - throttled, - xtags: url_params.get("xtags").cloned(), - }), - warnings, - }, - Err(_) => MapResult { - c: None, - warnings: vec![format!( - "url could not be joined. url: `{url_base}` params: {url_params:?}" - )], - }, - } -} - -fn map_video_stream( - f: player::Format, - deobf: &Deobfuscator, - last_nsig: &mut [String; 2], -) -> MapResult> { - let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { - return MapResult { - c: None, - warnings: vec![format!( - "Invalid mime type `{}` in video format {:?}", - &f.mime_type, &f - )], - }; - }; - let Some(format) = get_video_format(mtype) else { - return MapResult { - c: None, - warnings: vec![format!("invalid video format. itag: {}", f.itag)], - }; - }; - let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig); - - match map_res.c { - Some(url) => MapResult { - c: Some(VideoStream { - url: url.url, - itag: f.itag, - bitrate: f.bitrate, - average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), - size: f.content_length, - index_range: f.index_range, - init_range: f.init_range, - duration_ms: f.approx_duration_ms, - // Note that the format has already been verified using - // is_video(), so these unwraps are safe - width: f.width.unwrap(), - height: f.height.unwrap(), - fps: f.fps.unwrap(), - quality: f.quality_label.unwrap(), - hdr: f.color_info.unwrap_or_default().primaries - == player::Primaries::ColorPrimariesBt2020, - format, - codec: get_video_codec(codecs), - mime: f.mime_type, - throttled: url.throttled, - }), - warnings: map_res.warnings, - }, - None => MapResult { - c: None, - warnings: map_res.warnings, - }, - } -} - -fn map_audio_stream( - f: player::Format, - deobf: &Deobfuscator, - last_nsig: &mut [String; 2], -) -> MapResult> { - let Some((mtype, codecs)) = parse_mime(&f.mime_type) else { - return MapResult { - c: None, - warnings: vec![format!( - "Invalid mime type `{}` in video format {:?}", - &f.mime_type, &f - )], - }; - }; - let Some(format) = get_audio_format(mtype) else { - return MapResult { - c: None, - warnings: vec![format!("invalid audio format. itag: {}", f.itag)], - }; - }; - let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig); - let mut warnings = map_res.warnings; - - match map_res.c { - Some(url) => MapResult { - c: Some(AudioStream { - url: url.url, - itag: f.itag, - bitrate: f.bitrate, - average_bitrate: f.average_bitrate.unwrap_or(f.bitrate), - size: f.content_length.unwrap(), - index_range: f.index_range, - init_range: f.init_range, - duration_ms: f.approx_duration_ms, - format, - codec: get_audio_codec(codecs), - mime: f.mime_type, - channels: f.audio_channels, - loudness_db: f.loudness_db, - throttled: url.throttled, - track: f - .audio_track - .map(|t| map_audio_track(t, url.xtags, &mut warnings)), - }), - warnings, - }, - None => MapResult { c: None, warnings }, - } -} - fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> { static PATTERN: Lazy = Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap()); @@ -662,43 +715,6 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec { AudioCodec::Unknown } -fn map_audio_track( - track: response::player::AudioTrack, - xtags: Option, - warnings: &mut Vec, -) -> AudioTrack { - let mut lang = None; - let mut track_type = None; - - if let Some(xtags) = xtags { - xtags - .split(':') - .filter_map(|param| param.split_once('=')) - .for_each(|(k, v)| match k { - "lang" => { - lang = Some(v.to_owned()); - } - "acont" => match serde_plain::from_str(v) { - Ok(v) => { - track_type = Some(v); - } - Err(_) => { - warnings.push(format!("could not parse audio track type `{v}`")); - } - }, - _ => {} - }); - } - - AudioTrack { - id: track.id, - lang, - lang_name: track.display_name, - is_default: track.audio_is_default, - track_type, - } -} - #[cfg(test)] mod tests { use std::{fs::File, io::BufReader}; @@ -707,7 +723,7 @@ mod tests { use rstest::rstest; use super::*; - use crate::{deobfuscate::DeobfData, util::tests::TESTFILES}; + use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES}; static DEOBF_DATA: Lazy = Lazy::new(|| { DeobfData { @@ -719,18 +735,27 @@ mod tests { }); #[rstest] - #[case::desktop("desktop")] - #[case::desktop_music("desktopmusic")] - #[case::tv_html5_embed("tvhtml5embed")] - #[case::android("android")] - #[case::ios("ios")] - fn map_player_data(#[case] name: &str) { + #[case::desktop(ClientType::Desktop)] + #[case::desktop_music(ClientType::DesktopMusic)] + #[case::tv_html5_embed(ClientType::TvHtml5Embed)] + #[case::android(ClientType::Android)] + #[case::ios(ClientType::Ios)] + fn map_player_data(#[case] client_type: ClientType) { + let name = serde_plain::to_string(&client_type) + .unwrap() + .replace('_', ""); let json_path = path!(*TESTFILES / "player" / format!("{name}_video.json")); let json_file = File::open(json_path).unwrap(); let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res = resp - .map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA), None) + .map_response(&MapRespCtx { + id: "pPvd8UxmSbQ", + lang: Language::En, + deobf: Some(&DEOBF_DATA), + visitor_data: None, + client_type, + }) .unwrap(); assert!( @@ -755,22 +780,12 @@ mod tests { #[test] fn cipher_to_url() { let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D"; - let mut last_nsig: [String; 2] = [String::new(), String::new()]; - let deobf = Deobfuscator::new(&DEOBF_DATA).unwrap(); - let map_res = map_url( - &None, - &Some(signature_cipher.to_owned()), - &deobf, - &mut last_nsig, - ); - let url = map_res.c.unwrap(); + let mut mapper = StreamsMapper::new(Deobfuscator::new(&DEOBF_DATA).unwrap()); + let url = mapper + .map_url(&None, &Some(signature_cipher.to_owned())) + .unwrap() + .url; - assert_eq!(url.url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1"); - assert!(!url.throttled); - assert!( - map_res.warnings.is_empty(), - "deserialization/mapping warnings: {:?}", - map_res.warnings - ); + assert_eq!(url, "https://rr5---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=3781277&dur=229.301&ei=vb7nYvH5BMK8gAfBj7ToBQ&expire=1659376413&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2&initcwndbps=1588750&ip=2003%3Ade%3Aaf06%3A6300%3Ac750%3A1b77%3Ac74a%3A80e3&itag=251&keepalive=yes&lmt=1655510291473933&lsig=AG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=hH&mime=audio%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5ednsl&ms=au%2Conr&mt=1659354538&mv=m&mvi=5&n=XzXGSfGusw6OCQ&ns=b_Mq_qlTFcSGlG9RpwpM9xQH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPIsKd7-xi4xVHEC9gb__dU4hzfzsHEj9ytd3nt0gEceAiACJWBcw-wFEq9qir35bwKHJZxtQ9mOL7SKiVkLQNDa6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-Khi831z8dTejFIRCvCEwx_6romtM&txp=4532434&vprv=1"); } } diff --git a/src/client/playlist.rs b/src/client/playlist.rs index ce198f5..ecbf205 100644 --- a/src/client/playlist.rs +++ b/src/client/playlist.rs @@ -13,7 +13,7 @@ use crate::{ util::{self, timeago, TryRemove}, }; -use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery}; +use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery}; impl RustyPipeQuery { /// Get a YouTube playlist @@ -47,15 +47,9 @@ impl RustyPipeQuery { } impl MapResponse for response::Playlist { - fn map_response( - self, - id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let (Some(contents), Some(header)) = (self.contents, self.header) else { - return Err(response::alerts_to_err(id, self.alerts)); + return Err(response::alerts_to_err(ctx.id, self.alerts)); }; let video_items = contents @@ -85,7 +79,7 @@ impl MapResponse for response::Playlist { .playlist_video_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(video_items); let (description, thumbnails, last_update_txt) = match self.sidebar { @@ -144,9 +138,10 @@ impl MapResponse for response::Playlist { }; let playlist_id = header.playlist_header_renderer.playlist_id; - if playlist_id != id { + if playlist_id != ctx.id { return Err(ExtractionError::WrongResult(format!( - "got wrong playlist id {playlist_id}, expected {id}" + "got wrong playlist id {}, expected {}", + playlist_id, ctx.id ))); } @@ -165,7 +160,7 @@ impl MapResponse for response::Playlist { .and_then(|link| ChannelId::try_from(link).ok()); let last_update = last_update_txt.as_ref().and_then(|txt| { - timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings) + timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings) .map(OffsetDateTime::date) }); @@ -177,7 +172,7 @@ impl MapResponse for response::Playlist { Some(n_videos), mapper.items, mapper.ctoken, - vdata.map(str::to_owned), + ctx.visitor_data.map(str::to_owned), ContinuationEndpoint::Browse, ), video_count: n_videos, @@ -189,7 +184,7 @@ impl MapResponse for response::Playlist { visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings: mapper.warnings, }) @@ -203,7 +198,7 @@ mod tests { use path_macro::path; use rstest::rstest; - use crate::{param::Language, util::tests::TESTFILES}; + use crate::util::tests::TESTFILES; use super::*; @@ -218,7 +213,7 @@ mod tests { let playlist: response::Playlist = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = playlist.map_response(id, Language::En, None, None).unwrap(); + let map_res = playlist.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/search.rs b/src/client/search.rs index b99066b..03529f4 100644 --- a/src/client/search.rs +++ b/src/client/search.rs @@ -12,7 +12,7 @@ use crate::{ param::search_filter::SearchFilter, }; -use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext}; +use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -103,10 +103,7 @@ impl RustyPipeQuery { impl MapResponse> for response::Search { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .contents @@ -115,7 +112,7 @@ impl MapResponse> for response::Search { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -135,7 +132,7 @@ impl MapResponse> for response::Search { visitor_data: self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), }, warnings: mapper.warnings, }) @@ -150,9 +147,8 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{SearchResult, YouTubeItem}, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -168,7 +164,7 @@ mod tests { let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap(); let map_res: MapResult> = - search.map_response("", Language::En, None, None).unwrap(); + search.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap index 5b0b4f9..02798cc 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_android.snap @@ -79,7 +79,6 @@ VideoPlayer( mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\"", format: r#3gp, codec: mp4v, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=11439331&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=18&lmt=1580005476071743&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJAH-tWof01vrs8phEoz51XkWwdMzQ77k1UTrdY5XiuTAiA38z-qANX0jtfCiAl4EVMZaKo1ncrzJFRrCffZ6LagrA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -98,7 +97,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, - throttled: false, ), ], video_only_streams: [ @@ -125,7 +123,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2238952&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKCXHOCh_P3VlNWebTeWw0WdSln-zYe3BjZeEm2QiltCAiAQNcJBI4G-8dK5z1IUoqBZctk6ddjkl_QYKRFAKXyOcw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -150,7 +147,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=7808990&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjjrMvCEzSLlbvbrjItT4V9JdpggnO5IHye9i4PxTyzAiAmbaFCB2hH7evf9JX3JUx-tU9S6zv2IzSKz8ObGSVRjw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -175,7 +171,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=4130385&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgBrQhbygTP6RGjUk0lGbxBI5e3NdeR6C_SW8R_ckZ2PkCIQDaBg5cJxYVWfwRrrELQFgRMOJ4xS3oOOROayoQMjxaCA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -200,7 +195,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.01M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=6873325&dur=163.029&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMqBb1hKVVzWl3Awrh1T8GQG9IrSWF84zW_ZfjgbAN5QAiAaP3jYyI4ox2aclcOCzYFzqWgByWCxj_FgTN-SfsARXw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -225,7 +219,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.04M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&dur=163.096&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=22&lmt=1580005750956837&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFlQZgR63Yz9UgY9gVqiyGDVkZmSmACRP3-MmKN7CRzQCIAMHAwZbHmWL1qNH4Nu3A0pXZwErXMVPzMIt-PyxeZqa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -244,7 +237,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=22365208&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgR6KqCOoig_FMl2tWKa7qHSmCjIZa9S7ABzEI16qdO2sCIFXccwql4bqV9CHlqXY4tgxyMFUsp7vW4XUjxs3AyG6H&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -269,7 +261,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.08M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=65400181&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAPjxbuzkozPDc1Nd_0q5X8x8H2SiDvAUFuqqMadtz3SNAiEA_3kXCeePb2kci-WB2779tzI56E6E0iKwoHnUSkKCzwU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -294,7 +285,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=42567727&dur=163.046&ei=q1jpYtOPEYSBgQeHmqbwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AEDMTCojVtwpIKOdhBaxEHE5s322qnAJHGqa2r1F46BM&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFguw-cmBNOQegpyRRzcCScp2WaSnq_o7FB1-AiBgFpICIAGlMj9-kzNCWb3nhpg98Mc239ls6YYyoL8z1QpM8VmL&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -319,7 +309,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.09M.08\"", format: mp4, codec: av01, - throttled: false, ), ], audio_streams: [ @@ -343,7 +332,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: None, - throttled: false, track: None, ), AudioStream( @@ -366,7 +354,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: None, - throttled: false, track: None, ), AudioStream( @@ -389,7 +376,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: None, - throttled: false, track: None, ), AudioStream( @@ -412,7 +398,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: None, - throttled: false, track: None, ), AudioStream( @@ -435,7 +420,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: None, - throttled: false, track: None, ), ], @@ -482,5 +466,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: android, visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap index 68cd7f6..ec4ae26 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktop.snap @@ -84,7 +84,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, - throttled: false, ), ], video_only_streams: [ @@ -111,7 +110,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=1224002&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAI-uoNLUkMHpH35niVh1tBvwwFLtmSbeHyknmyCvccFVAiB2XriyJd0u2q-tGIRTx5qtKt6bJCs5ndXtMsdSxOheuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -136,7 +134,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2973283&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgEleuqkeo7x7BsHur5aGPfHaT6KjKEG4c1d_xXwqlrsYCIQD85X_m050XwWyYlfLiWtZz-TX--H8H0UvfZCWKpY7m4Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -161,7 +158,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=2238952&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIBttTR02kTdGb4vdxQ9Gro88JOAY7u5z69nJbdmVS1sAiBr61rqkUtra4PHLdnp2w-s8ZSaN_4qZ3OEeeuIr5C13w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -186,7 +182,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=7808990&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMBRhMAZ5GXFSZHN6D-XhXRdG_EWSNwnN2eLPlwVNQ6PAiEA75eH0iJLgwRkujaABZnaJxG2ni-4irYHEGD42x6uaQg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", @@ -211,7 +206,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=5169510&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgNi0fwQbep6oKsEeEGfms2Ay4x2OL2G0hUX5GFhycgKkCIANiC-j-Gz3-noxsNeSKKPxy--T9mFBu_8V7Vi5-zDYS&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -236,7 +230,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=4130385&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgFuBoOIkqwq0D1_OmnNJx3C0jmhHUyskpzPrTMoaWRYECIFZ1Y4QbQ41GsWS8yRHox8l_nGVosfXhXfKu3v18AyeT&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -261,7 +254,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.01M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=8890590&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgMYF0KQQNgYI8oOhgdCwyRY6E_hvFnJiaAadyMf89MRoCIHnDnROTvUoy0iIBM3MzFAxJh_bLA-2vFl9KFDrHOf1B&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -286,7 +278,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=6873325&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAOtLGFoFtLHIXzNRoSrR7ULbIz91OYmaVQkcSatqNKAiAiEA23ZF7h2BZZCAGc0Zdd2p3PWRotmwLDyH6yYCuQpE8xw%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -311,7 +302,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.04M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=16547577&dur=163.029&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgfYKbT_196P-2EtjuqcTKdataiM480y65Ko0a73dv7WECIQC6nqWienQvu7swC1OW9HlwFWRH7VwTwj6H4yjY6FYvzg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -336,7 +326,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=35955780&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgQG8GPj3w_5_Lr2apagmte66IFBY3bYcZ2KnhwnUpshYCIFgvHYIZsz8WdYGSk9adpfMNKX0pzSP_l8cW47Gq2RTi&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -361,7 +350,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=22365208&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAI-VhcBU6o8LGmeuVYC2_zbxeGvC6XWf7yIOQ1RvjURhAiEA0YcZlVOI2ZUtKl-31__Hzax2SOUPeekCRjqjfw4m15s%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -386,7 +374,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.08M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=65400181&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAIdbG-deTvLhp7mD2b-QZYQamPFv75l1bNBEEOMihrxPAiEA1NYvRlFphbRRvFIBCP-Ij9-5q8OTwUskgsL6LyIrD7c%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1", @@ -411,7 +398,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=62993617&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ8n34LQhg6iEg1Ux9rDkk48e8l3vBR4WwuHeIpKnorlAiBopK4z-nq-pJTPTmrdbbKPW1Lfufdz2f9sGUKY-dzk5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -436,7 +422,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=WEB&clen=42567727&dur=163.046&ei=q1jpYtq3BJCX1gKVyJGQDg&expire=1659481355&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AGfBIFoT5D_NZAwXN7lVCS2VYLDMMegfaJQqvSJp-Hhy&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C26&mn=sn-h0jelnez%2Csn-4g5edn6k&ms=au%2Conr&mt=1659459429&mv=m&mvi=4&n=T16m7p0RvV7UhQ&ns=tWuNfisHu8yiCA6Avm7nUlwH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAMewAT3SgJRGn7wqDaDzNWcsAfrjFRu6k0wm7O_5YJeQAiANVhGmILp_gmNXnmixDesxsZ44_72YBT2SqjLLSZV32w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1", @@ -461,7 +446,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.09M.08\"", format: mp4, codec: av01, - throttled: false, ), ], audio_streams: [ @@ -485,7 +469,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), AudioStream( @@ -508,7 +491,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), AudioStream( @@ -531,7 +513,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: Some(5.2159004), - throttled: false, track: None, ), AudioStream( @@ -554,7 +535,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), ], @@ -601,5 +581,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: desktop, visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap index fa9f936..8ea0e63 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_desktopmusic.snap @@ -52,7 +52,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, - throttled: false, ), ], video_only_streams: [ @@ -79,7 +78,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2973283&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAO7DI5E91yHpLhgiWg9C99NsMoJBVOWsNTNF3os9kREQAiAr2oC8vFtXIHwkJJt45q0sdmjiJdkTO2i8VAjUodk6Xw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -104,7 +102,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=7808990&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgTkOjFd0nExEtpr8sBIaNu9HhkxWNdjhSKufHMhLR8-8CIHJAmOuCD7VBv_krH6rn5zqXFqAfsq9rQPXlC3CcQrjM&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", @@ -129,7 +126,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=5169510&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAPqQfxwIANgIC3DrQ6avaWOhCvIMLdzMPQtFOx2gwEXNAiAwJp2mgN9-zl4vPOB2uoQXOfmGsYDB470q1zg7wRW4Sw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -154,7 +150,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=8890590&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAIjdvhcThMxoo_v2bzEjaR_w0ryWFQDs0f0INaI5WPcVAiApQZUYTqcQJdfxZlNSsp7cl3FK8XPfDZ-qbVvj9GuauQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -179,7 +174,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=16547577&dur=163.029&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgBV4Oa1IQ0YNDvRrKO5ec3Pfbg65MxzmIxCcm0gOuwT0CIFysQdow6DQXzz1W9KZVuqACTdjXQ3-yiBj9GcmNw3HE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -204,7 +198,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=35955780&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAOiqSNfGfOprZ9InWVMc7gY0KrTf8weLibcpK0W2Hfa6AiAFHW213qsByzlar5ivCAYttjo1rPciQnLEnh-izJ3ZhA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -229,7 +222,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=65400181&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgdkJv6w9_Azf0m6poA-ULyX0eH_GKBtSJRwUY1lNBAZgCIDCrC0lnu__ycTaIhg0pUcsRUqay60S3QMo5084EWifd&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1", @@ -254,7 +246,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=62993617&dur=163.046&ei=knDpYub6BojEgAf6jbLgDw&expire=1659487474&fexp=24001373%2C24007246&fvip=4&gir=yes&id=o-AM-wcJVO-yYYbVFnuifnzM4eRnD-AG1bS1AhLoDqi_is&initcwndbps=1418750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659465669&mv=m&mvi=5&n=1taQMNHGExb_Vg&ns=UTT8RXHZNhPYTw6NgkzWMWEH&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgZi9dDSMWh10NID8-QNn3azIH1zw5UooZrRTPZjVn7hYCIAm9bFc6NBwJ_DzY4V2R_zGmJSpOwQl8LEsfCb7hf6i7&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=1311222&vprv=1", @@ -279,7 +270,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), ], audio_streams: [ @@ -303,7 +293,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(0.0006532669), - throttled: false, track: None, ), AudioStream( @@ -326,7 +315,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(0.0006532669), - throttled: false, track: None, ), AudioStream( @@ -349,7 +337,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: Some(-0.003446579), - throttled: false, track: None, ), AudioStream( @@ -372,7 +359,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(0.0006532669), - throttled: false, track: None, ), ], @@ -419,5 +405,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: desktop_music, visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap index c068095..dcad31b 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_ios.snap @@ -81,7 +81,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4D401E\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=65400181&dur=163.046&ei=q1jpYq-xHs7NgQev0bfwAQ&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-ANNg3iPHI56jhLSlPQk4pi4mdub5iAby0hmJBVrtiJgY&initcwndbps=1513750&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&otfp=1&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAP6zxXXA18ToZWUfalauhhsgOsDHTu-R0QrqNrJR7D5kAiEAi8HBa9OkYwmA0bcRxhgvXfN9JsFlXwCWJ-x4ty6TjoY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Csvpuc%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&svpuc=1&txp=2211222&vprv=1", @@ -106,7 +105,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002A\"", format: mp4, codec: avc1, - throttled: false, ), ], audio_streams: [ @@ -130,7 +128,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: Some(5.2159004), - throttled: false, track: None, ), AudioStream( @@ -153,7 +150,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: Some(5.2159004), - throttled: false, track: None, ), ], @@ -200,5 +196,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: ios, visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"), ) diff --git a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap index 82d769b..ee018d8 100644 --- a/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap +++ b/src/client/snapshots/rustypipe__client__player__tests__map_player_data_tvhtml5embed.snap @@ -84,7 +84,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", format: mp4, codec: avc1, - throttled: false, ), ], video_only_streams: [ @@ -111,7 +110,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=1224002&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=394&keepalive=yes&lmt=1608045375671513&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKyA5SE5VppKcNlosTsDsa4s039Ia-Qymp9zS3hAlScmAiBzo8tirHhDQVcMHejguHQ3F5rglFmjjy1hFlopVpNe-A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -136,7 +134,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2973283&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=242&keepalive=yes&lmt=1608509388282028&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgN7FPp-_Ay_e78kvW7bcBceUhHDnpgXSZKxxn-x34DTgCIEqr4KN5E3R9ZVzCFV3HGaTr6YZEGeNDRxS4ne7JFDRN&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -161,7 +158,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2238952&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=395&keepalive=yes&lmt=1608045728968690&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAKBPl7ZiI0t6SteLZUEX96zhu1FVKBLZz6GP-_6K-nJMAiBcWq7zKq-fNeSJbMaGcrgU8tshLKzNu2Mv0b1pFrPbMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -186,7 +182,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.00M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=7808990&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=134&keepalive=yes&lmt=1580005649163759&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgLnuMRsG-Huz0E9KzrpsLbN8akn6slETHnYESZLtoJXgCIFXPrk4JyA2KRZnD8EVn7c1JRqFNUV1acExNy0Z6wfeX&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -211,7 +206,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.4d401e\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=5169510&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=243&keepalive=yes&lmt=1608509388282405&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhANJoH9RPIFwd08jukBbSBYSH-gmli5NIdZRVDZD8StFiAiEAtjCXNscOn1rgndc2QQQYV97sWCCYPwWvO0tgkUjRm74%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -236,7 +230,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=4130385&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=396&keepalive=yes&lmt=1608045761576250&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgcVEF2GELVbjio4lbmnBkFmi2HT4gkRQyM-SU3Tv-bMgCIQDs8WhxxNLSj3K-0ccvv6wzpWweOuwhdj9hjCXa0-9PnQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -261,7 +254,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.01M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=8890590&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=244&keepalive=yes&lmt=1608509388284632&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgEC-9_1jHyfgc_Vtpe7vuWTJYd2S_MrJaSDfYfx8cCQcCIEIPWqkLyLh3yLlAM-ZPpySBXCS9Z9Hs1Mk_dVLsnBhY&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -286,7 +278,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=6873325&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=397&keepalive=yes&lmt=1608045990917419&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAK8Grn-QuhjptRGaHT2NYU97O15VoIXwX0EYKhl4FIFIAiEA9152IGHn7QbRCGRfk1Q0Yqfpr9Hhjp-u4e8L8vhuXtk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -311,7 +302,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.04M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=16547577&dur=163.029&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=247&keepalive=yes&lmt=1608509388326822&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgFVGnmP4_M__D1Lga0s1av1aEBTmW54m9NdJY5I88xaECIQDMMIOCWFm-Aje4sHxWihE_tFpg1qrfS0qlbGRtouR1zA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -336,7 +326,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=35955780&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=302&keepalive=yes&lmt=1608509234088626&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAKDysUcBDLlWx0vZ8CifiOcjQWBo4uc9JlogYR4z1cX0AiEA6Jgek2vwU6z3zM-aiQDh7GZXX2f19HPPKxwhZLvkshE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -361,7 +350,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=22365208&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=398&keepalive=yes&lmt=1608048380553749&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgcHUn_ogkBtSQLpq8m-l4IqLlx7EKsddusFPuwvMlLuoCIDF1FiMdigJzd_H5xIgglkW7GaS3CG5Sx9aC2O5pAtUG&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -386,7 +374,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.08M.08\"", format: mp4, codec: av01, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=65400181&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=299&keepalive=yes&lmt=1580005649161486&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgRoFTJHusyDU4PA4tIpFb7cNHxwiKOH_C5FGDdcx16ScCIC2SlCLt3gTJ2mUuTbav41TnZ5pVEAbiLxuY6pMV4stE&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1", @@ -411,7 +398,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"avc1.64002a\"", format: mp4, codec: avc1, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=62993617&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=303&keepalive=yes&lmt=1608509371758331&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgIChm15WPOCXfBDCY0W_4Ul3wdL8YRia4knFoPl_u8AsCIQCTSOnu_bi5-FkCPiOM0P8WTDaXo9hGJuYmxguzxbF88A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -436,7 +422,6 @@ VideoPlayer( mime: "video/webm; codecs=\"vp9\"", format: webm, codec: vp9, - throttled: false, ), VideoStream( url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C394%2C395%2C396%2C397%2C398%2C399&c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=42567727&dur=163.046&ei=q1jpYv-eJ9uF6dsPhvyH8As&expire=1659481355&fexp=24001373%2C24007246&fvip=5&gir=yes&id=o-AKkOKYSoYWWfNLdrt3aQbxbIwHh4mMVyXLb2mtjc0uXQ&initcwndbps=1527500&ip=2003%3Ade%3Aaf0e%3A2f00%3Ade47%3A297%3Aa6db%3A774e&itag=399&keepalive=yes&lmt=1608052932785283&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelnez%2Csn-h0jeenek&ms=au%2Crdu&mt=1659459429&mv=m&mvi=4&n=U0g9MK69PQnuYQ&ns=h4nQ-PcvhL4hLZwTU9i7QAoH&otfp=1&pcm2=yes&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgO3omBCES-iEOIeuiy9Jsz9wB_QfRkCuRCiCQ-N5KdqoCIQDANFWf0zfBSm1qGjA7jYJEti7hiM9klZHFZjC2CN9r9A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1", @@ -461,7 +446,6 @@ VideoPlayer( mime: "video/mp4; codecs=\"av01.0.09M.08\"", format: mp4, codec: av01, - throttled: false, ), ], audio_streams: [ @@ -485,7 +469,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), AudioStream( @@ -508,7 +491,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), AudioStream( @@ -531,7 +513,6 @@ VideoPlayer( codec: mp4a, channels: Some(2), loudness_db: Some(5.2159004), - throttled: false, track: None, ), AudioStream( @@ -554,7 +535,6 @@ VideoPlayer( codec: opus, channels: Some(2), loudness_db: Some(5.2200003), - throttled: false, track: None, ), ], @@ -601,5 +581,6 @@ VideoPlayer( frames_per_page_y: 5, ), ], + client_type: tv_html5_embed, visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"), ) diff --git a/src/client/trends.rs b/src/client/trends.rs index 8b6f3b8..0a46bc5 100644 --- a/src/client/trends.rs +++ b/src/client/trends.rs @@ -10,7 +10,9 @@ use crate::{ serializer::MapResult, }; -use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery}; +use super::{ + response, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery, +}; impl RustyPipeQuery { /// Get the videos from the YouTube startpage @@ -56,10 +58,7 @@ impl RustyPipeQuery { impl MapResponse> for response::Startpage { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let grid = self .contents @@ -75,10 +74,10 @@ impl MapResponse> for response::Startpage { Ok(map_startpage_videos( grid, - lang, + ctx.lang, self.response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)), + .or_else(|| ctx.visitor_data.map(str::to_owned)), )) } } @@ -86,10 +85,7 @@ impl MapResponse> for response::Startpage { impl MapResponse> for response::Trending { fn map_response( self, - _id: &str, - lang: crate::param::Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let items = self .contents @@ -103,7 +99,7 @@ impl MapResponse> for response::Trending { .section_list_renderer .contents; - let mut mapper = response::YouTubeListMapper::::new(lang); + let mut mapper = response::YouTubeListMapper::::new(ctx.lang); mapper.map_response(items); Ok(MapResult { @@ -141,9 +137,8 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, + client::{response, MapRespCtx, MapResponse}, model::{paginator::Paginator, VideoItem}, - param::Language, serializer::MapResult, util::tests::TESTFILES, }; @@ -155,9 +150,8 @@ mod tests { let startpage: response::Startpage = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = startpage - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + startpage.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), @@ -179,9 +173,8 @@ mod tests { let startpage: response::Trending = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res: MapResult> = startpage - .map_response("", Language::En, None, None) - .unwrap(); + let map_res: MapResult> = + startpage.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/client/url_resolver.rs b/src/client/url_resolver.rs index b9b0a0b..ab69903 100644 --- a/src/client/url_resolver.rs +++ b/src/client/url_resolver.rs @@ -5,14 +5,13 @@ use serde::Serialize; use crate::{ error::{Error, ExtractionError}, model::UrlTarget, - param::Language, serializer::MapResult, util, }; use super::{ response::{self, url_endpoint::NavigationEndpoint}, - ClientType, MapResponse, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -325,13 +324,7 @@ impl RustyPipeQuery { } impl MapResponse for response::ResolvedUrl { - fn map_response( - self, - _id: &str, - _lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, - ) -> Result, ExtractionError> { + fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result, ExtractionError> { let pt = self.endpoint.page_type(); if let NavigationEndpoint::Browse { browse_endpoint, .. diff --git a/src/client/video_details.rs b/src/client/video_details.rs index d08d0a2..1d8a9db 100644 --- a/src/client/video_details.rs +++ b/src/client/video_details.rs @@ -15,7 +15,7 @@ use crate::{ use super::{ response::{self, video_details::Payload, IconType}, - ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext, + ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext, }; #[derive(Debug, Serialize)] @@ -89,28 +89,26 @@ impl RustyPipeQuery { impl MapResponse for response::VideoDetails { fn map_response( self, - id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result, ExtractionError> { let mut warnings = Vec::new(); let contents = self.contents.ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no content".into(), })?; let current_video_endpoint = self.current_video_endpoint .ok_or_else(|| ExtractionError::NotFound { - id: id.to_owned(), + id: ctx.id.to_owned(), msg: "no current_video_endpoint".into(), })?; let video_id = current_video_endpoint.watch_endpoint.video_id; - if id != video_id { + if ctx.id != video_id { return Err(ExtractionError::WrongResult(format!( - "got wrong video id {video_id}, expected {id}" + "got wrong video id {}, expected {}", + video_id, ctx.id ))); } @@ -120,7 +118,7 @@ impl MapResponse for response::VideoDetails { .results .contents .ok_or_else(|| ExtractionError::NotFound { - id: id.into(), + id: ctx.id.into(), msg: "no primary_results".into(), })?; warnings.append(&mut primary_results.warnings); @@ -189,7 +187,7 @@ impl MapResponse for response::VideoDetails { // so we ignore parse errors here for now like_text.and_then(|txt| util::parse_numeric(&txt).ok()), date_text.as_deref().and_then(|txt| { - timeago::parse_textual_date_or_warn(lang, txt, &mut warnings) + timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings) }), date_text, view_count @@ -207,7 +205,7 @@ impl MapResponse for response::VideoDetails { let comment_count = comment_count_section.and_then(|s| { util::parse_large_numstr_or_warn::( &s.comments_entry_point_header_renderer.comment_count, - lang, + ctx.lang, &mut warnings, ) }); @@ -275,7 +273,7 @@ impl MapResponse for response::VideoDetails { let visitor_data = self .response_context .visitor_data - .or_else(|| vdata.map(str::to_owned)); + .or_else(|| ctx.visitor_data.map(str::to_owned)); let recommended = contents .two_column_watch_next_results .secondary_results @@ -285,7 +283,7 @@ impl MapResponse for response::VideoDetails { r, sr.secondary_results.continuations, visitor_data.clone(), - lang, + ctx.lang, ); warnings.append(&mut res.warnings); res.c @@ -350,7 +348,7 @@ impl MapResponse for response::VideoDetails { avatar: owner.thumbnail.into(), verification: owner.badges.into(), subscriber_count: owner.subscriber_count_text.and_then(|txt| { - util::parse_large_numstr_or_warn(&txt, lang, &mut warnings) + util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings) }), }, view_count, @@ -385,10 +383,7 @@ impl MapResponse for response::VideoDetails { impl MapResponse> for response::VideoComments { fn map_response( self, - _id: &str, - lang: Language, - _deobf: Option<&crate::deobfuscate::DeobfData>, - _vdata: Option<&str>, + ctx: &MapRespCtx<'_>, ) -> Result>, ExtractionError> { let received_endpoints = self.on_response_received_endpoints; let mut warnings = Vec::new(); @@ -415,7 +410,7 @@ impl MapResponse> for response::VideoComments { comment.comment_renderer, Some(thread.replies), thread.rendering_priority, - lang, + ctx.lang, &mut warnings, )); } else if let Some(vm) = thread.comment_view_model { @@ -424,7 +419,7 @@ impl MapResponse> for response::VideoComments { &mut mutations, Some(thread.replies), thread.rendering_priority, - lang, + ctx.lang, &mut warnings, ) { comments.push(c); @@ -440,7 +435,7 @@ impl MapResponse> for response::VideoComments { comment, None, response::video_details::CommentPriority::RenderingPriorityUnknown, - lang, + ctx.lang, &mut warnings, )); } @@ -450,7 +445,7 @@ impl MapResponse> for response::VideoComments { &mut mutations, None, response::video_details::CommentPriority::RenderingPriorityUnknown, - lang, + ctx.lang, &mut warnings, ) { comments.push(c); @@ -654,8 +649,7 @@ mod tests { use rstest::rstest; use crate::{ - client::{response, MapResponse}, - param::Language, + client::{response, MapRespCtx, MapResponse}, util::tests::TESTFILES, }; @@ -676,7 +670,7 @@ mod tests { let details: response::VideoDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = details.map_response(id, Language::En, None, None).unwrap(); + let map_res = details.map_response(&MapRespCtx::test(id)).unwrap(); assert!( map_res.warnings.is_empty(), @@ -696,9 +690,7 @@ mod tests { let details: response::VideoDetails = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let err = details - .map_response("", Language::En, None, None) - .unwrap_err(); + let err = details.map_response(&MapRespCtx::test("")).unwrap_err(); assert!(matches!( err, crate::error::ExtractionError::NotFound { .. } @@ -716,7 +708,7 @@ mod tests { let comments: response::VideoComments = serde_json::from_reader(BufReader::new(json_file)).unwrap(); - let map_res = comments.map_response("", Language::En, None, None).unwrap(); + let map_res = comments.map_response(&MapRespCtx::test("")).unwrap(); assert!( map_res.warnings.is_empty(), diff --git a/src/deobfuscate.rs b/src/deobfuscate.rs index 776d221..58e2924 100644 --- a/src/deobfuscate.rs +++ b/src/deobfuscate.rs @@ -57,7 +57,7 @@ impl DeobfData { res } - fn extract_fns(js_url: &str, player_js: &str) -> Result { + pub fn extract_fns(js_url: &str, player_js: &str) -> Result { let sig_fn = get_sig_fn(player_js)?; let nsig_fn = get_nsig_fn(player_js)?; let sts = get_sts(player_js)?; @@ -84,28 +84,19 @@ impl Deobfuscator { /// Deobfuscate the `s` parameter from the `signature_cipher` field pub fn deobfuscate_sig(&self, sig: &str) -> Result { - let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?; + let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, [sig])?; - res.as_str().map_or( - Err(DeobfError::Other("sig deobfuscation func returned null")), - |res| { - tracing::debug!("deobfuscated sig"); - Ok(res.to_owned()) - }, - ) + res.into_string() + .ok_or(DeobfError::Other("sig deobfuscation fn returned no string")) } /// Deobfuscate the `n` stream URL parameter to circumvent throttling pub fn deobfuscate_nsig(&self, nsig: &str) -> Result { - let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?; + let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, [nsig])?; - res.as_str().map_or( - Err(DeobfError::Other("nsig deobfuscation func returned null")), - |res| { - tracing::debug!("deobfuscated nsig"); - Ok(res.to_owned()) - }, - ) + res.into_string().ok_or(DeobfError::Other( + "nsig deobfuscation fn returned no string", + )) } } @@ -144,12 +135,9 @@ fn get_sig_fn(player_js: &str) -> Result { let deobfuscate_function = format!( "var {};", - function_pattern + &function_pattern .captures(player_js) - .ok_or(DeobfError::Extraction("deobf function"))? - .get(1) - .unwrap() - .as_str() + .ok_or(DeobfError::Extraction("deobf function"))?[1] ); static HELPER_OBJECT_NAME_REGEX: Lazy = @@ -168,59 +156,37 @@ fn get_sig_fn(player_js: &str) -> Result { let helper_pattern = Regex::new(&helper_pattern_str) .map_err(|_| DeobfError::Other("could not parse helper pattern regex"))?; let player_js_nonl = player_js.replace('\n', ""); - let helper_object = helper_pattern + let helper_object = &helper_pattern .captures(&player_js_nonl) - .ok_or(DeobfError::Extraction("helper object"))? - .get(1) - .unwrap() - .as_str(); + .ok_or(DeobfError::Extraction("helper object"))?[1]; - Ok(helper_object.to_owned() + let js_fn = helper_object.to_owned() + &deobfuscate_function - + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name)) + + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name); + verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?; + + Ok(js_fn) } -fn get_nsig_fn_name(player_js: &str) -> Result { +fn get_nsig_fn_names(player_js: &str) -> impl Iterator + '_ { static FUNCTION_NAME_REGEX: Lazy = Lazy::new(|| { - Regex::new( - r#"\.get\("n"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)"#, - ) - .unwrap() + // x.get( .. y=functionName[array_num](z) .. x.set( + Regex::new(r#"\w\.get\(.+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap() }); - let fname_match = FUNCTION_NAME_REGEX - .captures(player_js) - .ok_or(DeobfError::Extraction("n_deobf function"))?; + FUNCTION_NAME_REGEX + .captures_iter(player_js) + .filter_map(|fname_match| { + let function_name = &fname_match[1]; - let function_name = fname_match.get(1).unwrap().as_str(); + let array_num = fname_match[2].parse::().ok()?; + let array_pattern_str = + format!(r#"var {}\s*=\s*\[(.+?)]"#, regex::escape(function_name)); + let array_pattern = Regex::new(&array_pattern_str).ok()?; - if fname_match.len() == 1 { - return Ok(function_name.to_owned()); - } - - let array_num = fname_match - .get(2) - .unwrap() - .as_str() - .parse::() - .or(Err(DeobfError::Other("could not parse array_num")))?; - let array_pattern_str = format!(r#"var {}\s*=\s*\[(.+?)]"#, regex::escape(function_name)); - let array_pattern = Regex::new(&array_pattern_str).or(Err(DeobfError::Other( - "could not parse helper pattern regex", - )))?; - - let array_str = array_pattern - .captures(player_js) - .ok_or(DeobfError::Extraction("n_deobf array_str"))? - .get(1) - .unwrap() - .as_str(); - - let mut names = array_str.split(','); - let name = names - .nth(array_num) - .ok_or(DeobfError::Extraction("n_deobf function name"))?; - Ok(name.to_owned()) + let array_str = &array_pattern.captures(player_js)?[1]; + array_str.split(',').nth(array_num).map(str::to_owned) + }) } fn extract_js_fn(js: &str, name: &str) -> Result { @@ -275,13 +241,44 @@ fn extract_js_fn(js: &str, name: &str) -> Result { Ok(js[start..end].to_owned()) } -fn get_nsig_fn(player_js: &str) -> Result { - let function_name = get_nsig_fn_name(player_js)?; - let function_base = function_name.clone() + "=function"; - let offset = player_js.find(&function_base).unwrap_or_default(); +/// Verify if the deobfuscation function successfully processes a random input string +fn verify_fn(js_fn: &str, fn_name: &str) -> Result<(), DeobfError> { + let ctx = quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; + ctx.eval(js_fn)?; + let res = ctx + .call_function(fn_name, [util::generate_content_playback_nonce()])? + .into_string() + .ok_or(DeobfError::Other("deobfuscation fn returned no string"))?; + if res.is_empty() { + return Err(DeobfError::Other("deobfuscation fn returned empty string")); + } + Ok(()) +} - extract_js_fn(&player_js[offset..], &function_name) - .map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_name)) +fn get_nsig_fn(player_js: &str) -> Result { + let extract_fn = |name: &str| -> Result { + let function_base = format!("{name}=function"); + let offset = player_js + .find(&function_base) + .ok_or(DeobfError::Extraction("could not find function base"))?; + + let js_fn = extract_js_fn(&player_js[offset..], name) + .map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, name))?; + verify_fn(&js_fn, DEOBF_NSIG_FUNC_NAME)?; + tracing::info!("Successfully extracted nsig fn `{name}`"); + Ok(js_fn) + }; + + util::find_map_or_last_err( + get_nsig_fn_names(player_js), + DeobfError::Extraction("no nsig fn name found"), + |name| { + extract_fn(&name).map_err(|e| { + tracing::warn!("Failed to extract nsig fn `{name}`: {e}"); + e + }) + }, + ) } async fn get_player_js_url(http: &Client) -> Result { @@ -295,12 +292,9 @@ async fn get_player_js_url(http: &Client) -> Result { static PLAYER_HASH_PATTERN: Lazy = Lazy::new(|| { Regex::new(r"https:\\/\\/www\.youtube\.com\\/s\\/player\\/([a-z0-9]{8})\\/").unwrap() }); - let player_hash = PLAYER_HASH_PATTERN + let player_hash = &PLAYER_HASH_PATTERN .captures(&text) - .ok_or(DeobfError::Extraction("player hash"))? - .get(1) - .unwrap() - .as_str(); + .ok_or(DeobfError::Extraction("player hash"))?[1]; Ok(format!( "https://www.youtube.com/s/player/{player_hash}/player_ias.vflset/en_US/base.js" @@ -318,10 +312,7 @@ fn get_sts(player_js: &str) -> Result { Ok(STS_PATTERN .captures(player_js) - .ok_or(DeobfError::Extraction("sts"))? - .get(1) - .unwrap() - .as_str() + .ok_or(DeobfError::Extraction("sts"))?[1] .to_owned()) } @@ -331,6 +322,7 @@ mod tests { use crate::util::tests::TESTFILES; use path_macro::path; use rstest::{fixture, rstest}; + use tracing_test::traced_test; static TEST_JS: Lazy = Lazy::new(|| { let js_path = path!(*TESTFILES / "deobf" / "dummy_player.js"); @@ -382,9 +374,9 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c } #[test] - fn t_get_nsig_fn_name() { - let name = get_nsig_fn_name(&TEST_JS).unwrap(); - assert_eq!(name, "Vo"); + fn t_get_nsig_fn_names() { + let names = get_nsig_fn_names(&TEST_JS).collect::>(); + assert_eq!(names, ["Vo"]); } #[test] @@ -435,14 +427,15 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c } #[tokio::test] + #[traced_test] async fn t_update() { let client = Client::new(); let deobf_data = DeobfData::extract(client, None).await.unwrap(); let deobf = Deobfuscator::new(&deobf_data).unwrap(); let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap(); - println!("{deobf_sig}"); + assert!(deobf_sig.len() >= 100); let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap(); - println!("{deobf_nsig}"); + assert!(deobf_nsig.len() >= 10); } } diff --git a/src/model/mod.rs b/src/model/mod.rs index 3407f7b..99744ec 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; use time::{Date, OffsetDateTime}; use self::{paginator::Paginator, richtext::RichText}; -use crate::{error::Error, param::Country, validate}; +use crate::{client::ClientType, error::Error, param::Country, validate}; /* #COMMON @@ -143,6 +143,8 @@ pub struct VideoPlayer { pub dash_manifest_url: Option, /// Video frames for seek preview pub preview_frames: Vec, + /// Client type with which the player was fetched + pub client_type: ClientType, /// YouTube visitor data cookie pub visitor_data: Option, } @@ -211,9 +213,6 @@ pub struct VideoStream { pub format: VideoFormat, /// Video codec pub codec: VideoCodec, - /// True if the deobfuscation of the nsig url parameter failed - /// and the stream will be throttled - pub throttled: bool, } /// Audio stream @@ -259,9 +258,6 @@ pub struct AudioStream { /// /// The loudness parameter is not available when using the Android client. pub loudness_db: Option, - /// True if the deobfuscation of the nsig url parameter failed - /// and the stream will be throttled - pub throttled: bool, /// Audio track information /// /// Videos can have multiple audio tracks (different languages). @@ -829,7 +825,7 @@ pub enum YouTubeItem { Channel(ChannelItem), } -/// YouTube video list item +/// YouTube video list item (from search results, recommendations, playlists) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct VideoItem { @@ -868,7 +864,7 @@ pub struct VideoItem { pub short_description: Option, } -/// YouTube channel list item +/// YouTube channel list item (from search results) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct ChannelItem { @@ -890,7 +886,7 @@ pub struct ChannelItem { pub short_description: String, } -/// YouTube playlist list item +/// YouTube playlist list item (from search results) #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[non_exhaustive] pub struct PlaylistItem { diff --git a/src/model/traits.rs b/src/model/traits.rs index 329a01d..e493f4f 100644 --- a/src/model/traits.rs +++ b/src/model/traits.rs @@ -135,6 +135,14 @@ pub trait YtEntity { fn id(&self) -> &str; /// Name fn name(&self) -> &str; + /// Channel id + /// + /// `None` if the entity does not belong to a channel + fn channel_id(&self) -> Option<&str>; + /// Channel name + /// + /// `None` if the entity does not belong to a channel + fn channel_name(&self) -> Option<&str>; } macro_rules! yt_entity { @@ -147,6 +155,80 @@ macro_rules! yt_entity { fn name(&self) -> &str { &self.name } + + fn channel_id(&self) -> Option<&str> { + None + } + + fn channel_name(&self) -> Option<&str> { + None + } + } + }; +} + +macro_rules! yt_entity_owner { + ($entity_type:ty) => { + impl YtEntity for $entity_type { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn channel_id(&self) -> Option<&str> { + Some(&self.channel.id) + } + + fn channel_name(&self) -> Option<&str> { + Some(&self.channel.name) + } + } + }; +} + +macro_rules! yt_entity_owner_opt { + ($entity_type:ty) => { + impl YtEntity for $entity_type { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn channel_id(&self) -> Option<&str> { + self.channel.as_ref().map(|c| c.id.as_str()) + } + + fn channel_name(&self) -> Option<&str> { + self.channel.as_ref().map(|c| c.name.as_str()) + } + } + }; +} + +macro_rules! yt_entity_owner_music { + ($entity_type:ty) => { + impl YtEntity for $entity_type { + fn id(&self) -> &str { + &self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn channel_id(&self) -> Option<&str> { + self.artists.first().and_then(|a| a.id.as_deref()) + } + + fn channel_name(&self) -> Option<&str> { + self.artists.first().map(|a| a.name.as_str()) + } } }; } @@ -159,6 +241,14 @@ impl YtEntity for VideoPlayer { fn name(&self) -> &str { &self.details.name } + + fn channel_id(&self) -> Option<&str> { + Some(&self.details.channel.id) + } + + fn channel_name(&self) -> Option<&str> { + Some(&self.details.channel.name) + } } impl YtEntity for Channel { @@ -169,26 +259,34 @@ impl YtEntity for Channel { fn name(&self) -> &str { &self.name } + + fn channel_id(&self) -> Option<&str> { + None + } + + fn channel_name(&self) -> Option<&str> { + None + } } -yt_entity! {VideoPlayerDetails} -yt_entity! {Playlist} +yt_entity_owner! {VideoPlayerDetails} +yt_entity_owner_opt! {Playlist} yt_entity! {ChannelId} -yt_entity! {VideoDetails} +yt_entity_owner! {VideoDetails} yt_entity! {ChannelTag} yt_entity! {ChannelRss} yt_entity! {ChannelRssVideo} -yt_entity! {VideoItem} +yt_entity_owner_opt! {VideoItem} yt_entity! {ChannelItem} -yt_entity! {PlaylistItem} +yt_entity_owner_opt! {PlaylistItem} yt_entity! {VideoId} -yt_entity! {TrackItem} +yt_entity_owner_music! {TrackItem} yt_entity! {ArtistItem} -yt_entity! {AlbumItem} -yt_entity! {MusicPlaylistItem} +yt_entity_owner_music! {AlbumItem} +yt_entity_owner_opt! {MusicPlaylistItem} yt_entity! {AlbumId} -yt_entity! {MusicPlaylist} -yt_entity! {MusicAlbum} +yt_entity_owner_opt! {MusicPlaylist} +yt_entity_owner_music! {MusicAlbum} yt_entity! {MusicArtist} yt_entity! {MusicGenreItem} yt_entity! {MusicGenre} diff --git a/src/param/stream_filter.rs b/src/param/stream_filter.rs index 44c92b2..401f494 100644 --- a/src/param/stream_filter.rs +++ b/src/param/stream_filter.rs @@ -9,15 +9,15 @@ use crate::model::{ /// The StreamFilter is used for selecting audio/video streams from an extracted video #[derive(Debug, Default, Clone)] -pub struct StreamFilter<'a> { +pub struct StreamFilter { audio_max_bitrate: Option, - audio_formats: Option<&'a [AudioFormat]>, - audio_codecs: Option<&'a [AudioCodec]>, - audio_language: Option<&'a str>, + audio_formats: Option>, + audio_codecs: Option>, + audio_language: Option, video_max_res: Option, video_max_fps: Option, - video_formats: Option<&'a [VideoFormat]>, - video_codecs: Option<&'a [VideoCodec]>, + video_formats: Option>, + video_codecs: Option>, video_hdr: bool, video_none: bool, } @@ -64,7 +64,7 @@ impl FilterResult { } } -impl<'a> StreamFilter<'a> { +impl StreamFilter { /// Create a new [`StreamFilter`] #[must_use] pub fn new() -> Self { @@ -90,8 +90,8 @@ impl<'a> StreamFilter<'a> { /// Set the supported audio container formats #[must_use] - pub fn audio_formats(mut self, formats: &'a [AudioFormat]) -> Self { - self.audio_formats = Some(formats); + pub fn audio_formats>>(mut self, formats: F) -> Self { + self.audio_formats = Some(formats.into()); self } @@ -104,8 +104,8 @@ impl<'a> StreamFilter<'a> { /// Set the supported audio codecs #[must_use] - pub fn audio_codecs(mut self, codecs: &'a [AudioCodec]) -> Self { - self.audio_codecs = Some(codecs); + pub fn audio_codecs>>(mut self, codecs: C) -> Self { + self.audio_codecs = Some(codecs.into()); self } @@ -123,8 +123,8 @@ impl<'a> StreamFilter<'a> { /// If this filter is unset or no stream matches, /// the filter returns the default audio stream. #[must_use] - pub fn audio_language(mut self, language: &'a str) -> Self { - self.audio_language = Some(language); + pub fn audio_language>(mut self, language: S) -> Self { + self.audio_language = Some(language.into()); self } @@ -184,8 +184,8 @@ impl<'a> StreamFilter<'a> { /// Set the supported video container formats #[must_use] - pub fn video_formats(mut self, formats: &'a [VideoFormat]) -> Self { - self.video_formats = Some(formats); + pub fn video_formats>>(mut self, formats: F) -> Self { + self.video_formats = Some(formats.into()); self } @@ -198,8 +198,8 @@ impl<'a> StreamFilter<'a> { /// Set the supported video codecs #[must_use] - pub fn video_codecs(mut self, codecs: &'a [VideoCodec]) -> Self { - self.video_codecs = Some(codecs); + pub fn video_codecs>>(mut self, codecs: C) -> Self { + self.video_codecs = Some(codecs.into()); self } @@ -250,6 +250,11 @@ impl<'a> StreamFilter<'a> { ), ) } + + /// Return true if no video stream should be selected + pub fn is_video_none(&self) -> bool { + self.video_none + } } impl VideoPlayer { @@ -373,13 +378,13 @@ mod tests { #[rstest] #[case::default(StreamFilter::default(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16104136&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683782301237288&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAPcUhhfkNVA_JcdU6KLTOFjRCnNl6n8gamJA-Q0PgCpIAiBTMV2k2JfHzbHBtsHxuNW7zHvSaYaUbz-dEIQC45o1eA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] #[case::bitrate(StreamFilter::default().audio_max_bitrate(100_000).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=8217508&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=250&keepalive=yes&lmt=1683782195315620&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIga2iMQsToMxO7hTOx0gNAzhYoV1lL5PpE9lkAuBXt1nkCIQCuFuQXWNixIquEugtkT1C9khuKRP_C-wzSOiUmRp1DRg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] - #[case::m4a_format(StreamFilter::default().audio_formats(&[AudioFormat::M4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] - #[case::m4a_codec(StreamFilter::default().audio_codecs(&[AudioCodec::Mp4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] + #[case::m4a_format(StreamFilter::default().audio_formats([AudioFormat::M4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] + #[case::m4a_codec(StreamFilter::default().audio_codecs([AudioCodec::Mp4a]).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16390508&dur=1012.691&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=140&keepalive=yes&lmt=1683782363698612&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fmp4&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRgIhAMgM470I-QXq4lTRuPtXf5UInHB_tG0tTGXRhVZ6nwImAiEAn0JYRknq5dtTwcmzZheekxVOZKhZ2Rpxc_UyvX2CMRY%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] #[case::french(StreamFilter::default().audio_language("fr").clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=940286&dur=60.101&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683774002236584&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAIUUin7WZBnoVDb2p0wuTPc7HZwbF8I5sxzLrVN9WeBwAiBQTZwhxCQ1IdrUkkD1-cSGYBtMF1aKkjPZ-LWeie0aZA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Ddubbed%3Alang%3Dfr"))] #[case::br_fallback(StreamFilter::default().audio_max_bitrate(0).clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=6306327&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=249&keepalive=yes&lmt=1683782187865292&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRAIgW1DTCrLV_GyEM1rdjScgyceZE1llb73KJMFXmPm5Y04CIAYOLZuuzFX4ba5720kMOcQ1-Ld1DULs85nLxJglitCl&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] #[case::lang_fallback(StreamFilter::default().audio_language("xx").clone(), Some("https://rr4---sn-h0jeener.googlevideo.com/videoplayback?c=WEB&clen=16104136&dur=1012.661&ei=6OtcZNqtBdOi7gP1upHYCQ&expire=1683832904&fexp=24007246&fvip=2&gir=yes&id=o-ABVtPh3j24hkJeXp8igjvreyODn-oV0CacOqb7pDjJoG&initcwndbps=1720000&ip=2003%3Ade%3Aaf31%3A5200%3A791a%3A897%3Ac15c%3Aae59&itag=251&keepalive=yes&lmt=1683782301237288&lsig=AG3C_xAwRQIgC7HZtYuc6dI92m6wCcoXYpdzSpVtPTIbO7jBKGpUrYMCIQCc0WNtFvN8Awqx9uuRVp5SUSe3rOt2D7M-rCKpgVv_0A%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=wB&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jeener%2Csn-h0jeln7l&ms=au%2Crdu&mt=1683811031&mv=m&mvi=4&n=U8mCOo4eYD4n0A&ns=LToEdXWVFHcH53e3aTe1N7kN&pl=37&requiressl=yes&sig=AOq0QJ8wRQIhAPcUhhfkNVA_JcdU6KLTOFjRCnNl6n8gamJA-Q0PgCpIAiBTMV2k2JfHzbHBtsHxuNW7zHvSaYaUbz-dEIQC45o1eA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cxtags%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=qEK7B81AP536F3aOi5JzMyLCUDiktWigtEpf9nI2xg&svpuc=1&txp=4532434&vprv=1&xtags=acont%3Doriginal%3Alang%3Den-US"))] - #[case::noformat(StreamFilter::default().audio_formats(&[]).clone(), None)] - #[case::nocodec(StreamFilter::default().audio_codecs(&[]).clone(), None)] + #[case::noformat(StreamFilter::default().audio_formats([]).clone(), None)] + #[case::nocodec(StreamFilter::default().audio_codecs([]).clone(), None)] fn t_select_audio_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) { let selection = PLAYER_ML.select_audio_stream(&filter); @@ -395,10 +400,10 @@ mod tests { #[case::resolution(StreamFilter::default().video_max_res(720).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=76313586&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=302&keepalive=yes&lmt=1647455155369524&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIgW0H1434eh9Axw6zw95qezJB0D2aVd2bxEIs4T5bcfFACIDOjha9WLycp0L188FZyFGa1RBkLPoGrrJOppsaXqwDR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] #[case::resolution_fps(StreamFilter::default().video_max_res(720).video_max_fps(30).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=47531179&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=247&keepalive=yes&lmt=1647458657499381&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRgIhAMUsmcl1zgbr3YQranPWNV1kcxT5IdEoLL7FTFEDdHHPAiEAhQnrfYMU0A9xZ69MfBujWA4pXtCOQCg2Jn6ve9J_vBQ%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] #[case::res_fallback(StreamFilter::default().video_max_res(100).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=2763284&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=160&keepalive=yes&lmt=1647456833049253&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgLPNxzLxppSSpnDEHxVblrQ38890NMbGnLXlmxljprfQCIQDn4Ir_sjYh7S3ms-Rynm-K0nJpHpQGYsz1nv4TiqeELQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] - #[case::webm_format(StreamFilter::default().video_formats(&[VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] - #[case::vp9_codec(StreamFilter::default().video_codecs(&[VideoCodec::Vp9]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] - #[case::noformat(StreamFilter::default().video_formats(&[]).clone(), None)] - #[case::nocodec(StreamFilter::default().video_codecs(&[]).clone(), None)] + #[case::webm_format(StreamFilter::default().video_formats([VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] + #[case::vp9_codec(StreamFilter::default().video_codecs([VideoCodec::Vp9]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"))] + #[case::noformat(StreamFilter::default().video_formats([]).clone(), None)] + #[case::nocodec(StreamFilter::default().video_codecs([]).clone(), None)] fn t_select_video_only_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) { let selection = PLAYER_HDR.select_video_only_stream(&filter); @@ -415,12 +420,12 @@ mod tests { Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1") )] #[case::webm( - StreamFilter::default().video_formats(&[VideoFormat::Webm]).clone(), + StreamFilter::default().video_formats([VideoFormat::Webm]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303%2C308%2C315%2C330%2C331%2C332%2C333%2C334%2C335%2C336%2C337%2C394%2C395%2C396%2C397%2C398%2C399%2C400%2C401%2C694%2C695%2C696%2C697%2C698%2C699%2C700%2C701&c=WEB&clen=998696577&dur=313.780&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=315&keepalive=yes&lmt=1647476955807851&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRAIfP4IVSo-00_kq_JIkuh032hcLoJzNEhYjvwgLiDpEzQIhALPVrvDBjRwiFddXiAyADmRtYygte4HvlJ3XOrkOf_TR&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1"), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1") )] #[case::noaudio( - StreamFilter::default().audio_formats(&[]).clone(), + StreamFilter::default().audio_formats([]).clone(), Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=23544588&dur=313.834&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=18&lmt=1647456546485912&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=video%2Fmp4&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=HWZNhARNT_nJgg&ns=pLFQxzhiCbZ9F2HJmDLveKoH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgeCEjusAq6p33rH0NHyTAbPIRaaEkjDE32AXBFzDvR-ICIQD0LI8hQVH8oCMWu6OuADzc1FSQhIqYs5RLkxBmObIdsw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4530434&vprv=1"), None )] @@ -429,7 +434,7 @@ mod tests { None, Some("https://rr5---sn-h0jelne7.googlevideo.com/videoplayback?c=WEB&clen=5199784&dur=313.801&ei=eckIY72IKcGZ8gOMt6CwDg&expire=1661541849&fexp=24001373%2C24007246&fvip=2&gir=yes&id=o-AOqXE9lVS424yszv6LN5V_gaevdHxenJl-tYNy3Drs6g&initcwndbps=1428750&ip=2003%3Ade%3Aaf05%3A2500%3A5dad%3A319b%3Aca30%3Ae212&itag=251&keepalive=yes&lmt=1647453650291076&lsig=AG3C_xAwRQIhAMioKyc-dqs-6uvAwLViCcCTXKHn9sIbo0cbSSBXGG4kAiBQNsRBAvQrbWdOjZIsQXYrfPEb1KDpE_AlSEGQZXB9uA%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=NH&mime=audio%2Fwebm&mm=31%2C29&mn=sn-h0jelne7%2Csn-h0jeenl6&ms=au%2Crdu&mt=1661519833&mv=m&mvi=5&n=Zd7nrOM1B2C6PA&ns=426LxLap5MonJD_YWdS4lSYH&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhALtI3j8ZChpNb0LcyDZ3yosbWnSpqaO0-jKAe_UM_RQyAiAMwrpdeNbJEnQn3q1eveaAcRcNIwy5iJ4fIjeBW_MUfg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cdur%2Clmt&spc=lT-KhuPtxVzL5-QbZ7S9zNeOHsWTdms&txp=4532434&vprv=1") )] - #[case::noformat(StreamFilter::default().audio_formats(&[]).video_formats(&[]).clone(), None, None)] + #[case::noformat(StreamFilter::default().audio_formats([]).video_formats([]).clone(), None, None)] fn t_select_video_audio_stream( #[case] filter: StreamFilter, #[case] expect_video_url: Option<&str>, diff --git a/src/util/mod.rs b/src/util/mod.rs index 75dcb00..013ccdd 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -551,6 +551,24 @@ impl<'a> Iterator for SplitTokens<'a> { } } +/// Applies function to the elements of iterator and returns the first successful result +/// or the last error if the function fails on all elements. If the iterator is empty, e_empty +/// is returned. +pub fn find_map_or_last_err(mut iter: I, e_empty: E, mut f: P) -> Result +where + I: Iterator, + P: FnMut(T) -> Result, +{ + let res = iter.try_fold(e_empty, |_, itm| match f(itm) { + Ok(o) => Err(o), + Err(e) => Ok(e), + }); + match res { + Ok(e) => Err(e), + Err(o) => Ok(o), + } +} + #[cfg(test)] pub(crate) mod tests { use std::{fs::File, io::BufReader, path::PathBuf}; @@ -730,4 +748,27 @@ pub(crate) mod tests { let res = country_from_name(name); assert_eq!(res, expect); } + + #[test] + fn t_find_map_or_last_err() { + // Success + let res = find_map_or_last_err([1, 2, 3].into_iter(), 0, |x: i32| { + if x > 2 { + Ok(true) + } else { + Err(x) + } + }); + assert_eq!(res, Ok(true)); + + // Error + let res = find_map_or_last_err([1, 2, 3].into_iter(), 0, |x: i32| Err::<(), _>(x)); + assert_eq!(res, Err(3)); + + // Empty iterator + assert_eq!( + find_map_or_last_err(std::iter::empty(), 0, |_: i32| Ok(true)), + Err(0) + ); + } } diff --git a/testfiles/player_model/hdr.json b/testfiles/player_model/hdr.json index d9feb68..61c3192 100644 --- a/testfiles/player_model/hdr.json +++ b/testfiles/player_model/hdr.json @@ -1125,5 +1125,6 @@ "expires_in_seconds": 21540, "hls_manifest_url": null, "dash_manifest_url": null, - "preview_frames": [] + "preview_frames": [], + "client_type": "desktop" } diff --git a/testfiles/player_model/multilanguage.json b/testfiles/player_model/multilanguage.json index 7f6e1de..d36f102 100644 --- a/testfiles/player_model/multilanguage.json +++ b/testfiles/player_model/multilanguage.json @@ -2120,5 +2120,6 @@ "hls_manifest_url": null, "dash_manifest_url": null, "preview_frames": [], - "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D" + "visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D", + "client_type": "desktop" } diff --git a/tests/snapshots/youtube__music_album_no_artist.snap b/tests/snapshots/youtube__music_album_no_artist.snap index 11818a7..e7b9f2e 100644 --- a/tests/snapshots/youtube__music_album_no_artist.snap +++ b/tests/snapshots/youtube__music_album_no_artist.snap @@ -201,11 +201,11 @@ MusicAlbum( cover: [], artists: [ ArtistId( - id: Some("UCxByvsK9hDZk2MnnF9jsFGw"), + id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"), name: "Herbrido", ), ], - artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"), + artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"), album: Some(AlbumId( id: "MPREb_Z81wHtF9fhC", name: "June Compilation", diff --git a/tests/snapshots/youtube__music_album_no_artist_intl.snap b/tests/snapshots/youtube__music_album_no_artist_intl.snap index 4eb19bc..b57f39f 100644 --- a/tests/snapshots/youtube__music_album_no_artist_intl.snap +++ b/tests/snapshots/youtube__music_album_no_artist_intl.snap @@ -201,11 +201,11 @@ MusicAlbum( cover: [], artists: [ ArtistId( - id: Some("UCxByvsK9hDZk2MnnF9jsFGw"), + id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"), name: "[name]", ), ], - artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"), + artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"), album: Some(AlbumId( id: "MPREb_Z81wHtF9fhC", name: "[name]", diff --git a/tests/youtube.rs b/tests/youtube.rs index 2106c8d..f157d99 100644 --- a/tests/youtube.rs +++ b/tests/youtube.rs @@ -120,7 +120,6 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\""); assert_eq!(video.format, VideoFormat::Mp4); assert_eq!(video.codec, VideoCodec::Av01); - assert!(!video.throttled); assert_approx(audio.bitrate, 142_718); assert_approx(audio.average_bitrate, 130_708); @@ -128,7 +127,6 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe) assert_eq!(audio.mime, "audio/webm; codecs=\"opus\""); assert_eq!(audio.format, AudioFormat::Webm); assert_eq!(audio.codec, AudioCodec::Opus); - assert!(!audio.throttled); check_video_stream(video).await; check_video_stream(audio).await; @@ -1268,7 +1266,7 @@ mod channel_rss { async fn search(rp: RustyPipe, unlocalized: bool) { let result = rp .query() - .search::("doobydoobap") + .search::("arudino") .await .unwrap(); @@ -1279,7 +1277,7 @@ async fn search(rp: RustyPipe, unlocalized: bool) { ); if unlocalized { - assert_eq!(result.corrected_query.as_deref(), Some("doobydobap")); + assert_eq!(result.corrected_query.as_deref(), Some("arduino")); } assert_next(result.items, rp.query(), 10, 2, true).await;