Compare commits
No commits in common. "37a14aa9ce1573febdb025dcccfb47cf1b10bd92" and "182826a3ac31368da658a3561a2b2617f97510e9" have entirely different histories.
37a14aa9ce
...
182826a3ac
36 changed files with 1258 additions and 1765 deletions
|
@ -60,7 +60,7 @@ tracing = { version = "0.1.37", features = ["log"] }
|
||||||
indicatif = "0.17.0"
|
indicatif = "0.17.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
clap = { version = "4.0.29", features = ["derive"] }
|
clap = { version = "4.0.29", features = ["derive"] }
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
tracing-subscriber = "0.3.17"
|
||||||
serde_yaml = "0.9.19"
|
serde_yaml = "0.9.19"
|
||||||
dirs = "5.0.0"
|
dirs = "5.0.0"
|
||||||
filenamify = "0.1.0"
|
filenamify = "0.1.0"
|
||||||
|
@ -70,7 +70,6 @@ rstest = "0.21.0"
|
||||||
tokio-test = "0.4.2"
|
tokio-test = "0.4.2"
|
||||||
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
||||||
path_macro = "1.0.0"
|
path_macro = "1.0.0"
|
||||||
tracing-test = "0.2.5"
|
|
||||||
|
|
||||||
# Included crates
|
# Included crates
|
||||||
rustypipe = { path = ".", version = "0.2.0", default-features = false }
|
rustypipe = { path = ".", version = "0.2.0", default-features = false }
|
||||||
|
@ -116,4 +115,3 @@ rstest.workspace = true
|
||||||
tokio-test.workspace = true
|
tokio-test.workspace = true
|
||||||
insta.workspace = true
|
insta.workspace = true
|
||||||
path_macro.workspace = true
|
path_macro.workspace = true
|
||||||
tracing-test.workspace = true
|
|
||||||
|
|
|
@ -52,7 +52,6 @@ serde_json.workspace = true
|
||||||
indicatif.workspace = true
|
indicatif.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
tracing.workspace = true
|
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
|
370
cli/src/main.rs
370
cli/src/main.rs
|
@ -1,19 +1,18 @@
|
||||||
#![warn(clippy::todo, clippy::dbg_macro)]
|
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
use std::{path::PathBuf, str::FromStr};
|
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
|
use reqwest::{Client, ClientBuilder};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe},
|
client::{ClientType, RustyPipe},
|
||||||
model::{UrlTarget, VideoId, YouTubeItem},
|
model::{UrlTarget, VideoId, YouTubeItem},
|
||||||
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
||||||
};
|
};
|
||||||
use rustypipe_downloader::{DownloadQuery, DownloaderBuilder};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tracing::level_filters::LevelFilter;
|
|
||||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(author, version, about, long_about = None)]
|
#[clap(author, version, about, long_about = None)]
|
||||||
|
@ -34,41 +33,6 @@ struct Cli {
|
||||||
country: Option<String>,
|
country: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
|
||||||
#[group(multiple = false)]
|
|
||||||
struct DownloadTarget {
|
|
||||||
#[clap(short, long)]
|
|
||||||
output: Option<PathBuf>,
|
|
||||||
#[clap(long)]
|
|
||||||
output_file: Option<PathBuf>,
|
|
||||||
#[clap(long)]
|
|
||||||
template: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
#[derive(Subcommand)]
|
||||||
enum Commands {
|
enum Commands {
|
||||||
/// Download a video, playlist, album or channel
|
/// Download a video, playlist, album or channel
|
||||||
|
@ -76,22 +40,18 @@ enum Commands {
|
||||||
Download {
|
Download {
|
||||||
/// ID or URL
|
/// ID or URL
|
||||||
id: String,
|
id: String,
|
||||||
#[clap(flatten)]
|
/// Output path
|
||||||
target: DownloadTarget,
|
#[clap(short, default_value = ".")]
|
||||||
|
output: PathBuf,
|
||||||
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
|
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
resolution: Option<u32>,
|
resolution: Option<u32>,
|
||||||
/// Number of videos downloaded in parallel
|
/// Number of videos downloaded in parallel
|
||||||
#[clap(short, long, default_value_t = 8)]
|
#[clap(short, long, default_value_t = 8)]
|
||||||
parallel: usize,
|
parallel: usize,
|
||||||
/// Use YouTube Music for downloading playlists
|
|
||||||
#[clap(long)]
|
|
||||||
music: bool,
|
|
||||||
/// Limit the number of videos to download
|
/// Limit the number of videos to download
|
||||||
#[clap(long, default_value_t = 1000)]
|
#[clap(long, default_value_t = 1000)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
#[clap(long)]
|
|
||||||
player_type: Option<PlayerType>,
|
|
||||||
},
|
},
|
||||||
/// Extract video, playlist, album or channel data
|
/// Extract video, playlist, album or channel data
|
||||||
Get {
|
Get {
|
||||||
|
@ -156,7 +116,6 @@ enum Commands {
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
music: Option<MusicSearchCategory>,
|
music: Option<MusicSearchCategory>,
|
||||||
},
|
},
|
||||||
Vdata,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, ValueEnum)]
|
#[derive(Copy, Clone, ValueEnum)]
|
||||||
|
@ -293,6 +252,64 @@ impl From<PlayerType> for ClientType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn download_single_video(
|
||||||
|
video_id: &str,
|
||||||
|
video_title: &str,
|
||||||
|
output_dir: &str,
|
||||||
|
output_fname: Option<String>,
|
||||||
|
resolution: Option<u32>,
|
||||||
|
ffmpeg: &str,
|
||||||
|
rp: &RustyPipe,
|
||||||
|
http: Client,
|
||||||
|
multi: MultiProgress,
|
||||||
|
main: Option<ProgressBar>,
|
||||||
|
) -> 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<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
||||||
let stdout = std::io::stdout().lock();
|
let stdout = std::io::stdout().lock();
|
||||||
match format {
|
match format {
|
||||||
|
@ -310,59 +327,55 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
||||||
async fn download_video(
|
async fn download_video(
|
||||||
rp: &RustyPipe,
|
rp: &RustyPipe,
|
||||||
id: &str,
|
id: &str,
|
||||||
target: &DownloadTarget,
|
output_dir: &str,
|
||||||
|
output_fname: Option<String>,
|
||||||
resolution: Option<u32>,
|
resolution: Option<u32>,
|
||||||
player_type: Option<PlayerType>,
|
|
||||||
multi: MultiProgress,
|
|
||||||
) {
|
) {
|
||||||
let mut filter = StreamFilter::new();
|
let http = ClientBuilder::new()
|
||||||
if let Some(res) = resolution {
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0")
|
||||||
if res == 0 {
|
.gzip(true)
|
||||||
filter = filter.no_video();
|
.brotli(true)
|
||||||
} else {
|
.timeout(Duration::from_secs(10))
|
||||||
filter = filter.video_max_res(res);
|
.build()
|
||||||
}
|
.expect("unable to build the HTTP client");
|
||||||
}
|
|
||||||
let dl = DownloaderBuilder::new()
|
// Indicatif setup
|
||||||
.client(rp)
|
let multi = MultiProgress::new();
|
||||||
.stream_filter(filter)
|
|
||||||
.progress_bar(multi)
|
download_single_video(
|
||||||
.build();
|
id,
|
||||||
let mut q = target.apply(dl.download_id(id));
|
id,
|
||||||
if let Some(player_type) = player_type {
|
output_dir,
|
||||||
q = q.player_type(player_type.into());
|
output_fname,
|
||||||
}
|
resolution,
|
||||||
let res = q.download().await;
|
"ffmpeg",
|
||||||
if let Err(e) = res {
|
rp,
|
||||||
tracing::error!("[{id}]: {e}")
|
http,
|
||||||
}
|
multi,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| println!("ERROR: {e:?}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download_videos(
|
async fn download_videos(
|
||||||
rp: &RustyPipe,
|
rp: &RustyPipe,
|
||||||
videos: &[VideoId],
|
videos: &[VideoId],
|
||||||
target: &DownloadTarget,
|
output_dir: &str,
|
||||||
|
output_fname: Option<String>,
|
||||||
resolution: Option<u32>,
|
resolution: Option<u32>,
|
||||||
parallel: usize,
|
parallel: usize,
|
||||||
player_type: Option<PlayerType>,
|
|
||||||
multi: MultiProgress,
|
|
||||||
) {
|
) {
|
||||||
let mut filter = StreamFilter::new();
|
let http = ClientBuilder::new()
|
||||||
if let Some(res) = resolution {
|
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; rv:107.0) Gecko/20100101 Firefox/107.0")
|
||||||
if res == 0 {
|
.gzip(true)
|
||||||
filter = filter.no_video();
|
.brotli(true)
|
||||||
} else {
|
.timeout(Duration::from_secs(10))
|
||||||
filter = filter.video_max_res(res);
|
.build()
|
||||||
}
|
.expect("unable to build the HTTP client");
|
||||||
}
|
|
||||||
let dl = DownloaderBuilder::new()
|
|
||||||
.client(rp)
|
|
||||||
.stream_filter(filter)
|
|
||||||
.progress_bar(multi.clone())
|
|
||||||
.path_precheck()
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Indicatif setup
|
// Indicatif setup
|
||||||
|
let multi = MultiProgress::new();
|
||||||
let main = multi.add(ProgressBar::new(
|
let main = multi.add(ProgressBar::new(
|
||||||
videos.len().try_into().unwrap_or_default(),
|
videos.len().try_into().unwrap_or_default(),
|
||||||
));
|
));
|
||||||
|
@ -376,62 +389,38 @@ async fn download_videos(
|
||||||
main.tick();
|
main.tick();
|
||||||
|
|
||||||
stream::iter(videos)
|
stream::iter(videos)
|
||||||
.for_each_concurrent(parallel, |video| {
|
.map(|video| {
|
||||||
let dl = dl.clone();
|
download_single_video(
|
||||||
let main = main.clone();
|
&video.id,
|
||||||
let id = &video.id;
|
&video.name,
|
||||||
|
output_dir,
|
||||||
let mut q = target.apply(dl.download_entity(video));
|
output_fname.clone(),
|
||||||
if let Some(player_type) = player_type {
|
resolution,
|
||||||
q = q.player_type(player_type.into());
|
"ffmpeg",
|
||||||
}
|
rp,
|
||||||
|
http.clone(),
|
||||||
async move {
|
multi.clone(),
|
||||||
if let Err(e) = q.download().await {
|
Some(main.clone()),
|
||||||
tracing::error!("[{id}]: {e}");
|
)
|
||||||
} else {
|
|
||||||
main.inc(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.await;
|
.buffer_unordered(parallel)
|
||||||
}
|
.collect::<Vec<_>>()
|
||||||
|
.await
|
||||||
/// Stderr writer that suspends the progress bars before printing logs
|
.into_iter()
|
||||||
#[derive(Clone)]
|
.for_each(|res| match res {
|
||||||
struct ProgWriter(MultiProgress);
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
impl<'a> MakeWriter<'a> for ProgWriter {
|
println!("ERROR: {e:?}");
|
||||||
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<usize> {
|
|
||||||
self.0.suspend(|| std::io::stderr().write(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> std::io::Result<()> {
|
|
||||||
std::io::stderr().flush()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let cli = Cli::parse();
|
// env_logger::builder().format_timestamp_micros().init();
|
||||||
let multi = MultiProgress::new();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
tracing_subscriber::fmt::SubscriberBuilder::default()
|
let cli = Cli::parse();
|
||||||
.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);
|
let mut rp = RustyPipe::builder().visitor_data_opt(cli.vdata);
|
||||||
if cli.report {
|
if cli.report {
|
||||||
|
@ -453,20 +442,48 @@ async fn main() {
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Download {
|
Commands::Download {
|
||||||
id,
|
id,
|
||||||
target,
|
output,
|
||||||
resolution,
|
resolution,
|
||||||
parallel,
|
parallel,
|
||||||
music,
|
|
||||||
limit,
|
limit,
|
||||||
player_type,
|
|
||||||
} => {
|
} => {
|
||||||
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
|
// Cases: Existing folder, non-existing file with existing parent folder,
|
||||||
match url_target {
|
// 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 {
|
||||||
UrlTarget::Video { id, .. } => {
|
UrlTarget::Video { id, .. } => {
|
||||||
download_video(&rp, &id, &target, resolution, player_type, multi).await;
|
download_video(&rp, &id, &output_dir, output_fname, resolution).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Channel { id } => {
|
UrlTarget::Channel { id } => {
|
||||||
target.assert_dir();
|
|
||||||
let mut channel = rp.query().channel_videos(id).await.unwrap();
|
let mut channel = rp.query().channel_videos(id).await.unwrap();
|
||||||
channel
|
channel
|
||||||
.content
|
.content
|
||||||
|
@ -483,58 +500,38 @@ async fn main() {
|
||||||
download_videos(
|
download_videos(
|
||||||
&rp,
|
&rp,
|
||||||
&videos,
|
&videos,
|
||||||
&target,
|
&output_dir,
|
||||||
|
output_fname,
|
||||||
resolution,
|
resolution,
|
||||||
parallel,
|
parallel,
|
||||||
player_type,
|
|
||||||
multi,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
UrlTarget::Playlist { id } => {
|
UrlTarget::Playlist { id } => {
|
||||||
target.assert_dir();
|
let mut playlist = rp.query().playlist(id).await.unwrap();
|
||||||
let videos: Vec<VideoId> = if music {
|
playlist
|
||||||
let mut playlist = rp.query().music_playlist(id).await.unwrap();
|
.videos
|
||||||
playlist
|
.extend_limit(&rp.query(), limit)
|
||||||
.tracks
|
.await
|
||||||
.extend_limit(&rp.query(), limit)
|
.unwrap();
|
||||||
.await
|
let videos: Vec<VideoId> = playlist
|
||||||
.unwrap();
|
.videos
|
||||||
playlist
|
.items
|
||||||
.tracks
|
.into_iter()
|
||||||
.items
|
.take(limit)
|
||||||
.into_iter()
|
.map(VideoId::from)
|
||||||
.take(limit)
|
.collect();
|
||||||
.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(
|
download_videos(
|
||||||
&rp,
|
&rp,
|
||||||
&videos,
|
&videos,
|
||||||
&target,
|
&output_dir,
|
||||||
|
output_fname,
|
||||||
resolution,
|
resolution,
|
||||||
parallel,
|
parallel,
|
||||||
player_type,
|
|
||||||
multi,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
target.assert_dir();
|
|
||||||
let album = rp.query().music_album(id).await.unwrap();
|
let album = rp.query().music_album(id).await.unwrap();
|
||||||
let videos: Vec<VideoId> = album
|
let videos: Vec<VideoId> = album
|
||||||
.tracks
|
.tracks
|
||||||
|
@ -545,11 +542,10 @@ async fn main() {
|
||||||
download_videos(
|
download_videos(
|
||||||
&rp,
|
&rp,
|
||||||
&videos,
|
&videos,
|
||||||
&target,
|
&output_dir,
|
||||||
|
output_fname,
|
||||||
resolution,
|
resolution,
|
||||||
parallel,
|
parallel,
|
||||||
player_type,
|
|
||||||
multi,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -744,9 +740,5 @@ async fn main() {
|
||||||
print_data(&res, format, pretty);
|
print_data(&res, format, pretty);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Vdata => {
|
|
||||||
let vd = rp.query().get_visitor_data().await.unwrap();
|
|
||||||
println!("{vd}");
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,19 @@
|
||||||
#![warn(missing_docs, clippy::todo, clippy::dbg_macro)]
|
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||||
|
|
||||||
//! # YouTube audio/video downloader
|
//! # YouTube audio/video downloader
|
||||||
|
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use std::{
|
use std::{borrow::Cow, cmp::Ordering, ffi::OsString, ops::Range, path::PathBuf, time::Duration};
|
||||||
borrow::Cow,
|
|
||||||
cmp::Ordering,
|
|
||||||
ffi::OsString,
|
|
||||||
ops::Range,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{ProgressBar, ProgressStyle};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::{header, Client, StatusCode};
|
use reqwest::{header, Client};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe},
|
model::{traits::FileFormat, AudioCodec, VideoCodec, VideoPlayer},
|
||||||
model::{
|
|
||||||
traits::{FileFormat, YtEntity},
|
|
||||||
AudioCodec, VideoCodec, VideoPlayer,
|
|
||||||
},
|
|
||||||
param::StreamFilter,
|
param::StreamFilter,
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
@ -33,6 +21,7 @@ use tokio::{
|
||||||
io::AsyncWriteExt,
|
io::AsyncWriteExt,
|
||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use util::DownloadError;
|
use util::DownloadError;
|
||||||
|
|
||||||
|
@ -41,569 +30,6 @@ type Result<T> = core::result::Result<T, DownloadError>;
|
||||||
const CHUNK_SIZE_MIN: u64 = 9_000_000;
|
const CHUNK_SIZE_MIN: u64 = 9_000_000;
|
||||||
const CHUNK_SIZE_MAX: u64 = 10_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<DownloaderInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder to construct a new downloader
|
|
||||||
pub struct DownloaderBuilder {
|
|
||||||
rp: Option<RustyPipe>,
|
|
||||||
ffmpeg: String,
|
|
||||||
multi: Option<MultiProgress>,
|
|
||||||
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<MultiProgress>,
|
|
||||||
/// 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<MultiProgress>,
|
|
||||||
/// Stream filter
|
|
||||||
filter: Option<StreamFilter>,
|
|
||||||
/// Target video format
|
|
||||||
video_format: Option<DownloadVideoFormat>,
|
|
||||||
/// ClientType type for fetching videos
|
|
||||||
player_type: Option<ClientType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct DownloadVideo {
|
|
||||||
id: String,
|
|
||||||
name: Option<String>,
|
|
||||||
channel_id: Option<String>,
|
|
||||||
channel_name: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<Self> {
|
|
||||||
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<S: Into<String>>(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<S: Into<String>>(&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<P: Into<PathBuf>>(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<P: Into<PathBuf>>(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<P: Into<PathBuf>>(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<DownloadResult> {
|
|
||||||
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<ProgressBar>, n: u32) -> Result<DownloadResult> {
|
|
||||||
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<StreamDownload> = 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::<Vec<_>>()
|
|
||||||
.await
|
|
||||||
.into_iter()
|
|
||||||
.collect::<core::result::Result<_, _>>()?;
|
|
||||||
|
|
||||||
if let Some(pb) = pb {
|
|
||||||
pb.finish_and_clear();
|
|
||||||
}
|
|
||||||
Ok(DownloadResult {
|
|
||||||
dest: output_path,
|
|
||||||
player_data,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_download_range(offset: u64, size: Option<u64>) -> Range<u64> {
|
fn get_download_range(offset: u64, size: Option<u64>) -> Range<u64> {
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
|
let chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
|
||||||
|
@ -638,26 +64,11 @@ 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::<String>()
|
|
||||||
} else {
|
|
||||||
n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn download_single_file<P: Into<PathBuf>>(
|
async fn download_single_file<P: Into<PathBuf>>(
|
||||||
url: &str,
|
url: &str,
|
||||||
output: P,
|
output: P,
|
||||||
http: &Client,
|
http: Client,
|
||||||
user_agent: &str,
|
pb: ProgressBar,
|
||||||
pb: Option<ProgressBar>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Check if file is already downloaded
|
// Check if file is already downloaded
|
||||||
let output_path: PathBuf = output.into();
|
let output_path: PathBuf = output.into();
|
||||||
|
@ -688,7 +99,6 @@ async fn download_single_file<P: Into<PathBuf>>(
|
||||||
|
|
||||||
let res = http
|
let res = http
|
||||||
.head(url.to_owned())
|
.head(url.to_owned())
|
||||||
.header(header::USER_AGENT, user_agent)
|
|
||||||
.header(header::RANGE, "bytes=0-0")
|
.header(header::RANGE, "bytes=0-0")
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
@ -715,10 +125,8 @@ async fn download_single_file<P: Into<PathBuf>>(
|
||||||
size = Some(original_size);
|
size = Some(original_size);
|
||||||
offset = file_size;
|
offset = file_size;
|
||||||
|
|
||||||
if let Some(pb) = &pb {
|
pb.inc_length(original_size);
|
||||||
pb.inc_length(original_size);
|
pb.inc(offset);
|
||||||
pb.inc(offset);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ordering::Equal => {
|
Ordering::Equal => {
|
||||||
// Already downloaded
|
// Already downloaded
|
||||||
|
@ -745,10 +153,9 @@ async fn download_single_file<P: Into<PathBuf>>(
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if is_gvideo && size.is_some() {
|
if is_gvideo && size.is_some() {
|
||||||
download_chunks_by_param(http, &mut file, url, size.unwrap(), offset, user_agent, pb)
|
download_chunks_by_param(http, &mut file, url, size.unwrap(), offset, pb).await?;
|
||||||
.await?;
|
|
||||||
} else {
|
} else {
|
||||||
download_chunks_by_header(http, &mut file, url, size, offset, user_agent, pb).await?;
|
download_chunks_by_header(http, &mut file, url, size, offset, pb).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(&output_path_tmp, &output_path).await?;
|
fs::rename(&output_path_tmp, &output_path).await?;
|
||||||
|
@ -759,24 +166,22 @@ async fn download_single_file<P: Into<PathBuf>>(
|
||||||
// This is the standardized method that works on all web servers,
|
// This is the standardized method that works on all web servers,
|
||||||
// but I have observed throttling using this method.
|
// but I have observed throttling using this method.
|
||||||
async fn download_chunks_by_header(
|
async fn download_chunks_by_header(
|
||||||
http: &Client,
|
http: Client,
|
||||||
file: &mut File,
|
file: &mut File,
|
||||||
url: &str,
|
url: &str,
|
||||||
size: Option<u64>,
|
size: Option<u64>,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
user_agent: &str,
|
pb: ProgressBar,
|
||||||
pb: Option<ProgressBar>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut offset = offset;
|
let mut offset = offset;
|
||||||
let mut size = size;
|
let mut size = size;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let range = get_download_range(offset, size);
|
let range = get_download_range(offset, size);
|
||||||
tracing::debug!("Fetching range {}-{}", range.start, range.end);
|
debug!("Fetching range {}-{}", range.start, range.end);
|
||||||
|
|
||||||
let res = http
|
let res = http
|
||||||
.get(url.to_owned())
|
.get(url.to_owned())
|
||||||
.header(header::USER_AGENT, user_agent)
|
|
||||||
.header(header::ORIGIN, "https://www.youtube.com")
|
.header(header::ORIGIN, "https://www.youtube.com")
|
||||||
.header(header::REFERER, "https://www.youtube.com/")
|
.header(header::REFERER, "https://www.youtube.com/")
|
||||||
.header(
|
.header(
|
||||||
|
@ -806,19 +211,15 @@ async fn download_chunks_by_header(
|
||||||
offset = parsed_offset + 1;
|
offset = parsed_offset + 1;
|
||||||
if size.is_none() {
|
if size.is_none() {
|
||||||
size = Some(parsed_size);
|
size = Some(parsed_size);
|
||||||
if let Some(pb) = &pb {
|
pb.inc_length(parsed_size);
|
||||||
pb.inc_length(parsed_size);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::debug!("Retrieving chunks...");
|
debug!("Retrieving chunks...");
|
||||||
let mut stream = res.bytes_stream();
|
let mut stream = res.bytes_stream();
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
// Retrieve chunk.
|
// Retrieve chunk.
|
||||||
let mut chunk = item?;
|
let mut chunk = item?;
|
||||||
if let Some(pb) = &pb {
|
pb.inc(chunk.len() as u64);
|
||||||
pb.inc(chunk.len() as u64);
|
|
||||||
}
|
|
||||||
file.write_all_buf(&mut chunk).await?;
|
file.write_all_buf(&mut chunk).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -833,26 +234,22 @@ async fn download_chunks_by_header(
|
||||||
// This ist used by YouTube's web player. The file size
|
// This ist used by YouTube's web player. The file size
|
||||||
// must be known beforehand (it is included in the stream url).
|
// must be known beforehand (it is included in the stream url).
|
||||||
async fn download_chunks_by_param(
|
async fn download_chunks_by_param(
|
||||||
http: &Client,
|
http: Client,
|
||||||
file: &mut File,
|
file: &mut File,
|
||||||
url: &str,
|
url: &str,
|
||||||
size: u64,
|
size: u64,
|
||||||
offset: u64,
|
offset: u64,
|
||||||
user_agent: &str,
|
pb: ProgressBar,
|
||||||
pb: Option<ProgressBar>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut offset = offset;
|
let mut offset = offset;
|
||||||
if let Some(pb) = &pb {
|
pb.inc_length(size);
|
||||||
pb.inc_length(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let range = get_download_range(offset, Some(size));
|
let range = get_download_range(offset, Some(size));
|
||||||
tracing::debug!("Fetching range {}-{}", range.start, range.end);
|
debug!("Fetching range {}-{}", range.start, range.end);
|
||||||
|
|
||||||
let res = http
|
let res = http
|
||||||
.get(format!("{}&range={}-{}", url, range.start, range.end))
|
.get(format!("{}&range={}-{}", url, range.start, range.end))
|
||||||
.header(header::USER_AGENT, user_agent)
|
|
||||||
.header(header::ORIGIN, "https://www.youtube.com")
|
.header(header::ORIGIN, "https://www.youtube.com")
|
||||||
.header(header::REFERER, "https://www.youtube.com/")
|
.header(header::REFERER, "https://www.youtube.com/")
|
||||||
.send()
|
.send()
|
||||||
|
@ -861,19 +258,17 @@ async fn download_chunks_by_param(
|
||||||
|
|
||||||
let clen = res.content_length().unwrap();
|
let clen = res.content_length().unwrap();
|
||||||
|
|
||||||
tracing::debug!("Retrieving chunks...");
|
debug!("Retrieving chunks...");
|
||||||
let mut stream = res.bytes_stream();
|
let mut stream = res.bytes_stream();
|
||||||
while let Some(item) = stream.next().await {
|
while let Some(item) = stream.next().await {
|
||||||
// Retrieve chunk.
|
// Retrieve chunk.
|
||||||
let mut chunk = item?;
|
let mut chunk = item?;
|
||||||
if let Some(pb) = &pb {
|
pb.inc(chunk.len() as u64);
|
||||||
pb.inc(chunk.len() as u64);
|
|
||||||
}
|
|
||||||
file.write_all_buf(&mut chunk).await?;
|
file.write_all_buf(&mut chunk).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += clen;
|
offset += clen;
|
||||||
tracing::debug!("offset inc by {}, new: {}", clen, offset);
|
debug!("offset inc by {}, new: {}", clen, offset);
|
||||||
if offset >= size {
|
if offset >= size {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -884,21 +279,146 @@ async fn download_chunks_by_param(
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct StreamDownload {
|
struct StreamDownload {
|
||||||
file: PathBuf,
|
file: PathBuf,
|
||||||
|
// track_name: String TODO: add for multiple audio languages,
|
||||||
url: String,
|
url: String,
|
||||||
audio_codec: Option<AudioCodec>,
|
audio_codec: Option<AudioCodec>,
|
||||||
video_codec: Option<VideoCodec>,
|
video_codec: Option<VideoCodec>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub async fn download_video(
|
||||||
|
player_data: &VideoPlayer,
|
||||||
|
output_dir: &str,
|
||||||
|
output_fname: Option<String>,
|
||||||
|
output_format: Option<String>,
|
||||||
|
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<StreamDownload> = 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::<Vec<_>>()
|
||||||
|
.await
|
||||||
|
.into_iter()
|
||||||
|
.collect::<core::result::Result<_, _>>()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pb.finish_and_clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_streams(
|
async fn download_streams(
|
||||||
downloads: &Vec<StreamDownload>,
|
downloads: &Vec<StreamDownload>,
|
||||||
http: &Client,
|
http: Client,
|
||||||
user_agent: &str,
|
pb: ProgressBar,
|
||||||
pb: Option<ProgressBar>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let n = downloads.len();
|
let n = downloads.len();
|
||||||
|
|
||||||
stream::iter(downloads)
|
stream::iter(downloads)
|
||||||
.map(|d| download_single_file(&d.url, d.file.clone(), http, user_agent, pb.clone()))
|
.map(|d| download_single_file(&d.url, d.file.clone(), http.clone(), pb.clone()))
|
||||||
.buffer_unordered(n)
|
.buffer_unordered(n)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.await
|
.await
|
||||||
|
@ -912,7 +432,6 @@ async fn convert_streams<P: Into<PathBuf>>(
|
||||||
downloads: &[StreamDownload],
|
downloads: &[StreamDownload],
|
||||||
output: P,
|
output: P,
|
||||||
ffmpeg: &str,
|
ffmpeg: &str,
|
||||||
title: &str,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let output_path: PathBuf = output.into();
|
let output_path: PathBuf = output.into();
|
||||||
|
|
||||||
|
@ -932,9 +451,6 @@ async fn convert_streams<P: Into<PathBuf>>(
|
||||||
args.push("-c".into());
|
args.push("-c".into());
|
||||||
args.push("copy".into());
|
args.push("copy".into());
|
||||||
|
|
||||||
args.push("-metadata".into());
|
|
||||||
args.push(format!("title={title}").into());
|
|
||||||
|
|
||||||
args.push(output_path.into());
|
args.push(output_path.into());
|
||||||
|
|
||||||
let res = Command::new(ffmpeg).args(args).output().await?;
|
let res = Command::new(ffmpeg).args(args).output().await?;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, collections::BTreeMap, path::PathBuf};
|
use std::{borrow::Cow, collections::BTreeMap};
|
||||||
|
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
|
|
||||||
|
@ -6,28 +6,18 @@ use reqwest::Url;
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum DownloadError {
|
pub enum DownloadError {
|
||||||
/// RustyPipe error
|
|
||||||
#[error("{0}")]
|
|
||||||
RustyPipe(#[from] rustypipe::error::Error),
|
|
||||||
/// Error from the HTTP client
|
/// Error from the HTTP client
|
||||||
#[error("http error: {0}")]
|
#[error("http error: {0}")]
|
||||||
Http(#[from] reqwest::Error),
|
Http(#[from] reqwest::Error),
|
||||||
/// File IO error
|
/// File IO error
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
/// FFmpeg returned an error
|
|
||||||
#[error("FFmpeg error: {0}")]
|
#[error("FFmpeg error: {0}")]
|
||||||
Ffmpeg(Cow<'static, str>),
|
Ffmpeg(Cow<'static, str>),
|
||||||
/// Error parsing ranges for progressive download
|
|
||||||
#[error("Progressive download error: {0}")]
|
#[error("Progressive download error: {0}")]
|
||||||
Progressive(Cow<'static, str>),
|
Progressive(Cow<'static, str>),
|
||||||
/// Video could not be downloaded because of invalid player data
|
|
||||||
#[error("input error: {0}")]
|
#[error("input error: {0}")]
|
||||||
Input(Cow<'static, str>),
|
Input(Cow<'static, str>),
|
||||||
/// Download target already exists
|
|
||||||
#[error("file {0} already exists")]
|
|
||||||
Exists(PathBuf),
|
|
||||||
/// Other error
|
|
||||||
#[error("error: {0}")]
|
#[error("error: {0}")]
|
||||||
Other(Cow<'static, str>),
|
Other(Cow<'static, str>),
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,9 +16,7 @@ use crate::{
|
||||||
util::{self, timeago, ProtoBuilder},
|
util::{self, timeago, ProtoBuilder},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext};
|
||||||
response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -203,13 +201,16 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
|
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||||
let visitor_data = self
|
let visitor_data = self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
.or_else(|| vdata.map(str::to_owned));
|
||||||
|
|
||||||
let channel_data = map_channel(
|
let channel_data = map_channel(
|
||||||
MapChannelData {
|
MapChannelData {
|
||||||
|
@ -220,11 +221,12 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
has_shorts: content.has_shorts,
|
has_shorts: content.has_shorts,
|
||||||
has_live: content.has_live,
|
has_live: content.has_live,
|
||||||
},
|
},
|
||||||
ctx,
|
id,
|
||||||
|
lang,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
|
let mut mapper = response::YouTubeListMapper::<VideoItem>::with_channel(
|
||||||
ctx.lang,
|
lang,
|
||||||
&channel_data.c,
|
&channel_data.c,
|
||||||
channel_data.warnings,
|
channel_data.warnings,
|
||||||
);
|
);
|
||||||
|
@ -247,13 +249,16 @@ impl MapResponse<Channel<Paginator<VideoItem>>> for response::Channel {
|
||||||
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
) -> Result<MapResult<Channel<Paginator<PlaylistItem>>>, ExtractionError> {
|
||||||
let content = map_channel_content(ctx.id, self.contents, self.alerts)?;
|
let content = map_channel_content(id, self.contents, self.alerts)?;
|
||||||
let visitor_data = self
|
let visitor_data = self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
.or_else(|| vdata.map(str::to_owned));
|
||||||
|
|
||||||
let channel_data = map_channel(
|
let channel_data = map_channel(
|
||||||
MapChannelData {
|
MapChannelData {
|
||||||
|
@ -264,11 +269,12 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
has_shorts: content.has_shorts,
|
has_shorts: content.has_shorts,
|
||||||
has_live: content.has_live,
|
has_live: content.has_live,
|
||||||
},
|
},
|
||||||
ctx,
|
id,
|
||||||
|
lang,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
|
let mut mapper = response::YouTubeListMapper::<PlaylistItem>::with_channel(
|
||||||
ctx.lang,
|
lang,
|
||||||
&channel_data.c,
|
&channel_data.c,
|
||||||
channel_data.warnings,
|
channel_data.warnings,
|
||||||
);
|
);
|
||||||
|
@ -283,7 +289,13 @@ impl MapResponse<Channel<Paginator<PlaylistItem>>> for response::Channel {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
_lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_visitor_data: Option<&str>,
|
||||||
|
) -> Result<MapResult<ChannelInfo>, ExtractionError> {
|
||||||
// Channel info is always fetched in English. There is no localized data there
|
// Channel info is always fetched in English. There is no localized data there
|
||||||
// and it allows parsing the country name.
|
// and it allows parsing the country name.
|
||||||
let lang = Language::En;
|
let lang = Language::En;
|
||||||
|
@ -297,7 +309,7 @@ impl MapResponse<ChannelInfo> for response::ChannelAbout {
|
||||||
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
|
.ok_or(ExtractionError::InvalidData("no received endpoint".into()))?,
|
||||||
response::ChannelAbout::Content { contents } => {
|
response::ChannelAbout::Content { contents } => {
|
||||||
// Handle errors (e.g. age restriction) when regular channel content was returned
|
// Handle errors (e.g. age restriction) when regular channel content was returned
|
||||||
map_channel_content(ctx.id, contents, None)?;
|
map_channel_content(id, contents, None)?;
|
||||||
return Err(ExtractionError::InvalidData(
|
return Err(ExtractionError::InvalidData(
|
||||||
"could not extract aboutData".into(),
|
"could not extract aboutData".into(),
|
||||||
));
|
));
|
||||||
|
@ -376,35 +388,36 @@ struct MapChannelData {
|
||||||
|
|
||||||
fn map_channel(
|
fn map_channel(
|
||||||
d: MapChannelData,
|
d: MapChannelData,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
) -> Result<MapResult<Channel<()>>, ExtractionError> {
|
) -> Result<MapResult<Channel<()>>, ExtractionError> {
|
||||||
let header = d.header.ok_or_else(|| ExtractionError::NotFound {
|
let header = d.header.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no header".into(),
|
msg: "no header".into(),
|
||||||
})?;
|
})?;
|
||||||
let metadata = d
|
let metadata = d
|
||||||
.metadata
|
.metadata
|
||||||
.ok_or_else(|| ExtractionError::NotFound {
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no metadata".into(),
|
msg: "no metadata".into(),
|
||||||
})?
|
})?
|
||||||
.channel_metadata_renderer;
|
.channel_metadata_renderer;
|
||||||
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
|
let microformat = d.microformat.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no microformat".into(),
|
msg: "no microformat".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if metadata.external_id != ctx.id {
|
if metadata.external_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"got wrong channel id {}, expected {}",
|
"got wrong channel id {}, expected {}",
|
||||||
metadata.external_id, ctx.id
|
metadata.external_id, id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let vanity_url = metadata
|
let vanity_url = metadata
|
||||||
.vanity_channel_url
|
.vanity_channel_url
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|url| map_vanity_url(url, ctx.id));
|
.and_then(|url| map_vanity_url(url, id));
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
@ -412,9 +425,9 @@ fn map_channel(
|
||||||
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
|
response::channel::Header::C4TabbedHeaderRenderer(header) => Channel {
|
||||||
id: metadata.external_id,
|
id: metadata.external_id,
|
||||||
name: metadata.title,
|
name: metadata.title,
|
||||||
subscriber_count: header.subscriber_count_text.and_then(|txt| {
|
subscriber_count: header
|
||||||
util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
|
.subscriber_count_text
|
||||||
}),
|
.and_then(|txt| util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)),
|
||||||
avatar: header.avatar.into(),
|
avatar: header.avatar.into(),
|
||||||
verification: header.badges.into(),
|
verification: header.badges.into(),
|
||||||
description: metadata.description,
|
description: metadata.description,
|
||||||
|
@ -445,7 +458,7 @@ fn map_channel(
|
||||||
name: metadata.title,
|
name: metadata.title,
|
||||||
subscriber_count: hdata.as_ref().and_then(|hdata| {
|
subscriber_count: hdata.as_ref().and_then(|hdata| {
|
||||||
hdata.0.as_ref().and_then(|txt| {
|
hdata.0.as_ref().and_then(|txt| {
|
||||||
util::parse_large_numstr_or_warn(txt, ctx.lang, &mut warnings)
|
util::parse_large_numstr_or_warn(txt, lang, &mut warnings)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
|
avatar: hdata.map(|hdata| hdata.1.into()).unwrap_or_default(),
|
||||||
|
@ -474,7 +487,7 @@ fn map_channel(
|
||||||
md_rows.first().and_then(|md| md.metadata_parts.get(1))
|
md_rows.first().and_then(|md| md.metadata_parts.get(1))
|
||||||
};
|
};
|
||||||
let subscriber_count = sub_part.and_then(|t| {
|
let subscriber_count = sub_part.and_then(|t| {
|
||||||
util::parse_large_numstr_or_warn::<u64>(&t.text, ctx.lang, &mut warnings)
|
util::parse_large_numstr_or_warn::<u64>(&t.text, lang, &mut warnings)
|
||||||
});
|
});
|
||||||
|
|
||||||
Channel {
|
Channel {
|
||||||
|
@ -684,10 +697,10 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapRespCtx, MapResponse},
|
client::{response, MapResponse},
|
||||||
error::{ExtractionError, UnavailabilityReason},
|
error::{ExtractionError, UnavailabilityReason},
|
||||||
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
model::{paginator::Paginator, Channel, ChannelInfo, PlaylistItem, VideoItem},
|
||||||
param::{ChannelOrder, ChannelVideoTab},
|
param::{ChannelOrder, ChannelVideoTab, Language},
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
@ -715,7 +728,7 @@ mod tests {
|
||||||
let channel: response::Channel =
|
let channel: response::Channel =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Channel<Paginator<VideoItem>>> =
|
let map_res: MapResult<Channel<Paginator<VideoItem>>> =
|
||||||
channel.map_response(&MapRespCtx::test(id)).unwrap();
|
channel.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -742,7 +755,7 @@ mod tests {
|
||||||
let channel: response::Channel =
|
let channel: response::Channel =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> =
|
let res: Result<MapResult<Channel<Paginator<VideoItem>>>, ExtractionError> =
|
||||||
channel.map_response(&MapRespCtx::test("UCbfnHqxXs_K3kvaH-WlNlig"));
|
channel.map_response("UCbfnHqxXs_K3kvaH-WlNlig", Language::En, None, None);
|
||||||
if let Err(ExtractionError::Unavailable { reason, msg }) = res {
|
if let Err(ExtractionError::Unavailable { reason, msg }) = res {
|
||||||
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
assert_eq!(reason, UnavailabilityReason::AgeRestricted);
|
||||||
assert!(msg.starts_with("Laphroaig Whisky: "));
|
assert!(msg.starts_with("Laphroaig Whisky: "));
|
||||||
|
@ -759,7 +772,7 @@ mod tests {
|
||||||
let channel: response::Channel =
|
let channel: response::Channel =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel
|
let map_res: MapResult<Channel<Paginator<PlaylistItem>>> = channel
|
||||||
.map_response(&MapRespCtx::test("UC2DjFE7Xf11URZqWBigcVOQ"))
|
.map_response("UC2DjFE7Xf11URZqWBigcVOQ", Language::En, None, None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -778,7 +791,7 @@ mod tests {
|
||||||
let channel: response::ChannelAbout =
|
let channel: response::ChannelAbout =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<ChannelInfo> = channel
|
let map_res: MapResult<ChannelInfo> = channel
|
||||||
.map_response(&MapRespCtx::test("UC2DjFE7Xf11U-RZqWBigcVOQ"))
|
.map_response("UC2DjFE7Xf11U-RZqWBigcVOQ", Language::En, None, None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
|
|
@ -225,7 +225,6 @@ struct RustyPipeRef {
|
||||||
n_http_retries: u32,
|
n_http_retries: u32,
|
||||||
cache: CacheHolder,
|
cache: CacheHolder,
|
||||||
default_opts: RustyPipeOpts,
|
default_opts: RustyPipeOpts,
|
||||||
user_agent: Cow<'static, str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -433,7 +432,7 @@ impl Default for RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeBuilder {
|
impl RustyPipeBuilder {
|
||||||
/// Create a new [`RustyPipeBuilder`].
|
/// Return a new `RustyPipeBuilder`.
|
||||||
///
|
///
|
||||||
/// This is the same as [`RustyPipe::builder`]
|
/// This is the same as [`RustyPipe::builder`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
@ -449,20 +448,15 @@ impl RustyPipeBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new, configured [`RustyPipe`] instance.
|
/// Return a new, configured RustyPipe instance.
|
||||||
pub fn build(self) -> Result<RustyPipe, Error> {
|
pub fn build(self) -> Result<RustyPipe, Error> {
|
||||||
self.build_with_client(ClientBuilder::new())
|
self.build_with_client(ClientBuilder::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new, configured RustyPipe instance using a Reqwest client builder.
|
/// Return a new, configured RustyPipe instance using a Reqwest client builder.
|
||||||
pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
|
pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
|
||||||
let user_agent = self
|
|
||||||
.user_agent
|
|
||||||
.map(Cow::Owned)
|
|
||||||
.unwrap_or(Cow::Borrowed(DEFAULT_UA));
|
|
||||||
|
|
||||||
client_builder = client_builder
|
client_builder = client_builder
|
||||||
.user_agent(user_agent.as_ref())
|
.user_agent(self.user_agent.unwrap_or_else(|| DEFAULT_UA.to_owned()))
|
||||||
.gzip(true)
|
.gzip(true)
|
||||||
.brotli(true)
|
.brotli(true)
|
||||||
.redirect(reqwest::redirect::Policy::none());
|
.redirect(reqwest::redirect::Policy::none());
|
||||||
|
@ -509,7 +503,6 @@ impl RustyPipeBuilder {
|
||||||
deobf: RwLock::new(cdata.deobf),
|
deobf: RwLock::new(cdata.deobf),
|
||||||
},
|
},
|
||||||
default_opts: self.default_opts,
|
default_opts: self.default_opts,
|
||||||
user_agent,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -717,14 +710,6 @@ 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.
|
/// Execute the given http request.
|
||||||
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
|
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
|
||||||
let mut last_resp = None;
|
let mut last_resp = None;
|
||||||
|
@ -978,14 +963,7 @@ impl RustyPipe {
|
||||||
/// visitor data is extracted from the html page.
|
/// visitor data is extracted from the html page.
|
||||||
async fn get_visitor_data(&self) -> Result<String, Error> {
|
async fn get_visitor_data(&self) -> Result<String, Error> {
|
||||||
tracing::debug!("getting YT visitor data");
|
tracing::debug!("getting YT visitor data");
|
||||||
let resp = self
|
let resp = self.inner.http.get(YOUTUBE_MUSIC_HOME_URL).send().await?;
|
||||||
.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
|
let vdata = resp
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -994,10 +972,7 @@ impl RustyPipe {
|
||||||
.find_map(|c| {
|
.find_map(|c| {
|
||||||
if let Ok(cookie) = c.to_str() {
|
if let Ok(cookie) = c.to_str() {
|
||||||
if let Some(after) = cookie.strip_prefix("__Secure-YEC=") {
|
if let Some(after) = cookie.strip_prefix("__Secure-YEC=") {
|
||||||
return after
|
return after.split_once(';').map(|s| s.0.to_owned());
|
||||||
.split_once(';')
|
|
||||||
.map(|s| s.0.to_owned())
|
|
||||||
.filter(|s| !s.is_empty());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
@ -1090,27 +1065,6 @@ impl RustyPipeQuery {
|
||||||
self
|
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
|
/// Create a new context object, which is included in every request to
|
||||||
/// the YouTube API and contains language, country and device parameters.
|
/// the YouTube API and contains language, country and device parameters.
|
||||||
///
|
///
|
||||||
|
@ -1273,6 +1227,13 @@ impl RustyPipeQuery {
|
||||||
.post(format!(
|
.post(format!(
|
||||||
"{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
"{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"),
|
.header("X-Goog-Api-Format-Version", "2"),
|
||||||
ClientType::Ios => self
|
ClientType::Ios => self
|
||||||
.client
|
.client
|
||||||
|
@ -1281,9 +1242,15 @@ impl RustyPipeQuery {
|
||||||
.post(format!(
|
.post(format!(
|
||||||
"{YOUTUBEI_V1_GAPIS_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
"{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"),
|
.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) {
|
if let Some(vdata) = self.opts.visitor_data.as_deref().or(visitor_data) {
|
||||||
r = r.header("X-Goog-EOM-Visitor-Id", vdata);
|
r = r.header("X-Goog-EOM-Visitor-Id", vdata);
|
||||||
}
|
}
|
||||||
|
@ -1301,7 +1268,9 @@ impl RustyPipeQuery {
|
||||||
async fn yt_request_attempt<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
async fn yt_request_attempt<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||||
&self,
|
&self,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<RequestResult<M>, Error> {
|
) -> Result<RequestResult<M>, Error> {
|
||||||
let response = self
|
let response = self
|
||||||
.client
|
.client
|
||||||
|
@ -1320,7 +1289,7 @@ impl RustyPipeQuery {
|
||||||
|
|
||||||
Err(match status {
|
Err(match status {
|
||||||
StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound {
|
StatusCode::NOT_FOUND => Error::Extraction(ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: error_msg.unwrap_or("404".into()),
|
msg: error_msg.unwrap_or("404".into()),
|
||||||
}),
|
}),
|
||||||
StatusCode::BAD_REQUEST => {
|
StatusCode::BAD_REQUEST => {
|
||||||
|
@ -1330,7 +1299,12 @@ impl RustyPipeQuery {
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
match serde_json::from_str::<R>(&body) {
|
match serde_json::from_str::<R>(&body) {
|
||||||
Ok(deserialized) => match deserialized.map_response(ctx) {
|
Ok(deserialized) => match deserialized.map_response(
|
||||||
|
id,
|
||||||
|
self.opts.lang,
|
||||||
|
deobf,
|
||||||
|
self.opts.visitor_data.as_deref().or(visitor_data),
|
||||||
|
) {
|
||||||
Ok(mapres) => Ok(mapres),
|
Ok(mapres) => Ok(mapres),
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
},
|
},
|
||||||
|
@ -1346,11 +1320,15 @@ impl RustyPipeQuery {
|
||||||
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
async fn yt_request<R: DeserializeOwned + MapResponse<M> + Debug, M>(
|
||||||
&self,
|
&self,
|
||||||
request: &Request,
|
request: &Request,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
|
deobf: Option<&DeobfData>,
|
||||||
) -> Result<RequestResult<M>, Error> {
|
) -> Result<RequestResult<M>, Error> {
|
||||||
let mut last_resp = None;
|
let mut last_resp = None;
|
||||||
for n in 0..=self.client.inner.n_http_retries {
|
for n in 0..=self.client.inner.n_http_retries {
|
||||||
let resp = self.yt_request_attempt::<R, M>(request, ctx).await?;
|
let resp = self
|
||||||
|
.yt_request_attempt::<R, M>(request, id, visitor_data, deobf)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let err = match &resp.res {
|
let err = match &resp.res {
|
||||||
Ok(_) => return Ok(resp),
|
Ok(_) => return Ok(resp),
|
||||||
|
@ -1416,15 +1394,9 @@ impl RustyPipeQuery {
|
||||||
.json(body)
|
.json(body)
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
let ctx = MapRespCtx {
|
let req_res = self
|
||||||
id,
|
.yt_request::<R, M>(&request, id, visitor_data, deobf)
|
||||||
lang: self.opts.lang,
|
.await?;
|
||||||
deobf,
|
|
||||||
visitor_data,
|
|
||||||
client_type: ctype,
|
|
||||||
};
|
|
||||||
|
|
||||||
let req_res = self.yt_request::<R, M>(&request, &ctx).await?;
|
|
||||||
|
|
||||||
// Uncomment to debug response text
|
// Uncomment to debug response text
|
||||||
// println!("{}", &req_res.body);
|
// println!("{}", &req_res.body);
|
||||||
|
@ -1581,28 +1553,6 @@ impl AsRef<RustyPipeQuery> 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
|
/// Implement this for YouTube API response structs that need to be mapped to
|
||||||
/// RustyPipe models.
|
/// RustyPipe models.
|
||||||
trait MapResponse<T> {
|
trait MapResponse<T> {
|
||||||
|
@ -1619,7 +1569,13 @@ trait MapResponse<T> {
|
||||||
/// - `lang`: Language of the request. Used for mapping localized information like dates.
|
/// - `lang`: Language of the request. Used for mapping localized information like dates.
|
||||||
/// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method)
|
/// - `deobf`: Deobfuscator (if passed to the `execute_request_deobf` method)
|
||||||
/// - `visitor_data`: Visitor data option of the client
|
/// - `visitor_data`: Visitor data option of the client
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<T>, ExtractionError>;
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
deobf: Option<&DeobfData>,
|
||||||
|
visitor_data: Option<&str>,
|
||||||
|
) -> Result<MapResult<T>, ExtractionError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_country(country: Country) -> Country {
|
fn validate_country(country: Country) -> Country {
|
||||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
|
response::{self, music_item::MusicListMapper, url_endpoint::PageType},
|
||||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
ClientType, MapResponse, QBrowse, RustyPipeQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
|
@ -92,8 +92,14 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<MusicArtist> for response::MusicArtist {
|
impl MapResponse<MusicArtist> for response::MusicArtist {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
fn map_response(
|
||||||
let mapped = map_artist_page(self, ctx, false)?;
|
self,
|
||||||
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<MapResult<MusicArtist>, ExtractionError> {
|
||||||
|
let mapped = map_artist_page(self, id, lang, false)?;
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: mapped.c.0,
|
c: mapped.c.0,
|
||||||
warnings: mapped.warnings,
|
warnings: mapped.warnings,
|
||||||
|
@ -104,15 +110,19 @@ impl MapResponse<MusicArtist> for response::MusicArtist {
|
||||||
impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
|
impl MapResponse<(MusicArtist, bool)> for response::MusicArtist {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
||||||
map_artist_page(self, ctx, true)
|
map_artist_page(self, id, lang, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_artist_page(
|
fn map_artist_page(
|
||||||
res: response::MusicArtist,
|
res: response::MusicArtist,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
skip_extendables: bool,
|
skip_extendables: bool,
|
||||||
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
) -> Result<MapResult<(MusicArtist, bool)>, ExtractionError> {
|
||||||
// dbg!(&res);
|
// dbg!(&res);
|
||||||
|
@ -128,7 +138,7 @@ fn map_artist_page(
|
||||||
.and_then(|pb| util::string_from_pb(pb, 3));
|
.and_then(|pb| util::string_from_pb(pb, 3));
|
||||||
|
|
||||||
if let Some(share_channel_id) = share_channel_id {
|
if let Some(share_channel_id) = share_channel_id {
|
||||||
if share_channel_id != ctx.id {
|
if share_channel_id != id {
|
||||||
return Err(ExtractionError::Redirect(share_channel_id));
|
return Err(ExtractionError::Redirect(share_channel_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,9 +155,9 @@ fn map_artist_page(
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::with_artist(
|
let mut mapper = MusicListMapper::with_artist(
|
||||||
ctx.lang,
|
lang,
|
||||||
ArtistId {
|
ArtistId {
|
||||||
id: Some(ctx.id.to_owned()),
|
id: Some(id.to_owned()),
|
||||||
name: header.title.clone(),
|
name: header.title.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -254,7 +264,7 @@ fn map_artist_page(
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: (
|
c: (
|
||||||
MusicArtist {
|
MusicArtist {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
name: header.title,
|
name: header.title,
|
||||||
header_image: header.thumbnail.into(),
|
header_image: header.thumbnail.into(),
|
||||||
description: header.description,
|
description: header.description,
|
||||||
|
@ -262,7 +272,7 @@ fn map_artist_page(
|
||||||
subscriber_count: header.subscription_button.and_then(|btn| {
|
subscriber_count: header.subscription_button.and_then(|btn| {
|
||||||
util::parse_large_numstr_or_warn(
|
util::parse_large_numstr_or_warn(
|
||||||
&btn.subscribe_button_renderer.subscriber_count_text,
|
&btn.subscribe_button_renderer.subscriber_count_text,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut mapped.warnings,
|
&mut mapped.warnings,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
@ -283,13 +293,16 @@ fn map_artist_page(
|
||||||
impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
|
) -> Result<MapResult<Vec<AlbumItem>>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
let Some(header) = self.header else {
|
let Some(header) = self.header else {
|
||||||
return Err(ExtractionError::NotFound {
|
return Err(ExtractionError::NotFound {
|
||||||
id: ctx.id.into(),
|
id: id.into(),
|
||||||
msg: "no header".into(),
|
msg: "no header".into(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -307,9 +320,9 @@ impl MapResponse<Vec<AlbumItem>> for response::MusicArtistAlbums {
|
||||||
.contents;
|
.contents;
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::with_artist(
|
let mut mapper = MusicListMapper::with_artist(
|
||||||
ctx.lang,
|
lang,
|
||||||
ArtistId {
|
ArtistId {
|
||||||
id: Some(ctx.id.to_owned()),
|
id: Some(id.to_owned()),
|
||||||
name: header.music_header_renderer.title,
|
name: header.music_header_renderer.title,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -334,7 +347,7 @@ mod tests {
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::util::tests::TESTFILES;
|
use crate::{param::Language, util::tests::TESTFILES};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -356,7 +369,7 @@ mod tests {
|
||||||
let resp: response::MusicArtist =
|
let resp: response::MusicArtist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<(MusicArtist, bool)> =
|
let map_res: MapResult<(MusicArtist, bool)> =
|
||||||
resp.map_response(&MapRespCtx::test(id)).unwrap();
|
resp.map_response(id, Language::En, None, None).unwrap();
|
||||||
let (mut artist, can_fetch_more) = map_res.c;
|
let (mut artist, can_fetch_more) = map_res.c;
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -371,7 +384,7 @@ mod tests {
|
||||||
let resp: response::MusicArtistAlbums =
|
let resp: response::MusicArtistAlbums =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let mut map_res: MapResult<Vec<AlbumItem>> =
|
let mut map_res: MapResult<Vec<AlbumItem>> =
|
||||||
resp.map_response(&MapRespCtx::test(id)).unwrap();
|
resp.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -392,7 +405,7 @@ mod tests {
|
||||||
let artist: response::MusicArtist =
|
let artist: response::MusicArtist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicArtist> = artist
|
let map_res: MapResult<MusicArtist> = artist
|
||||||
.map_response(&MapRespCtx::test("UClmXPfaYhXOYsNn_QUyheWQ"))
|
.map_response("UClmXPfaYhXOYsNn_QUyheWQ", Language::En, None, None)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -411,7 +424,7 @@ mod tests {
|
||||||
let artist: response::MusicArtist =
|
let artist: response::MusicArtist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let res: Result<MapResult<MusicArtist>, ExtractionError> =
|
let res: Result<MapResult<MusicArtist>, ExtractionError> =
|
||||||
artist.map_response(&MapRespCtx::test("UCLkAepWjdylmXSltofFvsYQ"));
|
artist.map_response("UCLkAepWjdylmXSltofFvsYQ", Language::En, None, None);
|
||||||
let e = res.unwrap_err();
|
let e = res.unwrap_err();
|
||||||
|
|
||||||
match e {
|
match e {
|
||||||
|
|
|
@ -11,7 +11,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
|
response::{self, music_item::MusicListMapper, url_endpoint::MusicPageType},
|
||||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -56,7 +56,13 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<MusicCharts> for response::MusicCharts {
|
impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicCharts>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<crate::serializer::MapResult<MusicCharts>, crate::error::ExtractionError> {
|
||||||
let countries = self
|
let countries = self
|
||||||
.framework_updates
|
.framework_updates
|
||||||
.map(|fwu| {
|
.map(|fwu| {
|
||||||
|
@ -71,9 +77,9 @@ impl MapResponse<MusicCharts> for response::MusicCharts {
|
||||||
let mut top_playlist_id = None;
|
let mut top_playlist_id = None;
|
||||||
let mut trending_playlist_id = None;
|
let mut trending_playlist_id = None;
|
||||||
|
|
||||||
let mut mapper_top = MusicListMapper::new(ctx.lang);
|
let mut mapper_top = MusicListMapper::new(lang);
|
||||||
let mut mapper_trending = MusicListMapper::new(ctx.lang);
|
let mut mapper_trending = MusicListMapper::new(lang);
|
||||||
let mut mapper_other = MusicListMapper::new(ctx.lang);
|
let mut mapper_other = MusicListMapper::new(lang);
|
||||||
|
|
||||||
self.contents
|
self.contents
|
||||||
.single_column_browse_results_renderer
|
.single_column_browse_results_renderer
|
||||||
|
@ -145,6 +151,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::param::Language;
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::default("global")]
|
#[case::default("global")]
|
||||||
|
@ -156,7 +163,8 @@ mod tests {
|
||||||
|
|
||||||
let charts: response::MusicCharts =
|
let charts: response::MusicCharts =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicCharts> = charts.map_response(&MapRespCtx::test("")).unwrap();
|
let map_res: MapResult<MusicCharts> =
|
||||||
|
charts.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
paginator::{ContinuationEndpoint, Paginator},
|
paginator::{ContinuationEndpoint, Paginator},
|
||||||
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
|
ArtistId, Lyrics, MusicRelated, TrackDetails, TrackItem,
|
||||||
},
|
},
|
||||||
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,7 +17,7 @@ use super::{
|
||||||
self,
|
self,
|
||||||
music_item::{map_queue_item, MusicListMapper},
|
music_item::{map_queue_item, MusicListMapper},
|
||||||
},
|
},
|
||||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, QBrowse, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -169,7 +170,10 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<TrackDetails> for response::MusicDetails {
|
impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
) -> Result<MapResult<TrackDetails>, ExtractionError> {
|
||||||
let tabs = self
|
let tabs = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -207,7 +211,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = content.ok_or_else(|| ExtractionError::NotFound {
|
let content = content.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no content".into(),
|
msg: "no content".into(),
|
||||||
})?;
|
})?;
|
||||||
let track_item = content
|
let track_item = content
|
||||||
|
@ -221,7 +225,7 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
response::music_item::PlaylistPanelVideo::None => None,
|
response::music_item::PlaylistPanelVideo::None => None,
|
||||||
})
|
})
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
|
.ok_or(ExtractionError::InvalidData(Cow::Borrowed("no video item")))?;
|
||||||
let mut track = map_queue_item(track_item, ctx.lang);
|
let mut track = map_queue_item(track_item, lang);
|
||||||
|
|
||||||
let mut warnings = content.contents.warnings;
|
let mut warnings = content.contents.warnings;
|
||||||
warnings.append(&mut track.warnings);
|
warnings.append(&mut track.warnings);
|
||||||
|
@ -240,7 +244,10 @@ impl MapResponse<TrackDetails> for response::MusicDetails {
|
||||||
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<TrackItem>>, ExtractionError> {
|
||||||
let tabs = self
|
let tabs = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -253,7 +260,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|t| t.tab_renderer.content)
|
.find_map(|t| t.tab_renderer.content)
|
||||||
.ok_or_else(|| ExtractionError::NotFound {
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no content".into(),
|
msg: "no content".into(),
|
||||||
})?
|
})?
|
||||||
.music_queue_renderer
|
.music_queue_renderer
|
||||||
|
@ -268,7 +275,7 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|item| match item {
|
.filter_map(|item| match item {
|
||||||
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
|
response::music_item::PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) => {
|
||||||
let mut track = map_queue_item(item, ctx.lang);
|
let mut track = map_queue_item(item, lang);
|
||||||
warnings.append(&mut track.warnings);
|
warnings.append(&mut track.warnings);
|
||||||
Some(track.c)
|
Some(track.c)
|
||||||
}
|
}
|
||||||
|
@ -290,12 +297,18 @@ impl MapResponse<Paginator<TrackItem>> for response::MusicDetails {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<Lyrics> for response::MusicLyrics {
|
impl MapResponse<Lyrics> for response::MusicLyrics {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Lyrics>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
_lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<MapResult<Lyrics>, ExtractionError> {
|
||||||
let lyrics = self
|
let lyrics = self
|
||||||
.contents
|
.contents
|
||||||
.into_res()
|
.into_res()
|
||||||
.map_err(|msg| ExtractionError::NotFound {
|
.map_err(|msg| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: msg.into(),
|
msg: msg.into(),
|
||||||
})?
|
})?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -315,13 +328,16 @@ impl MapResponse<Lyrics> for response::MusicLyrics {
|
||||||
impl MapResponse<MusicRelated> for response::MusicRelated {
|
impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicRelated>, ExtractionError> {
|
) -> Result<MapResult<MusicRelated>, ExtractionError> {
|
||||||
let contents = self
|
let contents = self
|
||||||
.contents
|
.contents
|
||||||
.into_res()
|
.into_res()
|
||||||
.map_err(|msg| ExtractionError::NotFound {
|
.map_err(|msg| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: msg.into(),
|
msg: msg.into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -346,10 +362,10 @@ impl MapResponse<MusicRelated> for response::MusicRelated {
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
let mut mapper_tracks = MusicListMapper::new(ctx.lang);
|
let mut mapper_tracks = MusicListMapper::new(lang);
|
||||||
let mut mapper = match artist_id {
|
let mut mapper = match artist_id {
|
||||||
Some(artist_id) => MusicListMapper::with_artist(ctx.lang, artist_id),
|
Some(artist_id) => MusicListMapper::with_artist(lang, artist_id),
|
||||||
None => MusicListMapper::new(ctx.lang),
|
None => MusicListMapper::new(lang),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut sections = contents.into_iter();
|
let mut sections = contents.into_iter();
|
||||||
|
@ -396,7 +412,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{model, util::tests::TESTFILES};
|
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::mv("mv", "ZeerrnuLi5E")]
|
#[case::mv("mv", "ZeerrnuLi5E")]
|
||||||
|
@ -408,7 +424,7 @@ mod tests {
|
||||||
let details: response::MusicDetails =
|
let details: response::MusicDetails =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<model::TrackDetails> =
|
let map_res: MapResult<model::TrackDetails> =
|
||||||
details.map_response(&MapRespCtx::test(id)).unwrap();
|
details.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -428,7 +444,7 @@ mod tests {
|
||||||
let radio: response::MusicDetails =
|
let radio: response::MusicDetails =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<TrackItem>> =
|
let map_res: MapResult<Paginator<TrackItem>> =
|
||||||
radio.map_response(&MapRespCtx::test(id)).unwrap();
|
radio.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -445,7 +461,7 @@ mod tests {
|
||||||
|
|
||||||
let lyrics: response::MusicLyrics =
|
let lyrics: response::MusicLyrics =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Lyrics> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
|
let map_res: MapResult<Lyrics> = lyrics.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -462,7 +478,8 @@ mod tests {
|
||||||
|
|
||||||
let lyrics: response::MusicRelated =
|
let lyrics: response::MusicRelated =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicRelated> = lyrics.map_response(&MapRespCtx::test("")).unwrap();
|
let map_res: MapResult<MusicRelated> =
|
||||||
|
lyrics.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint},
|
response::{self, music_item::MusicListMapper, url_endpoint::NavigationEndpoint},
|
||||||
ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
|
ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
|
@ -59,8 +59,11 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
_ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
) -> Result<MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
_lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<crate::serializer::MapResult<Vec<MusicGenreItem>>, ExtractionError> {
|
||||||
let content = self
|
let content = self
|
||||||
.contents
|
.contents
|
||||||
.single_column_browse_results_renderer
|
.single_column_browse_results_renderer
|
||||||
|
@ -108,7 +111,13 @@ impl MapResponse<Vec<MusicGenreItem>> for response::MusicGenres {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<MusicGenre> for response::MusicGenre {
|
impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicGenre>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<crate::serializer::MapResult<MusicGenre>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
let content = self
|
let content = self
|
||||||
|
@ -170,7 +179,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
let mut mapped = mapper.conv_items();
|
let mut mapped = mapper.conv_items();
|
||||||
warnings.append(&mut mapped.warnings);
|
warnings.append(&mut mapped.warnings);
|
||||||
|
@ -185,7 +194,7 @@ impl MapResponse<MusicGenre> for response::MusicGenre {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: MusicGenre {
|
c: MusicGenre {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
name: self.header.music_header_renderer.title,
|
name: self.header.music_header_renderer.title,
|
||||||
sections,
|
sections,
|
||||||
},
|
},
|
||||||
|
@ -202,7 +211,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{model, util::tests::TESTFILES};
|
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn map_music_genres() {
|
fn map_music_genres() {
|
||||||
|
@ -212,7 +221,7 @@ mod tests {
|
||||||
let playlist: response::MusicGenres =
|
let playlist: response::MusicGenres =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Vec<model::MusicGenreItem>> =
|
let map_res: MapResult<Vec<model::MusicGenreItem>> =
|
||||||
playlist.map_response(&MapRespCtx::test("")).unwrap();
|
playlist.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -232,7 +241,7 @@ mod tests {
|
||||||
let playlist: response::MusicGenre =
|
let playlist: response::MusicGenre =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<model::MusicGenre> =
|
let map_res: MapResult<model::MusicGenre> =
|
||||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -4,10 +4,9 @@ use crate::{
|
||||||
client::response::music_item::MusicListMapper,
|
client::response::music_item::MusicListMapper,
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::{traits::FromYtItem, AlbumItem, TrackItem},
|
model::{traits::FromYtItem, AlbumItem, TrackItem},
|
||||||
serializer::MapResult,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery};
|
use super::{response, ClientType, MapResponse, QBrowse, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the new albums that were released on YouTube Music
|
/// Get the new albums that were released on YouTube Music
|
||||||
|
@ -50,7 +49,13 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Vec<T>>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<crate::serializer::MapResult<Vec<T>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.contents
|
.contents
|
||||||
.single_column_browse_results_renderer
|
.single_column_browse_results_renderer
|
||||||
|
@ -68,7 +73,7 @@ impl<T: FromYtItem> MapResponse<Vec<T>> for response::MusicNew {
|
||||||
.grid_renderer
|
.grid_renderer
|
||||||
.items;
|
.items;
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
|
|
||||||
Ok(mapper.conv_items())
|
Ok(mapper.conv_items())
|
||||||
|
@ -83,7 +88,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{serializer::MapResult, util::tests::TESTFILES};
|
use crate::{param::Language, serializer::MapResult, util::tests::TESTFILES};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::default("default")]
|
#[case::default("default")]
|
||||||
|
@ -93,8 +98,9 @@ mod tests {
|
||||||
|
|
||||||
let new_albums: response::MusicNew =
|
let new_albums: response::MusicNew =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Vec<AlbumItem>> =
|
let map_res: MapResult<Vec<AlbumItem>> = new_albums
|
||||||
new_albums.map_response(&MapRespCtx::test("")).unwrap();
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -113,8 +119,9 @@ mod tests {
|
||||||
|
|
||||||
let new_videos: response::MusicNew =
|
let new_videos: response::MusicNew =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Vec<TrackItem>> =
|
let map_res: MapResult<Vec<TrackItem>> = new_videos
|
||||||
new_videos.map_response(&MapRespCtx::test("")).unwrap();
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -17,7 +17,7 @@ use super::{
|
||||||
self,
|
self,
|
||||||
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
|
music_item::{map_album_type, map_artist_id, map_artists, MusicListMapper},
|
||||||
},
|
},
|
||||||
ClientType, MapRespCtx, MapResponse, QBrowse, RustyPipeQuery,
|
ClientType, MapResponse, QBrowse, RustyPipeQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
|
@ -138,7 +138,10 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
) -> Result<MapResult<MusicPlaylist>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
@ -183,15 +186,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
if let Some(playlist_id) = shelf.playlist_id {
|
if let Some(playlist_id) = shelf.playlist_id {
|
||||||
if playlist_id != ctx.id {
|
if playlist_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"got wrong playlist id {}, expected {}",
|
"got wrong playlist id {playlist_id}, expected {id}"
|
||||||
playlist_id, ctx.id
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
mapper.map_response(shelf.contents);
|
mapper.map_response(shelf.contents);
|
||||||
let map_res = mapper.conv_items();
|
let map_res = mapper.conv_items();
|
||||||
|
|
||||||
|
@ -271,7 +273,7 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: MusicPlaylist {
|
c: MusicPlaylist {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
name,
|
name,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
channel,
|
channel,
|
||||||
|
@ -282,14 +284,14 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
track_count,
|
track_count,
|
||||||
map_res.c,
|
map_res.c,
|
||||||
ctoken,
|
ctoken,
|
||||||
ctx.visitor_data.map(str::to_owned),
|
vdata.map(str::to_owned),
|
||||||
ContinuationEndpoint::MusicBrowse,
|
ContinuationEndpoint::MusicBrowse,
|
||||||
),
|
),
|
||||||
related_playlists: Paginator::new_ext(
|
related_playlists: Paginator::new_ext(
|
||||||
None,
|
None,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
related_ctoken,
|
related_ctoken,
|
||||||
ctx.visitor_data.map(str::to_owned),
|
vdata.map(str::to_owned),
|
||||||
ContinuationEndpoint::MusicBrowse,
|
ContinuationEndpoint::MusicBrowse,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -299,7 +301,13 @@ impl MapResponse<MusicPlaylist> for response::MusicPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<MapResult<MusicAlbum>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
let (header, sections) = match self.contents {
|
let (header, sections) = match self.contents {
|
||||||
|
@ -393,7 +401,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
.map(|part| part.to_string())
|
.map(|part| part.to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let album_type = map_album_type(album_type_txt.as_str(), ctx.lang);
|
let album_type = map_album_type(album_type_txt.as_str(), lang);
|
||||||
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
let year = year_txt.and_then(|txt| util::parse_numeric(&txt).ok());
|
||||||
|
|
||||||
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
|
fn map_playlist_id(ep: &NavigationEndpoint) -> Option<String> {
|
||||||
|
@ -440,11 +448,11 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));
|
let artist_id = artist_id.or_else(|| artists.first().and_then(|a| a.id.clone()));
|
||||||
|
|
||||||
let mut mapper = MusicListMapper::with_album(
|
let mut mapper = MusicListMapper::with_album(
|
||||||
ctx.lang,
|
lang,
|
||||||
artists.clone(),
|
artists.clone(),
|
||||||
by_va,
|
by_va,
|
||||||
AlbumId {
|
AlbumId {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
name: header.title.clone(),
|
name: header.title.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -452,7 +460,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
let tracks_res = mapper.conv_items();
|
let tracks_res = mapper.conv_items();
|
||||||
let mut warnings = tracks_res.warnings;
|
let mut warnings = tracks_res.warnings;
|
||||||
|
|
||||||
let mut variants_mapper = MusicListMapper::new(ctx.lang);
|
let mut variants_mapper = MusicListMapper::new(lang);
|
||||||
if let Some(res) = album_variants {
|
if let Some(res) = album_variants {
|
||||||
variants_mapper.map_response(res);
|
variants_mapper.map_response(res);
|
||||||
}
|
}
|
||||||
|
@ -461,7 +469,7 @@ impl MapResponse<MusicAlbum> for response::MusicPlaylist {
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: MusicAlbum {
|
c: MusicAlbum {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
playlist_id,
|
playlist_id,
|
||||||
name: header.title,
|
name: header.title,
|
||||||
cover: header.thumbnail.into(),
|
cover: header.thumbnail.into(),
|
||||||
|
@ -489,7 +497,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{model, util::tests::TESTFILES};
|
use crate::{model, param::Language, util::tests::TESTFILES};
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
||||||
|
@ -504,7 +512,7 @@ mod tests {
|
||||||
let playlist: response::MusicPlaylist =
|
let playlist: response::MusicPlaylist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<model::MusicPlaylist> =
|
let map_res: MapResult<model::MusicPlaylist> =
|
||||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -531,7 +539,7 @@ mod tests {
|
||||||
let playlist: response::MusicPlaylist =
|
let playlist: response::MusicPlaylist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<model::MusicAlbum> =
|
let map_res: MapResult<model::MusicAlbum> =
|
||||||
playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
playlist.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext};
|
use super::{response, ClientType, MapResponse, RustyPipeQuery, YTContext};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -152,7 +152,10 @@ impl RustyPipeQuery {
|
||||||
impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
|
impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
|
) -> Result<MapResult<MusicSearchResult<T>>, ExtractionError> {
|
||||||
// dbg!(&self);
|
// dbg!(&self);
|
||||||
|
|
||||||
|
@ -168,7 +171,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
||||||
|
|
||||||
let mut corrected_query = None;
|
let mut corrected_query = None;
|
||||||
let mut ctoken = None;
|
let mut ctoken = None;
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
|
|
||||||
sections.into_iter().for_each(|section| match section {
|
sections.into_iter().for_each(|section| match section {
|
||||||
response::music_search::ItemSection::MusicShelfRenderer(shelf) => {
|
response::music_search::ItemSection::MusicShelfRenderer(shelf) => {
|
||||||
|
@ -196,7 +199,7 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
||||||
None,
|
None,
|
||||||
map_res.c,
|
map_res.c,
|
||||||
ctoken,
|
ctoken,
|
||||||
ctx.visitor_data.map(str::to_owned),
|
vdata.map(str::to_owned),
|
||||||
ContinuationEndpoint::MusicSearch,
|
ContinuationEndpoint::MusicSearch,
|
||||||
),
|
),
|
||||||
corrected_query,
|
corrected_query,
|
||||||
|
@ -209,9 +212,12 @@ impl<T: FromYtItem> MapResponse<MusicSearchResult<T>> for response::MusicSearch
|
||||||
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
impl MapResponse<MusicSearchSuggestion> for response::MusicSearchSuggestion {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
|
) -> Result<MapResult<MusicSearchSuggestion>, ExtractionError> {
|
||||||
let mut mapper = MusicListMapper::new_search_suggest(ctx.lang);
|
let mut mapper = MusicListMapper::new_search_suggest(lang);
|
||||||
let mut terms = Vec::new();
|
let mut terms = Vec::new();
|
||||||
|
|
||||||
for section in self.contents {
|
for section in self.contents {
|
||||||
|
@ -250,11 +256,12 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapRespCtx, MapResponse},
|
client::{response, MapResponse},
|
||||||
model::{
|
model::{
|
||||||
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
|
AlbumItem, ArtistItem, MusicItem, MusicPlaylistItem, MusicSearchResult,
|
||||||
MusicSearchSuggestion, TrackItem,
|
MusicSearchSuggestion, TrackItem,
|
||||||
},
|
},
|
||||||
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
@ -271,7 +278,7 @@ mod tests {
|
||||||
let search: response::MusicSearch =
|
let search: response::MusicSearch =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchResult<MusicItem>> =
|
let map_res: MapResult<MusicSearchResult<MusicItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -294,7 +301,7 @@ mod tests {
|
||||||
let search: response::MusicSearch =
|
let search: response::MusicSearch =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchResult<TrackItem>> =
|
let map_res: MapResult<MusicSearchResult<TrackItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -313,7 +320,7 @@ mod tests {
|
||||||
let search: response::MusicSearch =
|
let search: response::MusicSearch =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchResult<AlbumItem>> =
|
let map_res: MapResult<MusicSearchResult<AlbumItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -332,7 +339,7 @@ mod tests {
|
||||||
let search: response::MusicSearch =
|
let search: response::MusicSearch =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchResult<ArtistItem>> =
|
let map_res: MapResult<MusicSearchResult<ArtistItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -353,7 +360,7 @@ mod tests {
|
||||||
let search: response::MusicSearch =
|
let search: response::MusicSearch =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> =
|
let map_res: MapResult<MusicSearchResult<MusicPlaylistItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -373,8 +380,9 @@ mod tests {
|
||||||
|
|
||||||
let suggestion: response::MusicSearchSuggestion =
|
let suggestion: response::MusicSearchSuggestion =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<MusicSearchSuggestion> =
|
let map_res: MapResult<MusicSearchSuggestion> = suggestion
|
||||||
suggestion.map_response(&MapRespCtx::test("")).unwrap();
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -10,7 +10,7 @@ use crate::model::{
|
||||||
use crate::serializer::MapResult;
|
use crate::serializer::MapResult;
|
||||||
|
|
||||||
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
|
use super::response::music_item::{map_queue_item, MusicListMapper, PlaylistPanelVideo};
|
||||||
use super::{response, ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery};
|
use super::{response, ClientType, MapResponse, QContinuation, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get more YouTube items from the given continuation token and endpoint
|
/// Get more YouTube items from the given continuation token and endpoint
|
||||||
|
@ -103,7 +103,10 @@ fn map_ytm_paginator<T: FromYtItem>(
|
||||||
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<YouTubeItem>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.on_response_received_actions
|
.on_response_received_actions
|
||||||
|
@ -123,7 +126,7 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
@ -136,9 +139,12 @@ impl MapResponse<Paginator<YouTubeItem>> for response::Continuation {
|
||||||
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<MusicItem>>, ExtractionError> {
|
||||||
let mut mapper = MusicListMapper::new(ctx.lang);
|
let mut mapper = MusicListMapper::new(lang);
|
||||||
let mut continuations = Vec::new();
|
let mut continuations = Vec::new();
|
||||||
|
|
||||||
match self.continuation_contents {
|
match self.continuation_contents {
|
||||||
|
@ -167,7 +173,7 @@ impl MapResponse<Paginator<MusicItem>> for response::MusicContinuation {
|
||||||
mapper.add_warnings(&mut panel.contents.warnings);
|
mapper.add_warnings(&mut panel.contents.warnings);
|
||||||
panel.contents.c.into_iter().for_each(|item| {
|
panel.contents.c.into_iter().for_each(|item| {
|
||||||
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
|
if let PlaylistPanelVideo::PlaylistPanelVideoRenderer(item) = item {
|
||||||
let mut track = map_queue_item(item, ctx.lang);
|
let mut track = map_queue_item(item, lang);
|
||||||
mapper.add_item(MusicItem::Track(track.c));
|
mapper.add_item(MusicItem::Track(track.c));
|
||||||
mapper.add_warnings(&mut track.warnings);
|
mapper.add_warnings(&mut track.warnings);
|
||||||
}
|
}
|
||||||
|
@ -350,6 +356,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
|
model::{MusicPlaylistItem, PlaylistItem, TrackItem, VideoItem},
|
||||||
|
param::Language,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -364,7 +371,7 @@ mod tests {
|
||||||
let items: response::Continuation =
|
let items: response::Continuation =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
items.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -386,7 +393,7 @@ mod tests {
|
||||||
let items: response::Continuation =
|
let items: response::Continuation =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
items.map_response("", Language::En, None, None).unwrap();
|
||||||
let paginator: Paginator<VideoItem> =
|
let paginator: Paginator<VideoItem> =
|
||||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||||
|
|
||||||
|
@ -409,7 +416,7 @@ mod tests {
|
||||||
let items: response::Continuation =
|
let items: response::Continuation =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<YouTubeItem>> =
|
let map_res: MapResult<Paginator<YouTubeItem>> =
|
||||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
items.map_response("", Language::En, None, None).unwrap();
|
||||||
let paginator: Paginator<PlaylistItem> =
|
let paginator: Paginator<PlaylistItem> =
|
||||||
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
map_yt_paginator(map_res.c, None, ContinuationEndpoint::Browse);
|
||||||
|
|
||||||
|
@ -432,7 +439,7 @@ mod tests {
|
||||||
let items: response::MusicContinuation =
|
let items: response::MusicContinuation =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<MusicItem>> =
|
let map_res: MapResult<Paginator<MusicItem>> =
|
||||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
items.map_response("", Language::En, None, None).unwrap();
|
||||||
let paginator: Paginator<TrackItem> =
|
let paginator: Paginator<TrackItem> =
|
||||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||||
|
|
||||||
|
@ -453,7 +460,7 @@ mod tests {
|
||||||
let items: response::MusicContinuation =
|
let items: response::MusicContinuation =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<MusicItem>> =
|
let map_res: MapResult<Paginator<MusicItem>> =
|
||||||
items.map_response(&MapRespCtx::test("")).unwrap();
|
items.map_response("", Language::En, None, None).unwrap();
|
||||||
let paginator: Paginator<MusicPlaylistItem> =
|
let paginator: Paginator<MusicPlaylistItem> =
|
||||||
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
map_ytm_paginator(map_res.c, None, ContinuationEndpoint::MusicBrowse);
|
||||||
|
|
||||||
|
|
|
@ -16,15 +16,13 @@ use crate::{
|
||||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset,
|
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset,
|
||||||
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||||
},
|
},
|
||||||
|
param::Language,
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{
|
response::{self, player},
|
||||||
self,
|
ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
||||||
player::{self, Format},
|
|
||||||
},
|
|
||||||
ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -64,52 +62,33 @@ struct QContentPlaybackContext<'a> {
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get YouTube player data (video/audio streams + basic metadata)
|
/// Get YouTube player data (video/audio streams + basic metadata)
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
pub async fn player<S: AsRef<str> + Debug>(&self, video_id: S) -> Result<VideoPlayer, Error> {
|
||||||
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<S: AsRef<str> + Debug>(
|
|
||||||
&self,
|
|
||||||
video_id: S,
|
|
||||||
clients: &[ClientType],
|
|
||||||
) -> Result<VideoPlayer, Error> {
|
|
||||||
let video_id = video_id.as_ref();
|
let video_id = video_id.as_ref();
|
||||||
let mut last_e = Error::Other("no clients".into());
|
let desktop_res = self.player_from_client(video_id, ClientType::Desktop).await;
|
||||||
// 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;
|
|
||||||
|
|
||||||
for client in clients {
|
match desktop_res {
|
||||||
let res = self.player_from_client(video_id, *client).await;
|
Ok(res) => Ok(res),
|
||||||
match res {
|
Err(Error::Extraction(e)) => {
|
||||||
Ok(res) => return Ok(res),
|
if e.switch_client() {
|
||||||
Err(Error::Extraction(e)) => {
|
let tv_res = self
|
||||||
if e.switch_client() {
|
.player_from_client(video_id, ClientType::TvHtml5Embed)
|
||||||
if let ExtractionError::Unavailable {
|
.await;
|
||||||
reason: UnavailabilityReason::AgeRestricted,
|
|
||||||
msg,
|
match tv_res {
|
||||||
} = &e
|
// Output desktop client error if the tv client is unsupported
|
||||||
{
|
Err(Error::Extraction(ExtractionError::Unavailable {
|
||||||
age_restricted_e =
|
reason: UnavailabilityReason::UnsupportedClient,
|
||||||
Some(Error::Extraction(ExtractionError::Unavailable {
|
..
|
||||||
reason: UnavailabilityReason::AgeRestricted,
|
})) => Err(Error::Extraction(e)),
|
||||||
msg: msg.to_owned(),
|
_ => tv_res,
|
||||||
}));
|
|
||||||
}
|
|
||||||
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
|
/// Get YouTube player data (video/audio streams + basic metadata) using the specified client
|
||||||
|
@ -170,8 +149,12 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<VideoPlayer> for response::Player {
|
impl MapResponse<VideoPlayer> for response::Player {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
_lang: Language,
|
||||||
|
deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
|
) -> Result<super::MapResult<VideoPlayer>, ExtractionError> {
|
||||||
|
let deobf = Deobfuscator::new(deobf.unwrap())?;
|
||||||
let mut warnings = vec![];
|
let mut warnings = vec![];
|
||||||
|
|
||||||
// Check playability status
|
// Check playability status
|
||||||
|
@ -241,7 +224,7 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let streaming_data =
|
let mut streaming_data =
|
||||||
self.streaming_data
|
self.streaming_data
|
||||||
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
.ok_or(ExtractionError::InvalidData(Cow::Borrowed(
|
||||||
"no streaming data",
|
"no streaming data",
|
||||||
|
@ -252,10 +235,10 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
"no video details",
|
"no video details",
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
if video_details.video_id != ctx.id {
|
if video_details.video_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"video id {}, expected {}",
|
"video id {}, expected {}",
|
||||||
video_details.video_id, ctx.id
|
video_details.video_id, id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,16 +258,54 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
is_live_content: video_details.is_live_content,
|
is_live_content: video_details.is_live_content,
|
||||||
};
|
};
|
||||||
|
|
||||||
let streams = if !is_live {
|
let mut formats = streaming_data.formats.c;
|
||||||
let mut mapper = StreamsMapper::new(Deobfuscator::new(ctx.deobf.unwrap())?);
|
formats.append(&mut streaming_data.adaptive_formats.c);
|
||||||
mapper.map_streams(streaming_data.formats);
|
|
||||||
mapper.map_streams(streaming_data.adaptive_formats);
|
let mut video_streams: Vec<VideoStream> = Vec::new();
|
||||||
let mut res = mapper.output()?;
|
let mut video_only_streams: Vec<VideoStream> = Vec::new();
|
||||||
warnings.append(&mut res.warnings);
|
let mut audio_streams: Vec<AudioStream> = Vec::new();
|
||||||
res.c
|
|
||||||
} else {
|
if !is_live {
|
||||||
Streams::default()
|
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 subtitles = self.captions.map_or(Vec::new(), |captions| {
|
let subtitles = self.captions.map_or(Vec::new(), |captions| {
|
||||||
captions
|
captions
|
||||||
|
@ -353,309 +374,235 @@ impl MapResponse<VideoPlayer> for response::Player {
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
c: VideoPlayer {
|
c: VideoPlayer {
|
||||||
details: video_info,
|
details: video_info,
|
||||||
video_streams: streams.video_streams,
|
video_streams,
|
||||||
video_only_streams: streams.video_only_streams,
|
video_only_streams,
|
||||||
audio_streams: streams.audio_streams,
|
audio_streams,
|
||||||
subtitles,
|
subtitles,
|
||||||
expires_in_seconds: streaming_data.expires_in_seconds,
|
expires_in_seconds: streaming_data.expires_in_seconds,
|
||||||
hls_manifest_url: streaming_data.hls_manifest_url,
|
hls_manifest_url: streaming_data.hls_manifest_url,
|
||||||
dash_manifest_url: streaming_data.dash_manifest_url,
|
dash_manifest_url: streaming_data.dash_manifest_url,
|
||||||
preview_frames,
|
preview_frames,
|
||||||
client_type: ctx.client_type,
|
|
||||||
visitor_data: self
|
visitor_data: self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
.or_else(|| vdata.map(str::to_owned)),
|
||||||
},
|
},
|
||||||
warnings,
|
warnings,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StreamsMapper {
|
fn cipher_to_url_params(
|
||||||
deobf: Deobfuscator,
|
signature_cipher: &str,
|
||||||
streams: Streams,
|
deobf: &Deobfuscator,
|
||||||
warnings: Vec<String>,
|
) -> Result<(Url, BTreeMap<String, String>), DeobfError> {
|
||||||
/// First stream mapping error
|
let params: HashMap<Cow<str>, Cow<str>> =
|
||||||
first_err: Option<ExtractionError>,
|
url::form_urlencoded::parse(signature_cipher.as_bytes()).collect();
|
||||||
/// Last obfuscated nsig parameter (cache)
|
|
||||||
last_nsig: String,
|
// Parameters:
|
||||||
/// Last deobfuscated nsig parameter
|
// `s`: Obfuscated signature
|
||||||
last_nsig_deobf: String,
|
// `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))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
fn deobf_nsig(
|
||||||
struct Streams {
|
url_params: &mut BTreeMap<String, String>,
|
||||||
video_streams: Vec<VideoStream>,
|
deobf: &Deobfuscator,
|
||||||
video_only_streams: Vec<VideoStream>,
|
last_nsig: &mut [String; 2],
|
||||||
audio_streams: Vec<AudioStream>,
|
) -> Result<(), DeobfError> {
|
||||||
}
|
let nsig: String;
|
||||||
|
if let Some(n) = url_params.get("n") {
|
||||||
impl StreamsMapper {
|
nsig = if n == &last_nsig[0] {
|
||||||
fn new(deobf: Deobfuscator) -> Self {
|
last_nsig[1].clone()
|
||||||
Self {
|
} else {
|
||||||
deobf,
|
let nsig = deobf.deobfuscate_nsig(n)?;
|
||||||
streams: Streams::default(),
|
last_nsig[0] = n.to_string();
|
||||||
warnings: Vec::new(),
|
last_nsig[1].clone_from(&nsig);
|
||||||
first_err: None,
|
nsig
|
||||||
last_nsig: String::new(),
|
|
||||||
last_nsig_deobf: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_streams(&mut self, mut streams: MapResult<Vec<Format>>) {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for f in streams.c {
|
url_params.insert("n".to_owned(), nsig);
|
||||||
if f.format_type == player::FormatType::FormatStreamTypeOtf {
|
};
|
||||||
continue;
|
Ok(())
|
||||||
}
|
|
||||||
|
|
||||||
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<MapResult<Streams>, 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<String, String>), DeobfError> {
|
|
||||||
let params: HashMap<Cow<str>, Cow<str>> =
|
|
||||||
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<String, String>) -> 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<String>,
|
|
||||||
signature_cipher: &Option<String>,
|
|
||||||
) -> Result<UrlMapRes, ExtractionError> {
|
|
||||||
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<VideoStream, ExtractionError> {
|
|
||||||
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<AudioStream, ExtractionError> {
|
|
||||||
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<String>,
|
|
||||||
) -> 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 {
|
struct UrlMapRes {
|
||||||
url: String,
|
url: String,
|
||||||
|
throttled: bool,
|
||||||
xtags: Option<String>,
|
xtags: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_url(
|
||||||
|
url: &Option<String>,
|
||||||
|
signature_cipher: &Option<String>,
|
||||||
|
deobf: &Deobfuscator,
|
||||||
|
last_nsig: &mut [String; 2],
|
||||||
|
) -> MapResult<Option<UrlMapRes>> {
|
||||||
|
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<Option<VideoStream>> {
|
||||||
|
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<Option<AudioStream>> {
|
||||||
|
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>)> {
|
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
|
||||||
static PATTERN: Lazy<Regex> =
|
static PATTERN: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
|
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
|
||||||
|
@ -715,6 +662,43 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
||||||
AudioCodec::Unknown
|
AudioCodec::Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_audio_track(
|
||||||
|
track: response::player::AudioTrack,
|
||||||
|
xtags: Option<String>,
|
||||||
|
warnings: &mut Vec<String>,
|
||||||
|
) -> 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader};
|
use std::{fs::File, io::BufReader};
|
||||||
|
@ -723,7 +707,7 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{deobfuscate::DeobfData, param::Language, util::tests::TESTFILES};
|
use crate::{deobfuscate::DeobfData, util::tests::TESTFILES};
|
||||||
|
|
||||||
static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| {
|
static DEOBF_DATA: Lazy<DeobfData> = Lazy::new(|| {
|
||||||
DeobfData {
|
DeobfData {
|
||||||
|
@ -735,27 +719,18 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
#[rstest]
|
#[rstest]
|
||||||
#[case::desktop(ClientType::Desktop)]
|
#[case::desktop("desktop")]
|
||||||
#[case::desktop_music(ClientType::DesktopMusic)]
|
#[case::desktop_music("desktopmusic")]
|
||||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
#[case::tv_html5_embed("tvhtml5embed")]
|
||||||
#[case::android(ClientType::Android)]
|
#[case::android("android")]
|
||||||
#[case::ios(ClientType::Ios)]
|
#[case::ios("ios")]
|
||||||
fn map_player_data(#[case] client_type: ClientType) {
|
fn map_player_data(#[case] name: &str) {
|
||||||
let name = serde_plain::to_string(&client_type)
|
|
||||||
.unwrap()
|
|
||||||
.replace('_', "");
|
|
||||||
let json_path = path!(*TESTFILES / "player" / format!("{name}_video.json"));
|
let json_path = path!(*TESTFILES / "player" / format!("{name}_video.json"));
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = resp
|
let map_res = resp
|
||||||
.map_response(&MapRespCtx {
|
.map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBF_DATA), None)
|
||||||
id: "pPvd8UxmSbQ",
|
|
||||||
lang: Language::En,
|
|
||||||
deobf: Some(&DEOBF_DATA),
|
|
||||||
visitor_data: None,
|
|
||||||
client_type,
|
|
||||||
})
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
|
@ -780,12 +755,22 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn cipher_to_url() {
|
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 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 mapper = StreamsMapper::new(Deobfuscator::new(&DEOBF_DATA).unwrap());
|
let mut last_nsig: [String; 2] = [String::new(), String::new()];
|
||||||
let url = mapper
|
let deobf = Deobfuscator::new(&DEOBF_DATA).unwrap();
|
||||||
.map_url(&None, &Some(signature_cipher.to_owned()))
|
let map_res = map_url(
|
||||||
.unwrap()
|
&None,
|
||||||
.url;
|
&Some(signature_cipher.to_owned()),
|
||||||
|
&deobf,
|
||||||
|
&mut last_nsig,
|
||||||
|
);
|
||||||
|
let url = map_res.c.unwrap();
|
||||||
|
|
||||||
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");
|
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
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
||||||
util::{self, timeago, TryRemove},
|
util::{self, timeago, TryRemove},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, QBrowse, RustyPipeQuery};
|
use super::{response, ClientType, MapResponse, MapResult, QBrowse, RustyPipeQuery};
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get a YouTube playlist
|
/// Get a YouTube playlist
|
||||||
|
@ -47,9 +47,15 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<Playlist> for response::Playlist {
|
impl MapResponse<Playlist> for response::Playlist {
|
||||||
fn map_response(self, ctx: &MapRespCtx<'_>) -> Result<MapResult<Playlist>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
|
) -> Result<MapResult<Playlist>, ExtractionError> {
|
||||||
let (Some(contents), Some(header)) = (self.contents, self.header) else {
|
let (Some(contents), Some(header)) = (self.contents, self.header) else {
|
||||||
return Err(response::alerts_to_err(ctx.id, self.alerts));
|
return Err(response::alerts_to_err(id, self.alerts));
|
||||||
};
|
};
|
||||||
|
|
||||||
let video_items = contents
|
let video_items = contents
|
||||||
|
@ -79,7 +85,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
.playlist_video_list_renderer
|
.playlist_video_list_renderer
|
||||||
.contents;
|
.contents;
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||||
mapper.map_response(video_items);
|
mapper.map_response(video_items);
|
||||||
|
|
||||||
let (description, thumbnails, last_update_txt) = match self.sidebar {
|
let (description, thumbnails, last_update_txt) = match self.sidebar {
|
||||||
|
@ -138,10 +144,9 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
};
|
};
|
||||||
|
|
||||||
let playlist_id = header.playlist_header_renderer.playlist_id;
|
let playlist_id = header.playlist_header_renderer.playlist_id;
|
||||||
if playlist_id != ctx.id {
|
if playlist_id != id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"got wrong playlist id {}, expected {}",
|
"got wrong playlist id {playlist_id}, expected {id}"
|
||||||
playlist_id, ctx.id
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +165,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
.and_then(|link| ChannelId::try_from(link).ok());
|
.and_then(|link| ChannelId::try_from(link).ok());
|
||||||
|
|
||||||
let last_update = last_update_txt.as_ref().and_then(|txt| {
|
let last_update = last_update_txt.as_ref().and_then(|txt| {
|
||||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut mapper.warnings)
|
timeago::parse_textual_date_or_warn(lang, txt, &mut mapper.warnings)
|
||||||
.map(OffsetDateTime::date)
|
.map(OffsetDateTime::date)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -172,7 +177,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
Some(n_videos),
|
Some(n_videos),
|
||||||
mapper.items,
|
mapper.items,
|
||||||
mapper.ctoken,
|
mapper.ctoken,
|
||||||
ctx.visitor_data.map(str::to_owned),
|
vdata.map(str::to_owned),
|
||||||
ContinuationEndpoint::Browse,
|
ContinuationEndpoint::Browse,
|
||||||
),
|
),
|
||||||
video_count: n_videos,
|
video_count: n_videos,
|
||||||
|
@ -184,7 +189,7 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
visitor_data: self
|
visitor_data: self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
.or_else(|| vdata.map(str::to_owned)),
|
||||||
},
|
},
|
||||||
warnings: mapper.warnings,
|
warnings: mapper.warnings,
|
||||||
})
|
})
|
||||||
|
@ -198,7 +203,7 @@ mod tests {
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::util::tests::TESTFILES;
|
use crate::{param::Language, util::tests::TESTFILES};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -213,7 +218,7 @@ mod tests {
|
||||||
|
|
||||||
let playlist: response::Playlist =
|
let playlist: response::Playlist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = playlist.map_response(&MapRespCtx::test(id)).unwrap();
|
let map_res = playlist.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
||||||
param::search_filter::SearchFilter,
|
param::search_filter::SearchFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, MapRespCtx, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
use super::{response, ClientType, MapResponse, MapResult, RustyPipeQuery, YTContext};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -103,7 +103,10 @@ impl RustyPipeQuery {
|
||||||
impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
|
) -> Result<MapResult<SearchResult<T>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -112,7 +115,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
||||||
.section_list_renderer
|
.section_list_renderer
|
||||||
.contents;
|
.contents;
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(ctx.lang);
|
let mut mapper = response::YouTubeListMapper::<YouTubeItem>::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
@ -132,7 +135,7 @@ impl<T: FromYtItem> MapResponse<SearchResult<T>> for response::Search {
|
||||||
visitor_data: self
|
visitor_data: self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
.or_else(|| vdata.map(str::to_owned)),
|
||||||
},
|
},
|
||||||
warnings: mapper.warnings,
|
warnings: mapper.warnings,
|
||||||
})
|
})
|
||||||
|
@ -147,8 +150,9 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapRespCtx, MapResponse},
|
client::{response, MapResponse},
|
||||||
model::{SearchResult, YouTubeItem},
|
model::{SearchResult, YouTubeItem},
|
||||||
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
@ -164,7 +168,7 @@ mod tests {
|
||||||
|
|
||||||
let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
let search: response::Search = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<SearchResult<YouTubeItem>> =
|
let map_res: MapResult<SearchResult<YouTubeItem>> =
|
||||||
search.map_response(&MapRespCtx::test("")).unwrap();
|
search.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -79,6 +79,7 @@ VideoPlayer(
|
||||||
mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\"",
|
mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\"",
|
||||||
format: r#3gp,
|
format: r#3gp,
|
||||||
codec: mp4v,
|
codec: mp4v,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -97,6 +98,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
video_only_streams: [
|
video_only_streams: [
|
||||||
|
@ -123,6 +125,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -147,6 +150,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -171,6 +175,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -195,6 +200,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -219,6 +225,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -237,6 +244,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"",
|
mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -261,6 +269,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -285,6 +294,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -309,6 +319,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
audio_streams: [
|
audio_streams: [
|
||||||
|
@ -332,6 +343,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: None,
|
loudness_db: None,
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -354,6 +366,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: None,
|
loudness_db: None,
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -376,6 +389,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: None,
|
loudness_db: None,
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -398,6 +412,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: None,
|
loudness_db: None,
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -420,6 +435,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: None,
|
loudness_db: None,
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -466,6 +482,5 @@ VideoPlayer(
|
||||||
frames_per_page_y: 5,
|
frames_per_page_y: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
client_type: android,
|
|
||||||
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
|
visitor_data: Some("Cgt2aHFtQU5YZFBvYyirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -84,6 +84,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
video_only_streams: [
|
video_only_streams: [
|
||||||
|
@ -110,6 +111,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -134,6 +136,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -158,6 +161,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -182,6 +186,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -206,6 +211,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -230,6 +236,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -254,6 +261,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -278,6 +286,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -302,6 +311,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -326,6 +336,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -350,6 +361,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -374,6 +386,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -398,6 +411,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -422,6 +436,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -446,6 +461,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
audio_streams: [
|
audio_streams: [
|
||||||
|
@ -469,6 +485,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -491,6 +508,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -513,6 +531,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2159004),
|
loudness_db: Some(5.2159004),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -535,6 +554,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -581,6 +601,5 @@ VideoPlayer(
|
||||||
frames_per_page_y: 5,
|
frames_per_page_y: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
client_type: desktop,
|
|
||||||
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
|
visitor_data: Some("CgtoS1pCMVJTNUJISSirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,6 +52,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
video_only_streams: [
|
video_only_streams: [
|
||||||
|
@ -78,6 +79,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -102,6 +104,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -126,6 +129,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -150,6 +154,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -174,6 +179,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -198,6 +204,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -222,6 +229,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -246,6 +254,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -270,6 +279,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
audio_streams: [
|
audio_streams: [
|
||||||
|
@ -293,6 +303,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(0.0006532669),
|
loudness_db: Some(0.0006532669),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -315,6 +326,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(0.0006532669),
|
loudness_db: Some(0.0006532669),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -337,6 +349,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(-0.003446579),
|
loudness_db: Some(-0.003446579),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -359,6 +372,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(0.0006532669),
|
loudness_db: Some(0.0006532669),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -405,6 +419,5 @@ VideoPlayer(
|
||||||
frames_per_page_y: 5,
|
frames_per_page_y: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
client_type: desktop_music,
|
|
||||||
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
|
visitor_data: Some("CgszSHZWNWs0SDhpTSiS4aWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -81,6 +81,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.4D401E\"",
|
mime: "video/mp4; codecs=\"avc1.4D401E\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -105,6 +106,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64002A\"",
|
mime: "video/mp4; codecs=\"avc1.64002A\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
audio_streams: [
|
audio_streams: [
|
||||||
|
@ -128,6 +130,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2159004),
|
loudness_db: Some(5.2159004),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -150,6 +153,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2159004),
|
loudness_db: Some(5.2159004),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -196,6 +200,5 @@ VideoPlayer(
|
||||||
frames_per_page_y: 5,
|
frames_per_page_y: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
client_type: ios,
|
|
||||||
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
|
visitor_data: Some("Cgs4TXV4dk13WVEyWSirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -84,6 +84,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
video_only_streams: [
|
video_only_streams: [
|
||||||
|
@ -110,6 +111,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -134,6 +136,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -158,6 +161,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -182,6 +186,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.00M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -206,6 +211,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -230,6 +236,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -254,6 +261,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.01M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -278,6 +286,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -302,6 +311,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.04M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -326,6 +336,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -350,6 +361,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -374,6 +386,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.08M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -398,6 +411,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: avc1,
|
codec: avc1,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -422,6 +436,7 @@ VideoPlayer(
|
||||||
mime: "video/webm; codecs=\"vp9\"",
|
mime: "video/webm; codecs=\"vp9\"",
|
||||||
format: webm,
|
format: webm,
|
||||||
codec: vp9,
|
codec: vp9,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
VideoStream(
|
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",
|
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",
|
||||||
|
@ -446,6 +461,7 @@ VideoPlayer(
|
||||||
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
mime: "video/mp4; codecs=\"av01.0.09M.08\"",
|
||||||
format: mp4,
|
format: mp4,
|
||||||
codec: av01,
|
codec: av01,
|
||||||
|
throttled: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
audio_streams: [
|
audio_streams: [
|
||||||
|
@ -469,6 +485,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -491,6 +508,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -513,6 +531,7 @@ VideoPlayer(
|
||||||
codec: mp4a,
|
codec: mp4a,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2159004),
|
loudness_db: Some(5.2159004),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
AudioStream(
|
AudioStream(
|
||||||
|
@ -535,6 +554,7 @@ VideoPlayer(
|
||||||
codec: opus,
|
codec: opus,
|
||||||
channels: Some(2),
|
channels: Some(2),
|
||||||
loudness_db: Some(5.2200003),
|
loudness_db: Some(5.2200003),
|
||||||
|
throttled: false,
|
||||||
track: None,
|
track: None,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -581,6 +601,5 @@ VideoPlayer(
|
||||||
frames_per_page_y: 5,
|
frames_per_page_y: 5,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
client_type: tv_html5_embed,
|
|
||||||
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
|
visitor_data: Some("CgtacUJOMG81dTI3cyirsaWXBg%3D%3D"),
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,9 +10,7 @@ use crate::{
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{response, ClientType, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery};
|
||||||
response, ClientType, MapRespCtx, MapResponse, QBrowse, QBrowseParams, RustyPipeQuery,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyPipeQuery {
|
||||||
/// Get the videos from the YouTube startpage
|
/// Get the videos from the YouTube startpage
|
||||||
|
@ -58,7 +56,10 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<VideoItem>>, ExtractionError> {
|
||||||
let grid = self
|
let grid = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -74,10 +75,10 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
||||||
|
|
||||||
Ok(map_startpage_videos(
|
Ok(map_startpage_videos(
|
||||||
grid,
|
grid,
|
||||||
ctx.lang,
|
lang,
|
||||||
self.response_context
|
self.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned)),
|
.or_else(|| vdata.map(str::to_owned)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,7 +86,10 @@ impl MapResponse<Paginator<VideoItem>> for response::Startpage {
|
||||||
impl MapResponse<Vec<VideoItem>> for response::Trending {
|
impl MapResponse<Vec<VideoItem>> for response::Trending {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: crate::param::Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
) -> Result<MapResult<Vec<VideoItem>>, ExtractionError> {
|
||||||
let items = self
|
let items = self
|
||||||
.contents
|
.contents
|
||||||
|
@ -99,7 +103,7 @@ impl MapResponse<Vec<VideoItem>> for response::Trending {
|
||||||
.section_list_renderer
|
.section_list_renderer
|
||||||
.contents;
|
.contents;
|
||||||
|
|
||||||
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(ctx.lang);
|
let mut mapper = response::YouTubeListMapper::<VideoItem>::new(lang);
|
||||||
mapper.map_response(items);
|
mapper.map_response(items);
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(MapResult {
|
||||||
|
@ -137,8 +141,9 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapRespCtx, MapResponse},
|
client::{response, MapResponse},
|
||||||
model::{paginator::Paginator, VideoItem},
|
model::{paginator::Paginator, VideoItem},
|
||||||
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
@ -150,8 +155,9 @@ mod tests {
|
||||||
|
|
||||||
let startpage: response::Startpage =
|
let startpage: response::Startpage =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Paginator<VideoItem>> =
|
let map_res: MapResult<Paginator<VideoItem>> = startpage
|
||||||
startpage.map_response(&MapRespCtx::test("")).unwrap();
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -173,8 +179,9 @@ mod tests {
|
||||||
|
|
||||||
let startpage: response::Trending =
|
let startpage: response::Trending =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res: MapResult<Vec<VideoItem>> =
|
let map_res: MapResult<Vec<VideoItem>> = startpage
|
||||||
startpage.map_response(&MapRespCtx::test("")).unwrap();
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -5,13 +5,14 @@ use serde::Serialize;
|
||||||
use crate::{
|
use crate::{
|
||||||
error::{Error, ExtractionError},
|
error::{Error, ExtractionError},
|
||||||
model::UrlTarget,
|
model::UrlTarget,
|
||||||
|
param::Language,
|
||||||
serializer::MapResult,
|
serializer::MapResult,
|
||||||
util,
|
util,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, url_endpoint::NavigationEndpoint},
|
response::{self, url_endpoint::NavigationEndpoint},
|
||||||
ClientType, MapRespCtx, MapResponse, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -324,7 +325,13 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<UrlTarget> for response::ResolvedUrl {
|
impl MapResponse<UrlTarget> for response::ResolvedUrl {
|
||||||
fn map_response(self, _ctx: &MapRespCtx<'_>) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
fn map_response(
|
||||||
|
self,
|
||||||
|
_id: &str,
|
||||||
|
_lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
|
) -> Result<MapResult<UrlTarget>, ExtractionError> {
|
||||||
let pt = self.endpoint.page_type();
|
let pt = self.endpoint.page_type();
|
||||||
if let NavigationEndpoint::Browse {
|
if let NavigationEndpoint::Browse {
|
||||||
browse_endpoint, ..
|
browse_endpoint, ..
|
||||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
response::{self, video_details::Payload, IconType},
|
response::{self, video_details::Payload, IconType},
|
||||||
ClientType, MapRespCtx, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
ClientType, MapResponse, QContinuation, RustyPipeQuery, YTContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
|
@ -89,26 +89,28 @@ impl RustyPipeQuery {
|
||||||
impl MapResponse<VideoDetails> for response::VideoDetails {
|
impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
vdata: Option<&str>,
|
||||||
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
) -> Result<MapResult<VideoDetails>, ExtractionError> {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
|
||||||
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound {
|
let contents = self.contents.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no content".into(),
|
msg: "no content".into(),
|
||||||
})?;
|
})?;
|
||||||
let current_video_endpoint =
|
let current_video_endpoint =
|
||||||
self.current_video_endpoint
|
self.current_video_endpoint
|
||||||
.ok_or_else(|| ExtractionError::NotFound {
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.to_owned(),
|
id: id.to_owned(),
|
||||||
msg: "no current_video_endpoint".into(),
|
msg: "no current_video_endpoint".into(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let video_id = current_video_endpoint.watch_endpoint.video_id;
|
let video_id = current_video_endpoint.watch_endpoint.video_id;
|
||||||
if ctx.id != video_id {
|
if id != video_id {
|
||||||
return Err(ExtractionError::WrongResult(format!(
|
return Err(ExtractionError::WrongResult(format!(
|
||||||
"got wrong video id {}, expected {}",
|
"got wrong video id {video_id}, expected {id}"
|
||||||
video_id, ctx.id
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +120,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
.results
|
.results
|
||||||
.contents
|
.contents
|
||||||
.ok_or_else(|| ExtractionError::NotFound {
|
.ok_or_else(|| ExtractionError::NotFound {
|
||||||
id: ctx.id.into(),
|
id: id.into(),
|
||||||
msg: "no primary_results".into(),
|
msg: "no primary_results".into(),
|
||||||
})?;
|
})?;
|
||||||
warnings.append(&mut primary_results.warnings);
|
warnings.append(&mut primary_results.warnings);
|
||||||
|
@ -187,7 +189,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
// so we ignore parse errors here for now
|
// so we ignore parse errors here for now
|
||||||
like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
|
like_text.and_then(|txt| util::parse_numeric(&txt).ok()),
|
||||||
date_text.as_deref().and_then(|txt| {
|
date_text.as_deref().and_then(|txt| {
|
||||||
timeago::parse_textual_date_or_warn(ctx.lang, txt, &mut warnings)
|
timeago::parse_textual_date_or_warn(lang, txt, &mut warnings)
|
||||||
}),
|
}),
|
||||||
date_text,
|
date_text,
|
||||||
view_count
|
view_count
|
||||||
|
@ -205,7 +207,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
let comment_count = comment_count_section.and_then(|s| {
|
let comment_count = comment_count_section.and_then(|s| {
|
||||||
util::parse_large_numstr_or_warn::<u64>(
|
util::parse_large_numstr_or_warn::<u64>(
|
||||||
&s.comments_entry_point_header_renderer.comment_count,
|
&s.comments_entry_point_header_renderer.comment_count,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut warnings,
|
&mut warnings,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -273,7 +275,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
let visitor_data = self
|
let visitor_data = self
|
||||||
.response_context
|
.response_context
|
||||||
.visitor_data
|
.visitor_data
|
||||||
.or_else(|| ctx.visitor_data.map(str::to_owned));
|
.or_else(|| vdata.map(str::to_owned));
|
||||||
let recommended = contents
|
let recommended = contents
|
||||||
.two_column_watch_next_results
|
.two_column_watch_next_results
|
||||||
.secondary_results
|
.secondary_results
|
||||||
|
@ -283,7 +285,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
r,
|
r,
|
||||||
sr.secondary_results.continuations,
|
sr.secondary_results.continuations,
|
||||||
visitor_data.clone(),
|
visitor_data.clone(),
|
||||||
ctx.lang,
|
lang,
|
||||||
);
|
);
|
||||||
warnings.append(&mut res.warnings);
|
warnings.append(&mut res.warnings);
|
||||||
res.c
|
res.c
|
||||||
|
@ -348,7 +350,7 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
avatar: owner.thumbnail.into(),
|
avatar: owner.thumbnail.into(),
|
||||||
verification: owner.badges.into(),
|
verification: owner.badges.into(),
|
||||||
subscriber_count: owner.subscriber_count_text.and_then(|txt| {
|
subscriber_count: owner.subscriber_count_text.and_then(|txt| {
|
||||||
util::parse_large_numstr_or_warn(&txt, ctx.lang, &mut warnings)
|
util::parse_large_numstr_or_warn(&txt, lang, &mut warnings)
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
view_count,
|
view_count,
|
||||||
|
@ -383,7 +385,10 @@ impl MapResponse<VideoDetails> for response::VideoDetails {
|
||||||
impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
fn map_response(
|
fn map_response(
|
||||||
self,
|
self,
|
||||||
ctx: &MapRespCtx<'_>,
|
_id: &str,
|
||||||
|
lang: Language,
|
||||||
|
_deobf: Option<&crate::deobfuscate::DeobfData>,
|
||||||
|
_vdata: Option<&str>,
|
||||||
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
) -> Result<MapResult<Paginator<Comment>>, ExtractionError> {
|
||||||
let received_endpoints = self.on_response_received_endpoints;
|
let received_endpoints = self.on_response_received_endpoints;
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
|
@ -410,7 +415,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
comment.comment_renderer,
|
comment.comment_renderer,
|
||||||
Some(thread.replies),
|
Some(thread.replies),
|
||||||
thread.rendering_priority,
|
thread.rendering_priority,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut warnings,
|
&mut warnings,
|
||||||
));
|
));
|
||||||
} else if let Some(vm) = thread.comment_view_model {
|
} else if let Some(vm) = thread.comment_view_model {
|
||||||
|
@ -419,7 +424,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
&mut mutations,
|
&mut mutations,
|
||||||
Some(thread.replies),
|
Some(thread.replies),
|
||||||
thread.rendering_priority,
|
thread.rendering_priority,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut warnings,
|
&mut warnings,
|
||||||
) {
|
) {
|
||||||
comments.push(c);
|
comments.push(c);
|
||||||
|
@ -435,7 +440,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
comment,
|
comment,
|
||||||
None,
|
None,
|
||||||
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut warnings,
|
&mut warnings,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -445,7 +450,7 @@ impl MapResponse<Paginator<Comment>> for response::VideoComments {
|
||||||
&mut mutations,
|
&mut mutations,
|
||||||
None,
|
None,
|
||||||
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
response::video_details::CommentPriority::RenderingPriorityUnknown,
|
||||||
ctx.lang,
|
lang,
|
||||||
&mut warnings,
|
&mut warnings,
|
||||||
) {
|
) {
|
||||||
comments.push(c);
|
comments.push(c);
|
||||||
|
@ -649,7 +654,8 @@ mod tests {
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{response, MapRespCtx, MapResponse},
|
client::{response, MapResponse},
|
||||||
|
param::Language,
|
||||||
util::tests::TESTFILES,
|
util::tests::TESTFILES,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -670,7 +676,7 @@ mod tests {
|
||||||
|
|
||||||
let details: response::VideoDetails =
|
let details: response::VideoDetails =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = details.map_response(&MapRespCtx::test(id)).unwrap();
|
let map_res = details.map_response(id, Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
@ -690,7 +696,9 @@ mod tests {
|
||||||
|
|
||||||
let details: response::VideoDetails =
|
let details: response::VideoDetails =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let err = details.map_response(&MapRespCtx::test("")).unwrap_err();
|
let err = details
|
||||||
|
.map_response("", Language::En, None, None)
|
||||||
|
.unwrap_err();
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
err,
|
err,
|
||||||
crate::error::ExtractionError::NotFound { .. }
|
crate::error::ExtractionError::NotFound { .. }
|
||||||
|
@ -708,7 +716,7 @@ mod tests {
|
||||||
|
|
||||||
let comments: response::VideoComments =
|
let comments: response::VideoComments =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = comments.map_response(&MapRespCtx::test("")).unwrap();
|
let map_res = comments.map_response("", Language::En, None, None).unwrap();
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
map_res.warnings.is_empty(),
|
map_res.warnings.is_empty(),
|
||||||
|
|
|
@ -57,7 +57,7 @@ impl DeobfData {
|
||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_fns(js_url: &str, player_js: &str) -> Result<Self, Error> {
|
fn extract_fns(js_url: &str, player_js: &str) -> Result<Self, Error> {
|
||||||
let sig_fn = get_sig_fn(player_js)?;
|
let sig_fn = get_sig_fn(player_js)?;
|
||||||
let nsig_fn = get_nsig_fn(player_js)?;
|
let nsig_fn = get_nsig_fn(player_js)?;
|
||||||
let sts = get_sts(player_js)?;
|
let sts = get_sts(player_js)?;
|
||||||
|
@ -84,19 +84,28 @@ impl Deobfuscator {
|
||||||
|
|
||||||
/// Deobfuscate the `s` parameter from the `signature_cipher` field
|
/// Deobfuscate the `s` parameter from the `signature_cipher` field
|
||||||
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> {
|
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> {
|
||||||
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, [sig])?;
|
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, vec![sig])?;
|
||||||
|
|
||||||
res.into_string()
|
res.as_str().map_or(
|
||||||
.ok_or(DeobfError::Other("sig deobfuscation fn returned no string"))
|
Err(DeobfError::Other("sig deobfuscation func returned null")),
|
||||||
|
|res| {
|
||||||
|
tracing::debug!("deobfuscated sig");
|
||||||
|
Ok(res.to_owned())
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deobfuscate the `n` stream URL parameter to circumvent throttling
|
/// Deobfuscate the `n` stream URL parameter to circumvent throttling
|
||||||
pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> {
|
pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> {
|
||||||
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, [nsig])?;
|
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, vec![nsig])?;
|
||||||
|
|
||||||
res.into_string().ok_or(DeobfError::Other(
|
res.as_str().map_or(
|
||||||
"nsig deobfuscation fn returned no string",
|
Err(DeobfError::Other("nsig deobfuscation func returned null")),
|
||||||
))
|
|res| {
|
||||||
|
tracing::debug!("deobfuscated nsig");
|
||||||
|
Ok(res.to_owned())
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,9 +144,12 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
||||||
|
|
||||||
let deobfuscate_function = format!(
|
let deobfuscate_function = format!(
|
||||||
"var {};",
|
"var {};",
|
||||||
&function_pattern
|
function_pattern
|
||||||
.captures(player_js)
|
.captures(player_js)
|
||||||
.ok_or(DeobfError::Extraction("deobf function"))?[1]
|
.ok_or(DeobfError::Extraction("deobf function"))?
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> =
|
static HELPER_OBJECT_NAME_REGEX: Lazy<Regex> =
|
||||||
|
@ -156,37 +168,59 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
||||||
let helper_pattern = Regex::new(&helper_pattern_str)
|
let helper_pattern = Regex::new(&helper_pattern_str)
|
||||||
.map_err(|_| DeobfError::Other("could not parse helper pattern regex"))?;
|
.map_err(|_| DeobfError::Other("could not parse helper pattern regex"))?;
|
||||||
let player_js_nonl = player_js.replace('\n', "");
|
let player_js_nonl = player_js.replace('\n', "");
|
||||||
let helper_object = &helper_pattern
|
let helper_object = helper_pattern
|
||||||
.captures(&player_js_nonl)
|
.captures(&player_js_nonl)
|
||||||
.ok_or(DeobfError::Extraction("helper object"))?[1];
|
.ok_or(DeobfError::Extraction("helper object"))?
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str();
|
||||||
|
|
||||||
let js_fn = helper_object.to_owned()
|
Ok(helper_object.to_owned()
|
||||||
+ &deobfuscate_function
|
+ &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_names(player_js: &str) -> impl Iterator<Item = String> + '_ {
|
fn get_nsig_fn_name(player_js: &str) -> Result<String, DeobfError> {
|
||||||
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
|
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||||
// x.get( .. y=functionName[array_num](z) .. x.set(
|
Regex::new(
|
||||||
Regex::new(r#"\w\.get\(.+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
|
r#"\.get\("n"\)\)&&\([a-zA-Z0-9$_]=([a-zA-Z0-9$_]+)(?:\[(\d+)])?\([a-zA-Z0-9$_]\)"#,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
});
|
});
|
||||||
|
|
||||||
FUNCTION_NAME_REGEX
|
let fname_match = FUNCTION_NAME_REGEX
|
||||||
.captures_iter(player_js)
|
.captures(player_js)
|
||||||
.filter_map(|fname_match| {
|
.ok_or(DeobfError::Extraction("n_deobf function"))?;
|
||||||
let function_name = &fname_match[1];
|
|
||||||
|
|
||||||
let array_num = fname_match[2].parse::<usize>().ok()?;
|
let function_name = fname_match.get(1).unwrap().as_str();
|
||||||
let array_pattern_str =
|
|
||||||
format!(r#"var {}\s*=\s*\[(.+?)]"#, regex::escape(function_name));
|
|
||||||
let array_pattern = Regex::new(&array_pattern_str).ok()?;
|
|
||||||
|
|
||||||
let array_str = &array_pattern.captures(player_js)?[1];
|
if fname_match.len() == 1 {
|
||||||
array_str.split(',').nth(array_num).map(str::to_owned)
|
return Ok(function_name.to_owned());
|
||||||
})
|
}
|
||||||
|
|
||||||
|
let array_num = fname_match
|
||||||
|
.get(2)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
|
.parse::<usize>()
|
||||||
|
.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())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
|
fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
|
||||||
|
@ -241,44 +275,13 @@ fn extract_js_fn(js: &str, name: &str) -> Result<String, DeobfError> {
|
||||||
Ok(js[start..end].to_owned())
|
Ok(js[start..end].to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
|
fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
|
||||||
let extract_fn = |name: &str| -> Result<String, DeobfError> {
|
let function_name = get_nsig_fn_name(player_js)?;
|
||||||
let function_base = format!("{name}=function");
|
let function_base = function_name.clone() + "=function";
|
||||||
let offset = player_js
|
let offset = player_js.find(&function_base).unwrap_or_default();
|
||||||
.find(&function_base)
|
|
||||||
.ok_or(DeobfError::Extraction("could not find function base"))?;
|
|
||||||
|
|
||||||
let js_fn = extract_js_fn(&player_js[offset..], name)
|
extract_js_fn(&player_js[offset..], &function_name)
|
||||||
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, name))?;
|
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, &function_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<String, Error> {
|
async fn get_player_js_url(http: &Client) -> Result<String, Error> {
|
||||||
|
@ -292,9 +295,12 @@ async fn get_player_js_url(http: &Client) -> Result<String, Error> {
|
||||||
static PLAYER_HASH_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
static PLAYER_HASH_PATTERN: Lazy<Regex> = Lazy::new(|| {
|
||||||
Regex::new(r"https:\\/\\/www\.youtube\.com\\/s\\/player\\/([a-z0-9]{8})\\/").unwrap()
|
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)
|
.captures(&text)
|
||||||
.ok_or(DeobfError::Extraction("player hash"))?[1];
|
.ok_or(DeobfError::Extraction("player hash"))?
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str();
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"https://www.youtube.com/s/player/{player_hash}/player_ias.vflset/en_US/base.js"
|
"https://www.youtube.com/s/player/{player_hash}/player_ias.vflset/en_US/base.js"
|
||||||
|
@ -312,7 +318,10 @@ fn get_sts(player_js: &str) -> Result<String, DeobfError> {
|
||||||
|
|
||||||
Ok(STS_PATTERN
|
Ok(STS_PATTERN
|
||||||
.captures(player_js)
|
.captures(player_js)
|
||||||
.ok_or(DeobfError::Extraction("sts"))?[1]
|
.ok_or(DeobfError::Extraction("sts"))?
|
||||||
|
.get(1)
|
||||||
|
.unwrap()
|
||||||
|
.as_str()
|
||||||
.to_owned())
|
.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,7 +331,6 @@ mod tests {
|
||||||
use crate::util::tests::TESTFILES;
|
use crate::util::tests::TESTFILES;
|
||||||
use path_macro::path;
|
use path_macro::path;
|
||||||
use rstest::{fixture, rstest};
|
use rstest::{fixture, rstest};
|
||||||
use tracing_test::traced_test;
|
|
||||||
|
|
||||||
static TEST_JS: Lazy<String> = Lazy::new(|| {
|
static TEST_JS: Lazy<String> = Lazy::new(|| {
|
||||||
let js_path = path!(*TESTFILES / "deobf" / "dummy_player.js");
|
let js_path = path!(*TESTFILES / "deobf" / "dummy_player.js");
|
||||||
|
@ -374,9 +382,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]
|
#[test]
|
||||||
fn t_get_nsig_fn_names() {
|
fn t_get_nsig_fn_name() {
|
||||||
let names = get_nsig_fn_names(&TEST_JS).collect::<Vec<_>>();
|
let name = get_nsig_fn_name(&TEST_JS).unwrap();
|
||||||
assert_eq!(names, ["Vo"]);
|
assert_eq!(name, "Vo");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -427,15 +435,14 @@ 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]
|
#[tokio::test]
|
||||||
#[traced_test]
|
|
||||||
async fn t_update() {
|
async fn t_update() {
|
||||||
let client = Client::new();
|
let client = Client::new();
|
||||||
let deobf_data = DeobfData::extract(client, None).await.unwrap();
|
let deobf_data = DeobfData::extract(client, None).await.unwrap();
|
||||||
let deobf = Deobfuscator::new(&deobf_data).unwrap();
|
let deobf = Deobfuscator::new(&deobf_data).unwrap();
|
||||||
|
|
||||||
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
||||||
assert!(deobf_sig.len() >= 100);
|
println!("{deobf_sig}");
|
||||||
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
|
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
|
||||||
assert!(deobf_nsig.len() >= 10);
|
println!("{deobf_nsig}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use time::{Date, OffsetDateTime};
|
use time::{Date, OffsetDateTime};
|
||||||
|
|
||||||
use self::{paginator::Paginator, richtext::RichText};
|
use self::{paginator::Paginator, richtext::RichText};
|
||||||
use crate::{client::ClientType, error::Error, param::Country, validate};
|
use crate::{error::Error, param::Country, validate};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#COMMON
|
#COMMON
|
||||||
|
@ -143,8 +143,6 @@ pub struct VideoPlayer {
|
||||||
pub dash_manifest_url: Option<String>,
|
pub dash_manifest_url: Option<String>,
|
||||||
/// Video frames for seek preview
|
/// Video frames for seek preview
|
||||||
pub preview_frames: Vec<Frameset>,
|
pub preview_frames: Vec<Frameset>,
|
||||||
/// Client type with which the player was fetched
|
|
||||||
pub client_type: ClientType,
|
|
||||||
/// YouTube visitor data cookie
|
/// YouTube visitor data cookie
|
||||||
pub visitor_data: Option<String>,
|
pub visitor_data: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -213,6 +211,9 @@ pub struct VideoStream {
|
||||||
pub format: VideoFormat,
|
pub format: VideoFormat,
|
||||||
/// Video codec
|
/// Video codec
|
||||||
pub codec: VideoCodec,
|
pub codec: VideoCodec,
|
||||||
|
/// True if the deobfuscation of the nsig url parameter failed
|
||||||
|
/// and the stream will be throttled
|
||||||
|
pub throttled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Audio stream
|
/// Audio stream
|
||||||
|
@ -258,6 +259,9 @@ pub struct AudioStream {
|
||||||
///
|
///
|
||||||
/// The loudness parameter is not available when using the Android client.
|
/// The loudness parameter is not available when using the Android client.
|
||||||
pub loudness_db: Option<f32>,
|
pub loudness_db: Option<f32>,
|
||||||
|
/// True if the deobfuscation of the nsig url parameter failed
|
||||||
|
/// and the stream will be throttled
|
||||||
|
pub throttled: bool,
|
||||||
/// Audio track information
|
/// Audio track information
|
||||||
///
|
///
|
||||||
/// Videos can have multiple audio tracks (different languages).
|
/// Videos can have multiple audio tracks (different languages).
|
||||||
|
@ -825,7 +829,7 @@ pub enum YouTubeItem {
|
||||||
Channel(ChannelItem),
|
Channel(ChannelItem),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YouTube video list item (from search results, recommendations, playlists)
|
/// YouTube video list item
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct VideoItem {
|
pub struct VideoItem {
|
||||||
|
@ -864,7 +868,7 @@ pub struct VideoItem {
|
||||||
pub short_description: Option<String>,
|
pub short_description: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YouTube channel list item (from search results)
|
/// YouTube channel list item
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct ChannelItem {
|
pub struct ChannelItem {
|
||||||
|
@ -886,7 +890,7 @@ pub struct ChannelItem {
|
||||||
pub short_description: String,
|
pub short_description: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// YouTube playlist list item (from search results)
|
/// YouTube playlist list item
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct PlaylistItem {
|
pub struct PlaylistItem {
|
||||||
|
|
|
@ -135,14 +135,6 @@ pub trait YtEntity {
|
||||||
fn id(&self) -> &str;
|
fn id(&self) -> &str;
|
||||||
/// Name
|
/// Name
|
||||||
fn name(&self) -> &str;
|
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 {
|
macro_rules! yt_entity {
|
||||||
|
@ -155,80 +147,6 @@ macro_rules! yt_entity {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.name
|
&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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -241,14 +159,6 @@ impl YtEntity for VideoPlayer {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.details.name
|
&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<T> YtEntity for Channel<T> {
|
impl<T> YtEntity for Channel<T> {
|
||||||
|
@ -259,34 +169,26 @@ impl<T> YtEntity for Channel<T> {
|
||||||
fn name(&self) -> &str {
|
fn name(&self) -> &str {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channel_id(&self) -> Option<&str> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn channel_name(&self) -> Option<&str> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
yt_entity_owner! {VideoPlayerDetails}
|
yt_entity! {VideoPlayerDetails}
|
||||||
yt_entity_owner_opt! {Playlist}
|
yt_entity! {Playlist}
|
||||||
yt_entity! {ChannelId}
|
yt_entity! {ChannelId}
|
||||||
yt_entity_owner! {VideoDetails}
|
yt_entity! {VideoDetails}
|
||||||
yt_entity! {ChannelTag}
|
yt_entity! {ChannelTag}
|
||||||
yt_entity! {ChannelRss}
|
yt_entity! {ChannelRss}
|
||||||
yt_entity! {ChannelRssVideo}
|
yt_entity! {ChannelRssVideo}
|
||||||
yt_entity_owner_opt! {VideoItem}
|
yt_entity! {VideoItem}
|
||||||
yt_entity! {ChannelItem}
|
yt_entity! {ChannelItem}
|
||||||
yt_entity_owner_opt! {PlaylistItem}
|
yt_entity! {PlaylistItem}
|
||||||
yt_entity! {VideoId}
|
yt_entity! {VideoId}
|
||||||
yt_entity_owner_music! {TrackItem}
|
yt_entity! {TrackItem}
|
||||||
yt_entity! {ArtistItem}
|
yt_entity! {ArtistItem}
|
||||||
yt_entity_owner_music! {AlbumItem}
|
yt_entity! {AlbumItem}
|
||||||
yt_entity_owner_opt! {MusicPlaylistItem}
|
yt_entity! {MusicPlaylistItem}
|
||||||
yt_entity! {AlbumId}
|
yt_entity! {AlbumId}
|
||||||
yt_entity_owner_opt! {MusicPlaylist}
|
yt_entity! {MusicPlaylist}
|
||||||
yt_entity_owner_music! {MusicAlbum}
|
yt_entity! {MusicAlbum}
|
||||||
yt_entity! {MusicArtist}
|
yt_entity! {MusicArtist}
|
||||||
yt_entity! {MusicGenreItem}
|
yt_entity! {MusicGenreItem}
|
||||||
yt_entity! {MusicGenre}
|
yt_entity! {MusicGenre}
|
||||||
|
|
|
@ -9,15 +9,15 @@ use crate::model::{
|
||||||
|
|
||||||
/// The StreamFilter is used for selecting audio/video streams from an extracted video
|
/// The StreamFilter is used for selecting audio/video streams from an extracted video
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct StreamFilter {
|
pub struct StreamFilter<'a> {
|
||||||
audio_max_bitrate: Option<u32>,
|
audio_max_bitrate: Option<u32>,
|
||||||
audio_formats: Option<Vec<AudioFormat>>,
|
audio_formats: Option<&'a [AudioFormat]>,
|
||||||
audio_codecs: Option<Vec<AudioCodec>>,
|
audio_codecs: Option<&'a [AudioCodec]>,
|
||||||
audio_language: Option<String>,
|
audio_language: Option<&'a str>,
|
||||||
video_max_res: Option<u32>,
|
video_max_res: Option<u32>,
|
||||||
video_max_fps: Option<u8>,
|
video_max_fps: Option<u8>,
|
||||||
video_formats: Option<Vec<VideoFormat>>,
|
video_formats: Option<&'a [VideoFormat]>,
|
||||||
video_codecs: Option<Vec<VideoCodec>>,
|
video_codecs: Option<&'a [VideoCodec]>,
|
||||||
video_hdr: bool,
|
video_hdr: bool,
|
||||||
video_none: bool,
|
video_none: bool,
|
||||||
}
|
}
|
||||||
|
@ -64,7 +64,7 @@ impl FilterResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamFilter {
|
impl<'a> StreamFilter<'a> {
|
||||||
/// Create a new [`StreamFilter`]
|
/// Create a new [`StreamFilter`]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -90,8 +90,8 @@ impl StreamFilter {
|
||||||
|
|
||||||
/// Set the supported audio container formats
|
/// Set the supported audio container formats
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn audio_formats<F: Into<Vec<AudioFormat>>>(mut self, formats: F) -> Self {
|
pub fn audio_formats(mut self, formats: &'a [AudioFormat]) -> Self {
|
||||||
self.audio_formats = Some(formats.into());
|
self.audio_formats = Some(formats);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,8 +104,8 @@ impl StreamFilter {
|
||||||
|
|
||||||
/// Set the supported audio codecs
|
/// Set the supported audio codecs
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn audio_codecs<C: Into<Vec<AudioCodec>>>(mut self, codecs: C) -> Self {
|
pub fn audio_codecs(mut self, codecs: &'a [AudioCodec]) -> Self {
|
||||||
self.audio_codecs = Some(codecs.into());
|
self.audio_codecs = Some(codecs);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,8 +123,8 @@ impl StreamFilter {
|
||||||
/// If this filter is unset or no stream matches,
|
/// If this filter is unset or no stream matches,
|
||||||
/// the filter returns the default audio stream.
|
/// the filter returns the default audio stream.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn audio_language<S: Into<String>>(mut self, language: S) -> Self {
|
pub fn audio_language(mut self, language: &'a str) -> Self {
|
||||||
self.audio_language = Some(language.into());
|
self.audio_language = Some(language);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,8 +184,8 @@ impl StreamFilter {
|
||||||
|
|
||||||
/// Set the supported video container formats
|
/// Set the supported video container formats
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn video_formats<F: Into<Vec<VideoFormat>>>(mut self, formats: F) -> Self {
|
pub fn video_formats(mut self, formats: &'a [VideoFormat]) -> Self {
|
||||||
self.video_formats = Some(formats.into());
|
self.video_formats = Some(formats);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,8 +198,8 @@ impl StreamFilter {
|
||||||
|
|
||||||
/// Set the supported video codecs
|
/// Set the supported video codecs
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn video_codecs<C: Into<Vec<VideoCodec>>>(mut self, codecs: C) -> Self {
|
pub fn video_codecs(mut self, codecs: &'a [VideoCodec]) -> Self {
|
||||||
self.video_codecs = Some(codecs.into());
|
self.video_codecs = Some(codecs);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,11 +250,6 @@ impl StreamFilter {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return true if no video stream should be selected
|
|
||||||
pub fn is_video_none(&self) -> bool {
|
|
||||||
self.video_none
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VideoPlayer {
|
impl VideoPlayer {
|
||||||
|
@ -378,13 +373,13 @@ mod tests {
|
||||||
#[rstest]
|
#[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::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::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_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_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::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::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::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::noformat(StreamFilter::default().audio_formats(&[]).clone(), None)]
|
||||||
#[case::nocodec(StreamFilter::default().audio_codecs([]).clone(), None)]
|
#[case::nocodec(StreamFilter::default().audio_codecs(&[]).clone(), None)]
|
||||||
fn t_select_audio_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
fn t_select_audio_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
||||||
let selection = PLAYER_ML.select_audio_stream(&filter);
|
let selection = PLAYER_ML.select_audio_stream(&filter);
|
||||||
|
|
||||||
|
@ -400,10 +395,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(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::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::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::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::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::noformat(StreamFilter::default().video_formats(&[]).clone(), None)]
|
||||||
#[case::nocodec(StreamFilter::default().video_codecs([]).clone(), None)]
|
#[case::nocodec(StreamFilter::default().video_codecs(&[]).clone(), None)]
|
||||||
fn t_select_video_only_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
fn t_select_video_only_stream(#[case] filter: StreamFilter, #[case] expect_url: Option<&str>) {
|
||||||
let selection = PLAYER_HDR.select_video_only_stream(&filter);
|
let selection = PLAYER_HDR.select_video_only_stream(&filter);
|
||||||
|
|
||||||
|
@ -420,12 +415,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")
|
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(
|
#[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?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")
|
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(
|
#[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"),
|
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
|
None
|
||||||
)]
|
)]
|
||||||
|
@ -434,7 +429,7 @@ mod tests {
|
||||||
None,
|
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")
|
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(
|
fn t_select_video_audio_stream(
|
||||||
#[case] filter: StreamFilter,
|
#[case] filter: StreamFilter,
|
||||||
#[case] expect_video_url: Option<&str>,
|
#[case] expect_video_url: Option<&str>,
|
||||||
|
|
|
@ -551,24 +551,6 @@ 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<I, T, P, O, E>(mut iter: I, e_empty: E, mut f: P) -> Result<O, E>
|
|
||||||
where
|
|
||||||
I: Iterator<Item = T>,
|
|
||||||
P: FnMut(T) -> Result<O, E>,
|
|
||||||
{
|
|
||||||
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)]
|
#[cfg(test)]
|
||||||
pub(crate) mod tests {
|
pub(crate) mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::PathBuf};
|
use std::{fs::File, io::BufReader, path::PathBuf};
|
||||||
|
@ -748,27 +730,4 @@ pub(crate) mod tests {
|
||||||
let res = country_from_name(name);
|
let res = country_from_name(name);
|
||||||
assert_eq!(res, expect);
|
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1125,6 +1125,5 @@
|
||||||
"expires_in_seconds": 21540,
|
"expires_in_seconds": 21540,
|
||||||
"hls_manifest_url": null,
|
"hls_manifest_url": null,
|
||||||
"dash_manifest_url": null,
|
"dash_manifest_url": null,
|
||||||
"preview_frames": [],
|
"preview_frames": []
|
||||||
"client_type": "desktop"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2120,6 +2120,5 @@
|
||||||
"hls_manifest_url": null,
|
"hls_manifest_url": null,
|
||||||
"dash_manifest_url": null,
|
"dash_manifest_url": null,
|
||||||
"preview_frames": [],
|
"preview_frames": [],
|
||||||
"visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D",
|
"visitor_data": "CgtGWDFCUllrcTdxayjo1_OiBg%3D%3D"
|
||||||
"client_type": "desktop"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,11 +201,11 @@ MusicAlbum(
|
||||||
cover: [],
|
cover: [],
|
||||||
artists: [
|
artists: [
|
||||||
ArtistId(
|
ArtistId(
|
||||||
id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||||
name: "Herbrido",
|
name: "Herbrido",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||||
album: Some(AlbumId(
|
album: Some(AlbumId(
|
||||||
id: "MPREb_Z81wHtF9fhC",
|
id: "MPREb_Z81wHtF9fhC",
|
||||||
name: "June Compilation",
|
name: "June Compilation",
|
||||||
|
|
|
@ -201,11 +201,11 @@ MusicAlbum(
|
||||||
cover: [],
|
cover: [],
|
||||||
artists: [
|
artists: [
|
||||||
ArtistId(
|
ArtistId(
|
||||||
id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||||
name: "[name]",
|
name: "[name]",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
artist_id: Some("UCzXI_RZ1Uqy8L8TiurTFTIg"),
|
artist_id: Some("UCxByvsK9hDZk2MnnF9jsFGw"),
|
||||||
album: Some(AlbumId(
|
album: Some(AlbumId(
|
||||||
id: "MPREb_Z81wHtF9fhC",
|
id: "MPREb_Z81wHtF9fhC",
|
||||||
name: "[name]",
|
name: "[name]",
|
||||||
|
|
|
@ -120,6 +120,7 @@ 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.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
|
||||||
assert_eq!(video.format, VideoFormat::Mp4);
|
assert_eq!(video.format, VideoFormat::Mp4);
|
||||||
assert_eq!(video.codec, VideoCodec::Av01);
|
assert_eq!(video.codec, VideoCodec::Av01);
|
||||||
|
assert!(!video.throttled);
|
||||||
|
|
||||||
assert_approx(audio.bitrate, 142_718);
|
assert_approx(audio.bitrate, 142_718);
|
||||||
assert_approx(audio.average_bitrate, 130_708);
|
assert_approx(audio.average_bitrate, 130_708);
|
||||||
|
@ -127,6 +128,7 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
||||||
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
|
||||||
assert_eq!(audio.format, AudioFormat::Webm);
|
assert_eq!(audio.format, AudioFormat::Webm);
|
||||||
assert_eq!(audio.codec, AudioCodec::Opus);
|
assert_eq!(audio.codec, AudioCodec::Opus);
|
||||||
|
assert!(!audio.throttled);
|
||||||
|
|
||||||
check_video_stream(video).await;
|
check_video_stream(video).await;
|
||||||
check_video_stream(audio).await;
|
check_video_stream(audio).await;
|
||||||
|
@ -1266,7 +1268,7 @@ mod channel_rss {
|
||||||
async fn search(rp: RustyPipe, unlocalized: bool) {
|
async fn search(rp: RustyPipe, unlocalized: bool) {
|
||||||
let result = rp
|
let result = rp
|
||||||
.query()
|
.query()
|
||||||
.search::<YouTubeItem, _>("arudino")
|
.search::<YouTubeItem, _>("doobydoobap")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1277,7 +1279,7 @@ async fn search(rp: RustyPipe, unlocalized: bool) {
|
||||||
);
|
);
|
||||||
|
|
||||||
if unlocalized {
|
if unlocalized {
|
||||||
assert_eq!(result.corrected_query.as_deref(), Some("arduino"));
|
assert_eq!(result.corrected_query.as_deref(), Some("doobydobap"));
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_next(result.items, rp.query(), 10, 2, true).await;
|
assert_next(result.items, rp.query(), 10, 2, true).await;
|
||||||
|
|
Loading…
Reference in a new issue