Compare commits

..

No commits in common. "8548bc81e923e33f0e6013751aecd3e1e6ef6305" and "925652acdd1c5ab8a040f9681f318be8b1a0ec3d" have entirely different histories.

50 changed files with 11548 additions and 1622 deletions

5
.gitignore vendored
View file

@ -1,5 +1,6 @@
/target
/Cargo.lock
rustypipe_reports
rustypipe_cache.json
RustyPipeReports
RustyPipeCache.json
rusty-tube.json

View file

@ -7,16 +7,12 @@ edition = "2021"
members = [".", "cli"]
[features]
default = ["default-tls", "yaml"]
default = ["default-tls"]
# Reqwest TLS
default-tls = ["reqwest/default-tls"]
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
# Error reports in yaml format
yaml = ["serde_yaml"]
[dependencies]
# quick-js = "0.4.1"
quick-js = { path = "../quickjs-rs" }
@ -30,9 +26,10 @@ reqwest = {version = "0.11.11", default-features = false, features = ["json", "g
tokio = {version = "1.20.0", features = ["macros", "fs", "process"]}
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.82"
serde_yaml = {version = "0.9.11", optional = true}
serde_yaml = "0.9.11"
serde_with = {version = "2.0.0", features = ["json"] }
rand = "0.8.5"
async-trait = "0.1.56"
chrono = {version = "0.4.19", features = ["serde"]}
chronoutil = "0.2.3"
futures = "0.3.21"

View file

@ -6,7 +6,7 @@ edition = "2021"
[dependencies]
rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]}
reqwest = {version = "0.11.11", default_features = false}
tokio = {version = "1.20.0", features = ["macros", "rt-multi-thread"]}
tokio = {version = "1.20.0", features = ["rt-multi-thread"]}
indicatif = "0.17.0"
futures = "0.3.21"
anyhow = "1.0"

View file

@ -6,7 +6,7 @@ use futures::stream::{self, StreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use reqwest::{Client, ClientBuilder};
use rustypipe::{
client::{ClientType, RustyPipe},
client2::{ClientType, RustyPipe},
model::stream_filter::Filter,
};
@ -59,7 +59,6 @@ async fn download_single_video(
let res = async {
let player_data = rp
.query()
.get_player(video_id.as_str(), ClientType::TvHtml5Embed)
.await
.context(format!(
@ -149,7 +148,7 @@ async fn download_playlist(
.expect("unable to build the HTTP client");
let rp = RustyPipe::default();
let playlist = rp.query().get_playlist(id).await.unwrap();
let playlist = rp.get_playlist(id).await.unwrap();
// Indicatif setup
let multi = MultiProgress::new();

View file

@ -1,57 +1,364 @@
use std::{
fs,
fs::File,
future::Future,
io::BufReader,
path::{Path, PathBuf},
sync::Arc,
};
use log::error;
use anyhow::Result;
use chrono::{DateTime, Duration, Utc};
use log::{error, info};
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;
pub trait CacheStorage {
fn write(&self, data: &str);
fn read(&self) -> Option<String>;
#[derive(Default, Debug, Clone)]
pub struct Cache {
file: Option<PathBuf>,
data: Arc<Mutex<CacheData>>,
}
pub struct FileStorage {
path: PathBuf,
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
struct CacheData {
desktop_client: Option<CacheEntry<ClientData>>,
music_client: Option<CacheEntry<ClientData>>,
deobf: Option<CacheEntry<DeobfData>>,
}
impl FileStorage {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheEntry<T> {
last_update: DateTime<Utc>,
data: T,
}
impl<T> From<T> for CacheEntry<T> {
fn from(f: T) -> Self {
Self {
path: path.as_ref().to_path_buf(),
last_update: Utc::now(),
data: f,
}
}
}
impl Default for FileStorage {
fn default() -> Self {
Self {
path: Path::new("rustypipe_cache.json").into(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ClientData {
pub version: String,
}
impl CacheStorage for FileStorage {
fn write(&self, data: &str) {
fs::write(&self.path, data).unwrap_or_else(|e| {
error!(
"Could not write cache to file `{}`. Error: {}",
self.path.to_string_lossy(),
e
);
});
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeobfData {
pub js_url: String,
pub sig_fn: String,
pub nsig_fn: String,
pub sts: String,
}
impl Cache {
pub async fn get_desktop_client_data<F>(&self, updater: F) -> Result<ClientData>
where
F: Future<Output = Result<ClientData>> + Send + 'static,
{
let mut cache = self.data.lock().await;
if cache.desktop_client.is_none()
|| cache.desktop_client.as_ref().unwrap().last_update < Utc::now() - Duration::hours(24)
{
let cdata = updater.await?;
cache.desktop_client = Some(CacheEntry::from(cdata.clone()));
self.save(&cache);
Ok(cdata)
} else {
Ok(cache.desktop_client.as_ref().unwrap().data.clone())
}
}
fn read(&self) -> Option<String> {
match fs::read_to_string(&self.path) {
Ok(data) => Some(data),
pub async fn get_music_client_data<F>(&self, updater: F) -> Result<ClientData>
where
F: Future<Output = Result<ClientData>> + Send + 'static,
{
let mut cache = self.data.lock().await;
if cache.music_client.is_none()
|| cache.music_client.as_ref().unwrap().last_update < Utc::now() - Duration::hours(24)
{
let cdata = updater.await?;
cache.music_client = Some(CacheEntry::from(cdata.clone()));
self.save(&cache);
Ok(cdata)
} else {
Ok(cache.music_client.as_ref().unwrap().data.clone())
}
}
pub async fn get_deobf_data<F>(&self, updater: F) -> Result<DeobfData>
where
F: Future<Output = Result<DeobfData>> + Send + 'static,
{
let mut cache = self.data.lock().await;
if cache.deobf.is_none()
|| cache.deobf.as_ref().unwrap().last_update < Utc::now() - Duration::hours(24)
{
let deobf_data = updater.await?;
cache.deobf = Some(CacheEntry::from(deobf_data.clone()));
self.save(&cache);
Ok(deobf_data)
} else {
Ok(cache.deobf.as_ref().unwrap().data.clone())
}
}
pub async fn to_json(&self) -> Result<String> {
let cache = self.data.lock().await;
Ok(serde_json::to_string(&cache.clone())?)
}
pub async fn to_json_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let cache = self.data.lock().await;
Ok(serde_json::to_writer(&File::create(path)?, &cache.clone())?)
}
pub fn from_json(json: &str) -> Self {
let data: CacheData = match serde_json::from_str(json) {
Ok(cd) => cd,
Err(e) => {
error!(
"Could not load cache from file `{}`. Error: {}",
self.path.to_string_lossy(),
"Could not load cache from json, falling back to default. Error: {}",
e
);
None
CacheData::default()
}
};
Cache {
data: Arc::new(Mutex::new(data)),
file: None,
}
}
pub fn from_json_file<P: AsRef<Path>>(path: P) -> Self {
let file = match File::open(path.as_ref()) {
Ok(file) => file,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
info!(
"Cache json file at {} not found, will be created",
path.as_ref().to_string_lossy()
)
} else {
error!(
"Could not open cache json file, falling back to default. Error: {}",
e
);
}
return Cache {
file: Some(path.as_ref().to_path_buf()),
..Default::default()
};
}
};
let data: CacheData = match serde_json::from_reader(BufReader::new(file)) {
Ok(data) => data,
Err(e) => {
error!(
"Could not load cache from json, falling back to default. Error: {}",
e
);
return Cache {
file: Some(path.as_ref().to_path_buf()),
..Default::default()
};
}
};
Cache {
data: Arc::new(Mutex::new(data)),
file: Some(path.as_ref().to_path_buf()),
}
}
fn save(&self, cache: &CacheData) {
match self.file.as_ref() {
Some(file) => match File::create(file) {
Ok(file) => match serde_json::to_writer(file, cache) {
Ok(_) => {}
Err(e) => error!("Could not write cache to json. Error: {}", e),
},
Err(e) => error!("Could not open cache json file. Error: {}", e),
},
None => {}
}
}
}
#[cfg(test)]
mod tests {
use temp_testdir::TempDir;
use super::*;
#[tokio::test]
async fn test() {
let cache = Cache::default();
let desktop_c = cache
.get_desktop_client_data(async {
Ok(ClientData {
version: "1.2.3".to_owned(),
})
})
.await
.unwrap();
assert_eq!(
desktop_c,
ClientData {
version: "1.2.3".to_owned()
}
);
let music_c = cache
.get_music_client_data(async {
Ok(ClientData {
version: "4.5.6".to_owned(),
})
})
.await
.unwrap();
assert_eq!(
music_c,
ClientData {
version: "4.5.6".to_owned()
}
);
let deobf_data = cache
.get_deobf_data(async {
Ok(DeobfData {
js_url:
"https://www.youtube.com/s/player/011af516/player_ias.vflset/en_US/base.js"
.to_owned(),
sig_fn: "t_sig_fn".to_owned(),
nsig_fn: "t_nsig_fn".to_owned(),
sts: "t_sts".to_owned(),
})
})
.await
.unwrap();
assert_eq!(
deobf_data,
DeobfData {
js_url: "https://www.youtube.com/s/player/011af516/player_ias.vflset/en_US/base.js"
.to_owned(),
sig_fn: "t_sig_fn".to_owned(),
nsig_fn: "t_nsig_fn".to_owned(),
sts: "t_sts".to_owned(),
}
);
// Create a new cache from the first one's json
// and check if it returns the same cached data
let json = cache.to_json().await.unwrap();
let new_cache = Cache::from_json(&json);
assert_eq!(
new_cache
.get_desktop_client_data(async {
Ok(ClientData {
version: "".to_owned(),
})
})
.await
.unwrap(),
desktop_c
);
assert_eq!(
new_cache
.get_music_client_data(async {
Ok(ClientData {
version: "".to_owned(),
})
})
.await
.unwrap(),
music_c
);
assert_eq!(
new_cache
.get_deobf_data(async {
Ok(DeobfData {
js_url: "".to_owned(),
nsig_fn: "".to_owned(),
sig_fn: "".to_owned(),
sts: "".to_owned(),
})
})
.await
.unwrap(),
deobf_data
);
}
#[tokio::test]
async fn test_file() {
let temp = TempDir::default();
let mut file_path = PathBuf::from(temp.as_ref());
file_path.push("cache.json");
let cache = Cache::from_json_file(file_path.clone());
let cdata = cache
.get_desktop_client_data(async {
Ok(ClientData {
version: "1.2.3".to_owned(),
})
})
.await
.unwrap();
let deobf_data = cache
.get_deobf_data(async {
Ok(DeobfData {
js_url:
"https://www.youtube.com/s/player/011af516/player_ias.vflset/en_US/base.js"
.to_owned(),
sig_fn: "t_sig_fn".to_owned(),
nsig_fn: "t_nsig_fn".to_owned(),
sts: "t_sts".to_owned(),
})
})
.await
.unwrap();
assert!(file_path.exists());
let new_cache = Cache::from_json_file(file_path.clone());
assert_eq!(
new_cache
.get_desktop_client_data(async {
Ok(ClientData {
version: "".to_owned(),
})
})
.await
.unwrap(),
cdata
);
assert_eq!(
new_cache
.get_deobf_data(async {
Ok(DeobfData {
js_url: "".to_owned(),
nsig_fn: "".to_owned(),
sig_fn: "".to_owned(),
sts: "".to_owned(),
})
})
.await
.unwrap(),
deobf_data
);
}
}

41
src/client/channel.rs Normal file
View file

@ -0,0 +1,41 @@
use anyhow::Result;
use reqwest::Method;
use serde::Serialize;
use super::{response, ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QChannel {
context: ContextYT,
browse_id: String,
params: String,
}
impl RustyTube {
async fn get_channel_response(&self, channel_id: &str) -> Result<response::Channel> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QChannel {
context,
browse_id: channel_id.to_owned(),
params: "EgZ2aWRlb3PyBgQKAjoA".to_owned(),
};
let resp = client
.request_builder(Method::POST, "browse")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::Channel>().await?)
}
}
#[cfg(test)]
mod tests {
}

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,20 @@
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
sync::Arc,
};
use anyhow::{anyhow, bail, Result};
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
use fancy_regex::Regex;
use log::{error, warn};
use once_cell::sync::Lazy;
use reqwest::{Method, Url};
use reqwest::Method;
use serde::Serialize;
use url::Url;
use crate::{
deobfuscate::Deobfuscator,
model::{
AudioCodec, AudioFormat, AudioStream, AudioTrack, Channel, Language, Subtitle, VideoCodec,
VideoFormat, VideoInfo, VideoPlayer, VideoStream,
},
util,
};
use super::{
response::{self, player},
ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery,
};
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@ -57,211 +49,58 @@ struct QContentPlaybackContext {
referer: String,
}
impl RustyPipeQuery {
pub async fn get_player(self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
let q1 = self.clone();
let t_context = tokio::spawn(async move { q1.get_context(client_type, false).await });
let q2 = self.clone();
let t_deobf = tokio::spawn(async move { q2.get_deobf().await });
impl RustyTube {
pub async fn get_player(&self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
let client = self.get_ytclient(client_type);
let (context, deobf) = tokio::join!(
client.get_context(false),
Deobfuscator::from_fetched_info(client.http_client(), self.cache.clone())
);
let deobf = deobf?;
let request_body = build_request_body(client.clone(), &deobf, context, video_id);
let (context, deobf) = tokio::join!(t_context, t_deobf);
let context = context.unwrap();
let deobf = deobf.unwrap()?;
let resp = client
.request_builder(Method::POST, "player")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
let request_body = if client_type.is_web() {
QPlayer {
context,
playback_context: Some(QPlaybackContext {
content_playback_context: QContentPlaybackContext {
signature_timestamp: deobf.get_sts(),
referer: format!("https://www.youtube.com/watch?v={}", video_id),
},
}),
cpn: None,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
} else {
QPlayer {
context,
playback_context: None,
cpn: Some(util::generate_content_playback_nonce()),
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
};
self.execute_request_deobf::<response::Player, _, _>(
client_type,
"get_player",
Method::POST,
"player",
video_id,
&request_body,
Some(&deobf),
)
.await
let player_response = resp.json::<response::Player>().await?;
map_player_data(player_response, &deobf)
}
}
impl MapResponse<VideoPlayer> for response::Player {
fn map_response(
self,
id: &str,
_lang: Language,
deobf: Option<&Deobfuscator>,
) -> Result<super::MapResult<VideoPlayer>> {
let deobf = deobf.unwrap();
let mut warnings = vec![];
// Check playability status
match self.playability_status {
response::player::PlayabilityStatus::Ok { live_streamability } => {
if live_streamability.is_some() {
bail!("Active livestreams are not supported")
}
}
response::player::PlayabilityStatus::Unplayable { reason } => {
bail!("Video is unplayable. Reason: {}", reason)
}
response::player::PlayabilityStatus::LoginRequired { reason } => {
bail!("Playback requires login. Reason: {}", reason)
}
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
bail!("Livestream is offline. Reason: {}", reason)
}
response::player::PlayabilityStatus::Error { reason } => {
bail!("Video was deleted. Reason: {}", reason)
}
};
let mut streaming_data = some_or_bail!(
self.streaming_data,
Err(anyhow!("No streaming data was returned"))
);
let video_details = some_or_bail!(
self.video_details,
Err(anyhow!("No video details were returned"))
);
let microformat = self.microformat.map(|m| m.player_microformat_renderer);
let (publish_date, category, tags, is_family_safe) =
microformat.map_or((None, None, None, None), |m| {
(
Local
.from_local_datetime(&NaiveDateTime::new(
m.publish_date,
NaiveTime::from_hms(0, 0, 0),
))
.single(),
Some(m.category),
m.tags,
Some(m.is_family_safe),
)
});
if video_details.video_id != id {
bail!(
"got wrong video id {}, expected {}",
video_details.video_id,
id
);
fn build_request_body(
client: Arc<dyn YTClient>,
deobf: &Deobfuscator,
context: ContextYT,
video_id: &str,
) -> QPlayer {
if client.get_type().is_web() {
QPlayer {
context,
playback_context: Some(QPlaybackContext {
content_playback_context: QContentPlaybackContext {
signature_timestamp: deobf.get_sts(),
referer: format!("https://www.youtube.com/watch?v={}", video_id),
},
}),
cpn: None,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
let video_info = VideoInfo {
id: video_details.video_id,
title: video_details.title,
description: video_details.short_description,
length: video_details.length_seconds,
thumbnails: video_details.thumbnail.unwrap_or_default().into(),
channel: Channel {
id: video_details.channel_id,
name: video_details.author,
},
publish_date,
view_count: video_details.view_count,
keywords: match video_details.keywords {
Some(keywords) => keywords,
None => tags.unwrap_or_default(),
},
category,
is_live_content: video_details.is_live_content,
is_family_safe,
};
let mut formats = streaming_data.formats.c;
formats.append(&mut streaming_data.adaptive_formats.c);
warnings.append(&mut streaming_data.formats.warnings);
warnings.append(&mut streaming_data.adaptive_formats.warnings);
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
let mut video_streams: Vec<VideoStream> = Vec::new();
let mut video_only_streams: Vec<VideoStream> = Vec::new();
let mut audio_streams: Vec<AudioStream> = Vec::new();
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)),
}
} else {
QPlayer {
context,
playback_context: None,
cpn: Some(util::generate_content_playback_nonce()),
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
video_streams.sort();
video_only_streams.sort();
audio_streams.sort();
let mut subtitles = vec![];
if let Some(captions) = self.captions {
for c in captions.player_captions_tracklist_renderer.caption_tracks {
let lang_auto = c.name.strip_suffix(" (auto-generated)");
subtitles.push(Subtitle {
url: c.base_url,
lang: c.language_code,
lang_name: lang_auto.unwrap_or(&c.name).to_owned(),
auto_generated: lang_auto.is_some(),
})
}
}
Ok(MapResult {
c: VideoPlayer {
info: video_info,
video_streams,
video_only_streams,
audio_streams,
subtitles,
expires_in_seconds: streaming_data.expires_in_seconds,
},
warnings,
})
}
}
@ -297,7 +136,7 @@ fn deobf_nsig(
let nsig: String;
match url_params.get("n") {
Some(n) => {
nsig = if n == &last_nsig[0] {
nsig = if n.to_owned() == last_nsig[0] {
last_nsig[1].to_owned()
} else {
let nsig = deobf.deobfuscate_nsig(n)?;
@ -318,192 +157,108 @@ fn map_url(
signature_cipher: &Option<String>,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<(String, bool)>> {
) -> Option<(String, bool)> {
let (url_base, mut url_params) = match url {
Some(url) => ok_or_bail!(
util::url_to_params(url),
MapResult {
c: None,
warnings: vec![format!("Could not parse url `{}`", url)]
}
),
Some(url) => ok_or_bail!(util::url_to_params(url), None),
None => match signature_cipher {
Some(signature_cipher) => match cipher_to_url_params(signature_cipher, deobf) {
Ok(res) => res,
Err(e) => {
return MapResult {
c: None,
warnings: vec![format!(
"Could not deobfuscate signatureCipher `{}`: {}",
signature_cipher, e
)],
};
error!("Could not deobfuscate signatureCipher: {}", e);
return None;
}
},
None => {
return MapResult {
c: None,
warnings: vec!["stream contained neither url nor cipher".to_owned()],
}
}
None => return None,
},
};
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
));
warn!("Could not deobfuscate nsig: {}", e);
throttled = true;
});
MapResult {
c: Some((
ok_or_bail!(
Url::parse_with_params(url_base.as_str(), url_params.iter()),
MapResult {
c: None,
warnings: vec![format!(
"url could not be joined. url: `{}` params: {:?}",
url_base, url_params
)],
}
)
.to_string(),
throttled,
)),
warnings,
}
Some((
ok_or_bail!(
Url::parse_with_params(url_base.as_str(), url_params.iter()),
None
)
.to_string(),
throttled,
))
}
fn map_video_stream(
f: player::Format,
f: &player::Format,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<VideoStream>> {
let (mtype, codecs) = some_or_bail!(
parse_mime(&f.mime_type),
MapResult {
c: None,
warnings: vec![format!(
"Invalid mime type `{}` in video format {:?}",
&f.mime_type, &f
)]
}
);
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
) -> Option<VideoStream> {
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
let (url, throttled) =
some_or_bail!(map_url(&f.url, &f.signature_cipher, deobf, last_nsig), None);
match map_res.c {
Some((url, throttled)) => MapResult {
c: Some(VideoStream {
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,
// 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,
mime: f.mime_type.to_owned(),
format: some_or_bail!(
get_video_format(mtype),
MapResult {
c: None,
warnings: vec![format!("invalid video format. itag: {}", f.itag)]
}
),
codec: get_video_codec(codecs),
throttled,
}),
warnings: map_res.warnings,
},
None => MapResult {
c: None,
warnings: map_res.warnings,
},
}
Some(VideoStream {
url,
itag: f.itag,
bitrate: f.bitrate,
average_bitrate: f.average_bitrate,
size: f.content_length,
index_range: f.index_range.clone(),
init_range: f.init_range.clone(),
width: some_or_bail!(f.width, None),
height: some_or_bail!(f.height, None),
fps: some_or_bail!(f.fps, None),
quality: some_or_bail!(f.quality_label.clone(), None),
hdr: f.color_info.clone().unwrap_or_default().primaries
== player::Primaries::ColorPrimariesBt2020,
mime: f.mime_type.to_owned(),
format: some_or_bail!(get_video_format(mtype), None),
codec: get_video_codec(codecs),
throttled,
})
}
fn map_audio_stream(
f: player::Format,
f: &player::Format,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<AudioStream>> {
) -> Option<AudioStream> {
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2})\."#).unwrap());
let (mtype, codecs) = some_or_bail!(
parse_mime(&f.mime_type),
MapResult {
c: None,
warnings: vec![format!(
"Invalid mime type `{}` in video format {:?}",
&f.mime_type, &f
)]
}
);
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
let (url, throttled) =
some_or_bail!(map_url(&f.url, &f.signature_cipher, deobf, last_nsig), None);
match map_res.c {
Some((url, throttled)) => MapResult {
c: Some(AudioStream {
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,
mime: f.mime_type.to_owned(),
format: some_or_bail!(
get_audio_format(mtype),
MapResult {
c: None,
warnings: vec![format!("invalid audio format. itag: {}", f.itag)]
}
),
codec: get_audio_codec(codecs),
throttled,
track: match f.audio_track {
Some(t) => {
let lang = LANG_PATTERN
.captures(&t.id)
.ok()
.flatten()
.map(|m| m.get(1).unwrap().as_str().to_owned());
Some(AudioTrack {
id: t.id,
lang,
lang_name: t.display_name,
is_default: t.audio_is_default,
})
}
None => None,
},
}),
warnings: map_res.warnings,
},
None => MapResult {
c: None,
warnings: map_res.warnings,
},
}
Some(AudioStream {
url,
itag: f.itag,
bitrate: f.bitrate,
average_bitrate: f.average_bitrate,
size: f.content_length,
index_range: f.index_range.to_owned(),
init_range: f.init_range.to_owned(),
mime: f.mime_type.to_owned(),
format: some_or_bail!(get_audio_format(mtype), None),
codec: get_audio_codec(codecs),
throttled,
track: f.audio_track.as_ref().map(|t| AudioTrack {
id: t.id.to_owned(),
lang: LANG_PATTERN
.captures(&t.id)
.ok()
.flatten()
.map(|m| m.get(1).unwrap().as_str().to_owned()),
lang_name: t.display_name.to_owned(),
is_default: t.audio_is_default,
}),
})
}
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
static PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
let captures = some_or_bail!(PATTERN.captures(mime).ok().flatten(), None);
let captures = some_or_bail!(PATTERN.captures(&mime).ok().flatten(), None);
Some((
captures.get(1).unwrap().as_str(),
captures
@ -558,16 +313,140 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
AudioCodec::Unknown
}
fn map_player_data(response: response::Player, deobf: &Deobfuscator) -> Result<VideoPlayer> {
// Check playability status
match response.playability_status {
response::player::PlayabilityStatus::Ok { live_streamability } => {
if live_streamability.is_some() {
bail!("Active livestreams are not supported")
}
}
response::player::PlayabilityStatus::Unplayable { reason } => {
bail!("Video is unplayable. Reason: {}", reason)
}
response::player::PlayabilityStatus::LoginRequired { reason } => {
bail!("Playback requires login. Reason: {}", reason)
}
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
bail!("Livestream is offline. Reason: {}", reason)
}
response::player::PlayabilityStatus::Error { reason } => {
bail!("Video was deleted. Reason: {}", reason)
}
};
let streaming_data = some_or_bail!(
response.streaming_data,
Err(anyhow!("No streaming data was returned"))
);
let video_details = some_or_bail!(
response.video_details,
Err(anyhow!("No video details were returned"))
);
let microformat = response.microformat.map(|m| m.player_microformat_renderer);
let video_info = VideoInfo {
id: video_details.video_id,
title: video_details.title,
description: video_details.short_description,
length: video_details.length_seconds,
thumbnails: video_details
.thumbnail
.unwrap_or_default()
.thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
height: t.height,
width: t.width,
})
.collect(),
channel: Channel {
id: video_details.channel_id,
name: video_details.author,
},
publish_date: microformat.as_ref().map(|m| {
let ndt = NaiveDateTime::new(m.publish_date, NaiveTime::from_hms(0, 0, 0));
Local.from_local_datetime(&ndt).unwrap()
}),
view_count: video_details.view_count,
keywords: video_details
.keywords
.or_else(|| microformat.as_ref().map_or(None, |mf| mf.tags.clone()))
.unwrap_or_default(),
category: microformat.as_ref().map(|m| m.category.to_owned()),
is_live_content: video_details.is_live_content,
is_family_safe: microformat.as_ref().map(|m| m.is_family_safe),
};
let mut formats = streaming_data.formats.clone();
formats.append(&mut streaming_data.adaptive_formats.clone());
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
let mut video_streams: Vec<VideoStream> = Vec::new();
let mut video_only_streams: Vec<VideoStream> = Vec::new();
let mut audio_streams: Vec<AudioStream> = Vec::new();
for f in formats {
if f.format_type == player::FormatType::FormatStreamTypeOtf {
continue;
}
match (f.is_video(), f.is_audio()) {
(true, true) => match map_video_stream(&f, deobf, &mut last_nsig) {
Some(stream) => video_streams.push(stream),
None => {}
},
(true, false) => match map_video_stream(&f, deobf, &mut last_nsig) {
Some(stream) => video_only_streams.push(stream),
None => {}
},
(false, true) => match map_audio_stream(&f, deobf, &mut last_nsig) {
Some(stream) => audio_streams.push(stream),
None => {}
},
(false, false) => {}
}
}
video_streams.sort();
video_only_streams.sort();
audio_streams.sort();
let subtitles = response.captions.map_or(vec![], |captions| {
captions
.player_captions_tracklist_renderer
.caption_tracks
.iter()
.map(|caption| {
let lang_auto = caption.name.strip_suffix(" (auto-generated)");
Subtitle {
url: caption.base_url.to_owned(),
lang: caption.language_code.to_owned(),
lang_name: lang_auto.unwrap_or(&caption.name).to_owned(),
auto_generated: lang_auto.is_some(),
}
})
.collect()
});
Ok(VideoPlayer {
info: video_info,
video_streams,
video_only_streams,
audio_streams,
subtitles,
expires_in_seconds: streaming_data.expires_in_seconds,
})
}
#[cfg(test)]
#[cfg(feature = "yaml")]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use crate::{
client::{RustyPipe, CLIENT_TYPES},
deobfuscate::DeobfData,
report::TestFileReporter,
};
use crate::{cache::DeobfData, client::CLIENT_TYPES};
use super::*;
use rstest::rstest;
@ -586,6 +465,8 @@ mod tests {
let tf_dir = Path::new("testfiles/player");
let video_id = "pPvd8UxmSbQ";
let rt = RustyTube::new();
for client_type in CLIENT_TYPES {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("{:?}_video.json", client_type).to_lowercase());
@ -593,20 +474,31 @@ mod tests {
continue;
}
let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
rp.test_query()
.report(true)
.get_player(video_id, client_type)
let client = rt.get_ytclient(client_type);
let context = client.get_context(false).await;
let request_body = build_request_body(client.clone(), &DEOBFUSCATOR, context, video_id);
let resp = client
.request_builder(Method::POST, "player")
.await
.json(&request_body)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let mut file = File::create(json_path).unwrap();
let mut content = std::io::Cursor::new(resp.bytes().await.unwrap());
std::io::copy(&mut content, &mut file).unwrap();
}
}
#[test_log::test(tokio::test)]
async fn download_model_testfiles() {
let tf_dir = Path::new("testfiles/player_model");
let rp = RustyPipe::new_test();
let rt = RustyTube::new();
for (name, id) in [("multilanguage", "tVWWp1PqDus"), ("hdr", "LXb3EKWsInQ")] {
let mut json_path = tf_dir.to_path_buf();
@ -615,11 +507,7 @@ mod tests {
continue;
}
let player_data = rp
.test_query()
.get_player(id, ClientType::Desktop)
.await
.unwrap();
let player_data = rt.get_player(id, ClientType::Desktop).await.unwrap();
let file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(file, &player_data).unwrap();
}
@ -637,17 +525,10 @@ mod tests {
let json_file = File::open(json_path).unwrap();
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = resp
.map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBFUSCATOR))
.unwrap();
let player_data = map_player_data(resp, &DEOBFUSCATOR).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
let is_desktop = name == "desktop" || name == "desktopmusic";
insta::assert_yaml_snapshot!(format!("map_player_data_{}", name), map_res.c, {
insta::assert_yaml_snapshot!(format!("map_player_data_{}", name), player_data, {
".info.publish_date" => insta::dynamic_redaction(move |value, _path| {
if is_desktop {
assert!(value.as_str().unwrap().starts_with("2019-05-30T00:00:00"));
@ -680,12 +561,8 @@ mod tests {
#[case::ios(ClientType::Ios)]
#[test_log::test(tokio::test)]
async fn t_get_player(#[case] client_type: ClientType) {
let rp = RustyPipe::new_test();
let player_data = rp
.test_query()
.get_player("n4tK7LYFxI0", client_type)
.await
.unwrap();
let rt = RustyTube::new();
let player_data = rt.get_player("n4tK7LYFxI0", client_type).await.unwrap();
// dbg!(&player_data);
@ -707,12 +584,7 @@ mod tests {
assert_eq!(player_data.info.is_live_content, false);
if client_type == ClientType::Desktop || client_type == ClientType::DesktopMusic {
assert!(player_data
.info
.publish_date
.unwrap()
.to_string()
.starts_with("2013-05-05 00:00:00"));
assert!(player_data.info.publish_date.unwrap().to_string().starts_with("2013-05-05 00:00:00"));
assert_eq!(player_data.info.category.unwrap(), "Music");
assert_eq!(player_data.info.is_family_safe.unwrap(), true);
}
@ -732,7 +604,7 @@ mod tests {
// Bitrates may change between requests
assert_approx(video.bitrate as f64, 1507068.0);
assert_eq!(video.average_bitrate, 1345149);
assert_eq!(video.size.unwrap(), 43553412);
assert_eq!(video.size, 43553412);
assert_eq!(video.width, 1280);
assert_eq!(video.height, 720);
assert_eq!(video.fps, 30);
@ -762,7 +634,7 @@ mod tests {
assert_approx(video.bitrate as f64, 1340829.0);
assert_approx(video.average_bitrate as f64, 1233444.0);
assert_approx(video.size.unwrap() as f64, 39936630.0);
assert_approx(video.size as f64, 39936630.0);
assert_eq!(video.width, 1280);
assert_eq!(video.height, 720);
assert_eq!(video.fps, 30);
@ -789,20 +661,15 @@ mod tests {
fn t_cipher_to_url() {
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
let map_res = map_url(
let (url, throttled) = map_url(
&None,
&Some(signature_cipher.to_owned()),
&DEOBFUSCATOR,
&mut last_nsig,
);
let (url, throttled) = map_res.c.unwrap();
)
.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!(throttled, false);
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
}
}

View file

@ -1,15 +1,14 @@
use anyhow::{anyhow, bail, Result};
use anyhow::{anyhow, Context, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{
deobfuscate::Deobfuscator,
model::{Channel, Language, Playlist, Thumbnail, Video},
serializer::text::{PageType, TextLink},
timeago, util,
};
use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery};
use super::{response, ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
@ -25,44 +24,62 @@ struct QPlaylistCont {
continuation: String,
}
impl RustyPipeQuery {
pub async fn get_playlist(self, playlist_id: &str) -> Result<Playlist> {
let context = self.get_context(ClientType::Desktop, true).await;
impl RustyTube {
pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QPlaylist {
context,
browse_id: "VL".to_owned() + playlist_id,
};
self.execute_request::<response::Playlist, _, _>(
ClientType::Desktop,
"get_playlist",
Method::POST,
"browse",
playlist_id,
&request_body,
)
.await
let resp = client
.request_builder(Method::POST, "browse")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
let resp_body = resp.text().await?;
let playlist_response =
serde_json::from_str::<response::Playlist>(&resp_body).context(resp_body)?;
map_playlist(&playlist_response, self.localization.language)
}
pub async fn get_playlist_cont(self, playlist: &mut Playlist) -> Result<()> {
pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> {
match &playlist.ctoken {
Some(ctoken) => {
let context = self.get_context(ClientType::Desktop, true).await;
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QPlaylistCont {
context,
continuation: ctoken.to_owned(),
};
let (mut videos, ctoken) = self
.execute_request::<response::PlaylistCont, _, _>(
ClientType::Desktop,
"get_playlist_cont",
Method::POST,
"browse",
&playlist.id,
&request_body,
)
.await?;
let resp = client
.request_builder(Method::POST, "browse")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
let cont_response = resp.json::<response::playlist::PlaylistCont>().await?;
let action = some_or_bail!(
cont_response
.on_response_received_actions
.iter()
.find(|a| a.append_continuation_items_action.target_id == playlist.id),
Err(anyhow!("no continuation action"))
);
let (mut videos, ctoken) =
map_playlist_items(&action.append_continuation_items_action.continuation_items);
playlist.videos.append(&mut videos);
playlist.ctoken = ctoken;
@ -78,179 +95,149 @@ impl RustyPipeQuery {
}
}
impl MapResponse<Playlist> for response::Playlist {
fn map_response(
self,
id: &str,
lang: Language,
_deobf: Option<&Deobfuscator>,
) -> Result<MapResult<Playlist>> {
let video_items = &some_or_bail!(
fn map_playlist(response: &response::Playlist, lang: Language) -> Result<Playlist> {
let video_items = &some_or_bail!(
some_or_bail!(
some_or_bail!(
some_or_bail!(
self.contents
.two_column_browse_results_renderer
.contents
.get(0),
Err(anyhow!("twoColumnBrowseResultsRenderer empty"))
)
.tab_renderer
.content
.section_list_renderer
.contents
.get(0),
Err(anyhow!("sectionListRenderer empty"))
response
.contents
.two_column_browse_results_renderer
.contents
.get(0),
Err(anyhow!("twoColumnBrowseResultsRenderer empty"))
)
.item_section_renderer
.tab_renderer
.content
.section_list_renderer
.contents
.get(0),
Err(anyhow!("itemSectionRenderer empty"))
Err(anyhow!("sectionListRenderer empty"))
)
.playlist_video_list_renderer
.contents;
.item_section_renderer
.contents
.get(0),
Err(anyhow!("itemSectionRenderer empty"))
)
.playlist_video_list_renderer
.contents;
let (videos, ctoken) = map_playlist_items(&video_items.c);
let (videos, ctoken) = map_playlist_items(video_items);
let (thumbnails, last_update_txt) = match &self.sidebar {
Some(sidebar) => {
let primary = some_or_bail!(
sidebar.playlist_sidebar_renderer.items.get(0),
Err(anyhow!("no primary sidebar"))
);
let (thumbnails, last_update_txt) = match &response.sidebar {
Some(sidebar) => {
let primary = some_or_bail!(
sidebar.playlist_sidebar_renderer.items.get(0),
Err(anyhow!("no primary sidebar"))
);
(
&primary
.playlist_sidebar_primary_info_renderer
.thumbnail_renderer
.playlist_video_thumbnail_renderer
.thumbnail
.thumbnails,
primary
.playlist_sidebar_primary_info_renderer
.stats
.get(2)
.map(|t| t.to_owned()),
)
}
None => {
let header_banner = some_or_bail!(
&self.header.playlist_header_renderer.playlist_header_banner,
Err(anyhow!("no thumbnail found"))
);
let last_update_txt = self
(
&primary
.playlist_sidebar_primary_info_renderer
.thumbnail_renderer
.playlist_video_thumbnail_renderer
.thumbnail
.thumbnails,
primary
.playlist_sidebar_primary_info_renderer
.stats
.get(2)
.map(|t| t.to_owned()),
)
}
None => {
let header_banner = some_or_bail!(
&response
.header
.playlist_header_renderer
.byline
.get(1)
.map(|b| b.playlist_byline_renderer.text.to_owned());
.playlist_header_banner,
Err(anyhow!("no thumbnail found"))
);
(
&header_banner
.hero_playlist_thumbnail_renderer
.thumbnail
.thumbnails,
last_update_txt,
)
}
};
let last_update_txt = response
.header
.playlist_header_renderer
.byline
.get(1)
.map(|b| b.playlist_byline_renderer.text.to_owned());
let thumbnails = thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect::<Vec<_>>();
let n_videos = match ctoken {
Some(_) => {
ok_or_bail!(
util::parse_numeric(&self.header.playlist_header_renderer.num_videos_text),
Err(anyhow!("no video count"))
)
}
None => videos.len() as u32,
};
let playlist_id = self.header.playlist_header_renderer.playlist_id;
if playlist_id != id {
bail!("got wrong playlist id {}, expected {}", playlist_id, id);
}
let name = self.header.playlist_header_renderer.title;
let description = self.header.playlist_header_renderer.description_text;
let channel = match self.header.playlist_header_renderer.owner_text {
Some(TextLink::Browse {
text,
page_type: PageType::Channel,
browse_id,
}) => Some(Channel {
id: browse_id,
name: text,
}),
_ => None,
};
let mut warnings = video_items.warnings.to_owned();
let last_update = match &last_update_txt {
Some(textual_date) => {
let parsed = timeago::parse_textual_date_to_dt(lang, textual_date);
if parsed.is_none() {
warnings.push(format!("could not parse textual date `{}`", textual_date));
}
parsed
}
None => None,
};
Ok(MapResult {
c: Playlist {
id: playlist_id,
name,
videos,
n_videos,
ctoken,
thumbnails,
description,
channel,
last_update,
(
&header_banner
.hero_playlist_thumbnail_renderer
.thumbnail
.thumbnails,
last_update_txt,
)
}
};
let thumbnails = thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect::<Vec<_>>();
let n_videos = match ctoken {
Some(_) => {
ok_or_bail!(
util::parse_numeric(&response.header.playlist_header_renderer.num_videos_text),
Err(anyhow!("no video count"))
)
}
None => videos.len() as u32,
};
let id = response
.header
.playlist_header_renderer
.playlist_id
.to_owned();
let name = response.header.playlist_header_renderer.title.to_owned();
let description = response
.header
.playlist_header_renderer
.description_text
.to_owned();
let channel = match &response.header.playlist_header_renderer.owner_text {
Some(owner_text) => match owner_text {
TextLink::Browse {
text,
page_type,
browse_id,
} => match page_type {
PageType::Channel => Some(Channel {
id: browse_id.to_owned(),
name: text.to_owned(),
}),
_ => None,
},
warnings,
})
}
}
_ => None,
},
None => None,
};
impl MapResponse<(Vec<Video>, Option<String>)> for response::PlaylistCont {
fn map_response(
self,
id: &str,
_lang: Language,
_deobf: Option<&Deobfuscator>,
) -> Result<MapResult<(Vec<Video>, Option<String>)>> {
let action = some_or_bail!(
self.on_response_received_actions
.iter()
.find(|a| a.append_continuation_items_action.target_id == id),
Err(anyhow!("no continuation action"))
);
Ok(MapResult {
c: map_playlist_items(&action.append_continuation_items_action.continuation_items.c),
warnings: action
.append_continuation_items_action
.continuation_items
.warnings
.to_owned(),
})
}
Ok(Playlist {
id,
name,
videos,
n_videos,
ctoken,
thumbnails,
description,
channel,
last_update: match &last_update_txt {
Some(textual_date) => timeago::parse_textual_date_to_dt(lang, textual_date),
None => None,
},
last_update_txt,
})
}
fn map_playlist_items(
items: &[response::VideoListItem<response::playlist::PlaylistVideo>],
items: &Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
) -> (Vec<Video>, Option<String>) {
let mut ctoken: Option<String> = None;
let videos = items
@ -259,27 +246,30 @@ fn map_playlist_items(
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
TextLink::Browse {
text,
page_type: PageType::Channel,
page_type,
browse_id,
} => Some(Video {
id: video.video_id.to_owned(),
title: video.title.to_owned(),
length: video.length_seconds,
thumbnails: video
.thumbnail
.thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect(),
channel: Channel {
id: browse_id.to_string(),
name: text.to_owned(),
},
}),
} => match page_type {
PageType::Channel => Some(Video {
id: video.video_id.to_owned(),
title: video.title.to_owned(),
length: video.length_seconds,
thumbnails: video
.thumbnail
.thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect(),
channel: Channel {
id: browse_id.to_string(),
name: text.to_owned(),
},
}),
_ => None,
},
_ => None,
},
response::VideoListItem::ContinuationItemRenderer {
@ -299,10 +289,49 @@ mod tests {
use rstest::rstest;
use crate::{client::RustyPipe, report::TestFileReporter};
use super::*;
#[test_log::test(tokio::test)]
async fn download_testfiles() {
let tf_dir = Path::new("testfiles/playlist");
let rt = RustyTube::new();
for (name, id) in [
("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"),
("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ"),
("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe"),
] {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("playlist_{}.json", name));
if json_path.exists() {
continue;
}
let client = rt.get_ytclient(ClientType::Desktop);
let context = client.get_context(false).await;
let request_body = QPlaylist {
context,
browse_id: "VL".to_owned() + id,
};
let resp = client
.request_builder(Method::POST, "browse")
.await
.json(&request_body)
.send()
.await
.unwrap()
.error_for_status()
.unwrap();
let mut file = std::fs::File::create(json_path).unwrap();
let mut content = std::io::Cursor::new(resp.bytes().await.unwrap());
std::io::copy(&mut content, &mut file).unwrap();
}
}
#[rstest]
#[case::long(
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
@ -339,8 +368,8 @@ mod tests {
#[case] description: Option<String>,
#[case] channel: Option<Channel>,
) {
let rp = RustyPipe::new_test();
let playlist = rp.test_query().get_playlist(id).await.unwrap();
let rt = RustyTube::new();
let playlist = rt.get_playlist(id).await.unwrap();
assert_eq!(playlist.id, id);
assert_eq!(playlist.name, name);
@ -349,70 +378,37 @@ mod tests {
assert!(playlist.n_videos > 10);
assert_eq!(playlist.n_videos > 100, is_long);
assert_eq!(playlist.description, description);
if channel.is_some() {
assert_eq!(playlist.channel, channel);
}
assert_eq!(playlist.channel, channel);
assert!(!playlist.thumbnails.is_empty());
}
#[test_log::test(tokio::test)]
async fn download_testfiles() {
let tf_dir = Path::new("testfiles/playlist");
for (name, id) in [
("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"),
("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ"),
("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe"),
] {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("playlist_{}.json", name));
if json_path.exists() {
continue;
}
let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
rp.test_query().report(true).get_playlist(id).await.unwrap();
}
}
#[rstest]
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
fn t_map_playlist_data(#[case] name: &str, #[case] id: &str) {
#[case::long("long")]
#[case::short("short")]
#[case::nomusic("nomusic")]
fn t_map_playlist_data(#[case] name: &str) {
let filename = format!("testfiles/playlist/playlist_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let playlist: response::Playlist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = playlist.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), map_res.c, {
let playlist_data = map_playlist(&playlist, Language::En).unwrap();
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), playlist_data, {
".last_update" => "[date]"
});
}
#[test_log::test(tokio::test)]
async fn t_playlist_cont() {
let rp = RustyPipe::new_test();
let mut playlist = rp
.test_query()
let rt = RustyTube::new();
let mut playlist = rt
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
.await
.unwrap();
while playlist.ctoken.is_some() {
rp.test_query()
.get_playlist_cont(&mut playlist)
.await
.unwrap();
rt.get_playlist_cont(&mut playlist).await.unwrap();
}
assert!(playlist.videos.len() > 100);

View file

@ -4,7 +4,6 @@ use serde_with::VecSkipError;
use super::TimeOverlay;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
use crate::serializer::text::Text;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -64,11 +63,11 @@ pub struct GridRenderer {
pub struct ChannelVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde_as(as = "Option<Text>")]
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count_text: String,
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>,

View file

@ -7,7 +7,6 @@ pub mod video;
pub use channel::Channel;
pub use player::Player;
pub use playlist::Playlist;
pub use playlist::PlaylistCont;
pub use playlist_music::PlaylistMusic;
pub use video::Video;
pub use video::VideoComments;
@ -16,7 +15,7 @@ pub use video::VideoRecommendations;
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextLink, TextLinks};
use crate::serializer::text::TextLink;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -94,10 +93,10 @@ pub struct VideoOwner {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwnerRenderer {
#[serde_as(as = "TextLink")]
#[serde_as(as = "crate::serializer::text::TextLink")]
pub title: TextLink,
pub thumbnail: Thumbnails,
#[serde_as(as = "Option<Text>")]
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub subscriber_count_text: Option<String>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
@ -133,7 +132,7 @@ pub struct TimeOverlay {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlayRenderer {
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
@ -199,7 +198,7 @@ pub struct MusicColumn {
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
pub struct MusicColumnRenderer {
#[serde_as(as = "TextLinks")]
#[serde_as(as = "crate::serializer::text::TextLinks")]
pub text: Vec<TextLink>,
}
@ -214,23 +213,3 @@ pub struct MusicContinuation {
pub struct MusicContinuationData {
pub continuation: String,
}
impl From<Thumbnail> for crate::model::Thumbnail {
fn from(tn: Thumbnail) -> Self {
crate::model::Thumbnail {
url: tn.url,
width: tn.width,
height: tn.height,
}
}
}
impl From<Thumbnails> for Vec<crate::model::Thumbnail> {
fn from(ts: Thumbnails) -> Self {
let mut thumbnails = vec![];
for t in ts.thumbnails {
thumbnails.push(t.into());
}
thumbnails
}
}

View file

@ -3,10 +3,9 @@ use std::ops::Range;
use chrono::NaiveDate;
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError};
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use super::Thumbnails;
use crate::serializer::{text::Text, MapResult, VecLogError};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -46,11 +45,11 @@ pub struct StreamingData {
#[serde_as(as = "JsonString")]
pub expires_in_seconds: u32,
#[serde(default)]
#[serde_as(as = "VecLogError<_>")]
pub formats: MapResult<Vec<Format>>,
#[serde_as(as = "VecSkipError<_>")]
pub formats: Vec<Format>,
#[serde(default)]
#[serde_as(as = "VecLogError<_>")]
pub adaptive_formats: MapResult<Vec<Format>>,
#[serde_as(as = "VecSkipError<_>")]
pub adaptive_formats: Vec<Format>,
/// Only on livestreams
pub dash_manifest_url: Option<String>,
/// Only on livestreams
@ -74,20 +73,20 @@ pub struct Format {
pub width: Option<u32>,
pub height: Option<u32>,
#[serde_as(as = "Option<crate::serializer::Range>")]
#[serde_as(as = "Option<crate::serializer::range::Range>")]
pub index_range: Option<Range<u32>>,
#[serde_as(as = "Option<crate::serializer::Range>")]
#[serde_as(as = "Option<crate::serializer::range::Range>")]
pub init_range: Option<Range<u32>>,
#[serde_as(as = "Option<JsonString>")]
pub content_length: Option<u64>,
#[serde_as(as = "JsonString")]
pub content_length: u64,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub quality: Option<Quality>,
pub fps: Option<u8>,
pub quality_label: Option<String>,
pub average_bitrate: Option<u32>,
pub average_bitrate: u32,
pub color_info: Option<ColorInfo>,
// Audio only
@ -105,9 +104,7 @@ pub struct Format {
impl Format {
pub fn is_audio(&self) -> bool {
self.content_length.is_some()
&& self.audio_quality.is_some()
&& self.audio_sample_rate.is_some()
self.audio_quality.is_some() && self.audio_sample_rate.is_some()
}
pub fn is_video(&self) -> bool {
@ -191,7 +188,7 @@ pub struct PlayerCaptionsTracklistRenderer {
#[serde(rename_all = "camelCase")]
pub struct CaptionTrack {
pub base_url: String,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub name: String,
pub language_code: String,
}

View file

@ -2,8 +2,7 @@ use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use crate::serializer::text::{Text, TextLink};
use crate::serializer::{MapResult, VecLogError};
use crate::serializer::text::TextLink;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
@ -57,8 +56,8 @@ pub struct PlaylistVideoListRenderer {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideoList {
#[serde_as(as = "VecLogError<_>")]
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<VideoListItem<PlaylistVideo>>,
}
#[serde_as]
@ -67,10 +66,10 @@ pub struct PlaylistVideoList {
pub struct PlaylistVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
#[serde_as(as = "TextLink")]
#[serde_as(as = "crate::serializer::text::TextLink")]
pub channel: TextLink,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
@ -87,14 +86,14 @@ pub struct Header {
#[serde(rename_all = "camelCase")]
pub struct HeaderRenderer {
pub playlist_id: String,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(default)]
#[serde_as(as = "DefaultOnError<Option<Text>>")]
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
pub description_text: Option<String>,
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub num_videos_text: String,
#[serde_as(as = "Option<TextLink>")]
#[serde_as(as = "Option<crate::serializer::text::TextLink>")]
pub owner_text: Option<TextLink>,
// Alternative layout
@ -119,7 +118,7 @@ pub struct Byline {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BylineRenderer {
#[serde_as(as = "Text")]
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
}
@ -151,7 +150,7 @@ pub struct SidebarPrimaryInfoRenderer {
// - `"495", " videos"`
// - `"3,310,996 views"`
// - `"Last updated on ", "Aug 7, 2022"`
#[serde_as(as = "Vec<Text>")]
#[serde_as(as = "Vec<crate::serializer::text::Text>")]
pub stats: Vec<String>,
}
@ -173,7 +172,7 @@ pub struct OnResponseReceivedAction {
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendAction {
#[serde_as(as = "VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
#[serde_as(as = "VecSkipError<_>")]
pub continuation_items: Vec<VideoListItem<PlaylistVideo>>,
pub target_id: String,
}

View file

@ -1,5 +1,3 @@
#![allow(clippy::enum_variant_names)]
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{DefaultOnError, VecSkipError};

View file

@ -1,6 +1,6 @@
---
source: src/client/player.rs
expression: map_res.c
expression: player_data
---
info:
id: pPvd8UxmSbQ
@ -184,22 +184,6 @@ video_only_streams:
format: mp4
codec: av01
throttled: false
- 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"
itag: 22
bitrate: 1574434
average_bitrate: 1574434
size: ~
index_range: ~
init_range: ~
width: 1280
height: 720
fps: 30
quality: 720p
hdr: false
mime: "video/mp4; codecs=\"avc1.64001F, mp4a.40.2\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 398
bitrate: 1348419

View file

@ -1,6 +1,6 @@
---
source: src/client/player.rs
expression: map_res.c
expression: player_data
---
info:
id: pPvd8UxmSbQ

View file

@ -1,6 +1,6 @@
---
source: src/client/player.rs
expression: map_res.c
expression: player_data
---
info:
id: pPvd8UxmSbQ

View file

@ -1,6 +1,6 @@
---
source: src/client/player.rs
expression: map_res.c
expression: player_data
---
info:
id: pPvd8UxmSbQ

View file

@ -1,6 +1,6 @@
---
source: src/client/player.rs
expression: map_res.c
expression: player_data
---
info:
id: pPvd8UxmSbQ

View file

@ -1,6 +1,6 @@
---
source: src/client/playlist.rs
expression: map_res.c
expression: playlist_data
---
id: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022

View file

@ -1,6 +1,6 @@
---
source: src/client/playlist.rs
expression: map_res.c
expression: playlist_data
---
id: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
name: Minecraft SHINE

View file

@ -1,6 +1,6 @@
---
source: src/client/playlist.rs
expression: map_res.c
expression: playlist_data
---
id: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk
name: Easy Pop

111
src/client/video.rs Normal file
View file

@ -0,0 +1,111 @@
use anyhow::Result;
use reqwest::Method;
use serde::Serialize;
use super::{response, ClientType, ContextYT, RustyTube};
#[derive(Clone, Debug, Serialize)]
struct QVideo {
context: ContextYT,
/// YouTube video ID
video_id: String,
/// Set to true to allow extraction of streams with sensitive content
content_check_ok: bool,
/// Probably refers to allowing sensitive content, too
racy_check_ok: bool,
}
#[derive(Clone, Debug, Serialize)]
struct QVideoCont {
context: ContextYT,
continuation: String,
}
impl RustyTube {
async fn get_video_response(&self, video_id: &str) -> Result<response::Video> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideo {
context,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::Video>().await?)
}
async fn get_comments_response(&self, ctoken: &str) -> Result<response::VideoComments> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::VideoComments>().await?)
}
async fn get_recommendations_response(
&self,
ctoken: &str,
) -> Result<response::VideoRecommendations> {
let client = self.get_ytclient(ClientType::Desktop);
let context = client.get_context(true).await;
let request_body = QVideoCont {
context,
continuation: ctoken.to_owned(),
};
let resp = client
.request_builder(Method::POST, "next")
.await
.json(&request_body)
.send()
.await?
.error_for_status()?;
Ok(resp.json::<response::VideoRecommendations>().await?)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn t_get_video_response() {
let rt = RustyTube::new();
// rt.get_video("ZeerrnuLi5E").await.unwrap();
dbg!(rt.get_video_response("iQfSvIgIs_M").await.unwrap());
}
#[tokio::test]
async fn t_get_comments_response() {
let rt = RustyTube::new();
// rt.get_comments("Eg0SC2lRZlN2SWdJc19NGAYyJSIRIgtpUWZTdklnSXNfTTAAeAJCEGNvbW1lbnRzLXNlY3Rpb24%3D").await.unwrap();
dbg!(rt.get_comments_response("Eg0SC2lRZlN2SWdJc19NGAYychpFEhRVZ2lnVGJVTEZ6Qk5FWGdDb0FFQyICCAAqGFVDWFgwUldPSUJqdDRvM3ppSHUtNmE1QTILaVFmU3ZJZ0lzX01AAUgKQiljb21tZW50LXJlcGxpZXMtaXRlbS1VZ2lnVGJVTEZ6Qk5FWGdDb0FFQw%3D%3D").await.unwrap());
}
#[tokio::test]
async fn t_get_recommendations_response() {
let rt = RustyTube::new();
dbg!(rt.get_recommendations_response("CBQSExILaVFmU3ZJZ0lzX03AAQHIAQEYACqkBjJzNkw2d3pVQkFyUkJBb0Q4ajRBQ2c3Q1Bnc0lvWXlRejhLZnRZUGNBUW9EOGo0QUNnN0NQZ3NJeElEX2w0YjFtNnUtQVFvRDhqNEFDZzNDUGdvSXg5Ykx3WUNKenFwX0NnUHlQZ0FLRGNJLUNnaW83T2pqZzVPTHZEOEtBX0ktQUFvTndqNEtDTE9venZmQThybVhXd29EOGo0QUNnM0NQZ29JdzZETV9vSFk0cHRCQ2dQeVBnQUtEc0ktQ3dqbW9QbURpcHVPel80QkNnUHlQZ0FLRGNJLUNnalY4THpEazlfOTRCWUtBX0ktQUFvT3dqNExDTXVZNU9YZzE3ejV2d0VLQV9JLUFBb053ajRLQ1A3eHZiSGswTnVuYWdvRDhqNEFDZzdDUGdzSXFQYVU5ZGp2Ml96S0FRb0Q4ajRBQ2c3Q1Bnc0lfSW1acUtQOTlfQ09BUW9EOGo0QUNnM0NQZ29JeGRtNzlZS3prcUFqQ2dQeVBnQUtEY0ktQ2dpZ3FJMkg0UENRX2s0S0FfSS1BQW9Pd2o0TENQV0V5NV9ZeDhERl9nRUtBX0ktQUFvT3dqNExDTzJid3VuV3BPX3ppd0VLQV9JLUFBb2gwajRlQ2h4U1JFTk5WVU5ZV0RCU1YwOUpRbXAwTkc4emVtbElkUzAyWVRWQkNnUHlQZ0FLRGNJLUNnaXpqcXZwcDh5MWwwMEtBX0ktQUFvTndqNEtDTFhWbl83dHhfWDJOUW9EOGo0QUNnN0NQZ3NJNWR5ZWc1NjZyUGUwQVJJVUFBSUVCZ2dLREE0UUVoUVdHQm9jSGlBaUpDWWFCQWdBRUFFYUJBZ0NFQU1hQkFnRUVBVWFCQWdHRUFjYUJBZ0lFQWthQkFnS0VBc2FCQWdNRUEwYUJBZ09FQThhQkFnUUVCRWFCQWdTRUJNYUJBZ1VFQlVhQkFnV0VCY2FCQWdZRUJrYUJBZ2FFQnNhQkFnY0VCMGFCQWdlRUI4YUJBZ2dFQ0VhQkFnaUVDTWFCQWdrRUNVYUJBZ21FQ2NxRkFBQ0JBWUlDZ3dPRUJJVUZoZ2FIQjRnSWlRbWoPd2F0Y2gtbmV4dC1mZWVk").await.unwrap());
}
}

534
src/client2/mod.rs Normal file
View file

@ -0,0 +1,534 @@
pub mod player;
pub mod playlist;
mod response;
use std::fmt::Debug;
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use fancy_regex::Regex;
use once_cell::sync::Lazy;
use rand::Rng;
use reqwest::{header, Client, ClientBuilder, Method, RequestBuilder};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use crate::{
cache::Cache,
deobfuscate::Deobfuscator,
model::{Country, Language},
report::{Level, Report, Reporter, YamlFileReporter},
};
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum ClientType {
Desktop,
DesktopMusic,
TvHtml5Embed,
Android,
Ios,
}
const CLIENT_TYPES: [ClientType; 5] = [
ClientType::Desktop,
ClientType::DesktopMusic,
ClientType::TvHtml5Embed,
ClientType::Android,
ClientType::Ios,
];
impl ClientType {
fn is_web(&self) -> bool {
match self {
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true,
ClientType::Android | ClientType::Ios => false,
}
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ContextYT {
client: ClientInfo,
/// only used on desktop
#[serde(skip_serializing_if = "Option::is_none")]
request: Option<RequestYT>,
user: User,
/// only used for the embedded player
#[serde(skip_serializing_if = "Option::is_none")]
third_party: Option<ThirdParty>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ClientInfo {
client_name: String,
client_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
client_screen: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
device_model: Option<String>,
platform: String,
#[serde(skip_serializing_if = "Option::is_none")]
original_url: Option<String>,
hl: Language,
gl: Country,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RequestYT {
internal_experiment_flags: Vec<String>,
use_ssl: bool,
}
impl Default for RequestYT {
fn default() -> Self {
Self {
internal_experiment_flags: vec![],
use_ssl: true,
}
}
}
#[derive(Clone, Debug, Serialize, Default)]
#[serde(rename_all = "camelCase")]
struct User {
// TO DO: provide a way to enable restricted mode with:
// "enableSafetyMode": true
locked_safety_mode: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ThirdParty {
embed_url: String,
}
const DEFAULT_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0";
const CONSENT_COOKIE: &str = "CONSENT";
const CONSENT_COOKIE_YES: &str = "YES+yt.462272069.de+FX+";
const YOUTUBEI_V1_URL: &str = "https://www.youtube.com/youtubei/v1/";
const YOUTUBEI_V1_GAPIS_URL: &str = "https://youtubei.googleapis.com/youtubei/v1/";
const YOUTUBE_MUSIC_V1_URL: &str = "https://music.youtube.com/youtubei/v1/";
const DISABLE_PRETTY_PRINT_PARAMETER: &str = "&prettyPrint=false";
const DESKTOP_CLIENT_VERSION: &str = "2.20220909.00.00";
const DESKTOP_API_KEY: &str = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8";
const TVHTML5_CLIENT_VERSION: &str = "2.0";
const DESKTOP_MUSIC_API_KEY: &str = "AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30";
const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20220831.01.02";
const MOBILE_CLIENT_VERSION: &str = "17.29.35";
const ANDROID_API_KEY: &str = "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w";
const IOS_API_KEY: &str = "AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc";
const IOS_DEVICE_MODEL: &str = "iPhone14,5";
static CLIENT_VERSION_REGEXES: Lazy<[Regex; 1]> =
Lazy::new(|| [Regex::new("INNERTUBE_CONTEXT_CLIENT_VERSION\":\"([0-9\\.]+?)\"").unwrap()]);
#[derive(Clone)]
pub struct RustyPipe {
inner: Arc<RustyPipeRef>,
opts: RustyPipeOpts,
}
struct RustyPipeRef {
http: Client,
cache: Cache,
reporter: Option<Box<dyn Reporter>>,
user_agent: String,
consent_cookie: String,
}
#[derive(Clone)]
struct RustyPipeOpts {
lang: Language,
country: Country,
report: bool,
}
impl Default for RustyPipe {
fn default() -> Self {
Self::new(
Some(Cache::from_json_file("RustyPipeCache.json")),
Some(Box::new(YamlFileReporter::default())),
None,
)
}
}
impl Default for RustyPipeOpts {
fn default() -> Self {
Self {
lang: Language::En,
country: Country::Us,
report: false,
}
}
}
impl RustyPipe {
pub fn new(
cache: Option<Cache>,
reporter: Option<Box<dyn Reporter>>,
user_agent: Option<String>,
) -> Self {
let cache = cache.unwrap_or_else(|| Cache::default());
let user_agent = user_agent.unwrap_or(DEFAULT_UA.to_owned());
let http = ClientBuilder::new()
.user_agent(user_agent.to_owned())
.gzip(true)
.brotli(true)
.build()
.expect("unable to build the HTTP client");
RustyPipe {
inner: Arc::new(RustyPipeRef {
http,
cache,
reporter,
user_agent,
consent_cookie: format!(
"{}={}{}",
CONSENT_COOKIE,
CONSENT_COOKIE_YES,
rand::thread_rng().gen_range(100..1000)
),
}),
opts: RustyPipeOpts::default(),
}
}
pub fn lang(mut self, lang: Language) -> Self {
self.opts.lang = lang;
self
}
pub fn country(mut self, country: Country) -> Self {
self.opts.country = country;
self
}
pub fn report(mut self, report: bool) -> Self {
self.opts.report = report;
self
}
async fn get_context(&self, ctype: ClientType, localized: bool) -> ContextYT {
let hl = match localized {
true => self.opts.lang,
false => Language::En,
};
let gl = match localized {
true => self.opts.country,
false => Country::Us,
};
match ctype {
ClientType::Desktop => ContextYT {
client: ClientInfo {
client_name: "WEB".to_owned(),
client_version: DESKTOP_CLIENT_VERSION.to_owned(),
client_screen: None,
device_model: None,
platform: "DESKTOP".to_owned(),
original_url: Some("https://www.youtube.com/".to_owned()),
hl,
gl,
},
request: Some(RequestYT::default()),
user: User::default(),
third_party: None,
},
ClientType::DesktopMusic => ContextYT {
client: ClientInfo {
client_name: "WEB_REMIX".to_owned(),
client_version: DESKTOP_MUSIC_CLIENT_VERSION.to_owned(),
client_screen: None,
device_model: None,
platform: "DESKTOP".to_owned(),
original_url: Some("https://music.youtube.com/".to_owned()),
hl,
gl,
},
request: Some(RequestYT::default()),
user: User::default(),
third_party: None,
},
ClientType::TvHtml5Embed => ContextYT {
client: ClientInfo {
client_name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER".to_owned(),
client_version: TVHTML5_CLIENT_VERSION.to_owned(),
client_screen: Some("EMBED".to_owned()),
device_model: None,
platform: "TV".to_owned(),
original_url: None,
hl,
gl,
},
request: Some(RequestYT::default()),
user: User::default(),
third_party: Some(ThirdParty {
embed_url: "https://www.youtube.com/".to_owned(),
}),
},
ClientType::Android => ContextYT {
client: ClientInfo {
client_name: "ANDROID".to_owned(),
client_version: MOBILE_CLIENT_VERSION.to_owned(),
client_screen: None,
device_model: None,
platform: "MOBILE".to_owned(),
original_url: None,
hl,
gl,
},
request: None,
user: User::default(),
third_party: None,
},
ClientType::Ios => ContextYT {
client: ClientInfo {
client_name: "IOS".to_owned(),
client_version: MOBILE_CLIENT_VERSION.to_owned(),
client_screen: None,
device_model: Some(IOS_DEVICE_MODEL.to_owned()),
platform: "MOBILE".to_owned(),
original_url: None,
hl,
gl,
},
request: None,
user: User::default(),
third_party: None,
},
}
}
async fn request_builder(
&self,
ctype: ClientType,
method: Method,
endpoint: &str,
) -> RequestBuilder {
match ctype {
ClientType::Desktop => self
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
),
)
.header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned())
.header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", DESKTOP_CLIENT_VERSION),
ClientType::DesktopMusic => self
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBE_MUSIC_V1_URL,
endpoint,
DESKTOP_MUSIC_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
),
)
.header(header::ORIGIN, "https://music.youtube.com")
.header(header::REFERER, "https://music.youtube.com")
.header(header::COOKIE, self.inner.consent_cookie.to_owned())
.header("X-YouTube-Client-Name", "67")
.header("X-YouTube-Client-Version", DESKTOP_MUSIC_CLIENT_VERSION),
ClientType::TvHtml5Embed => self
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_URL, endpoint, DESKTOP_API_KEY, DISABLE_PRETTY_PRINT_PARAMETER
),
)
.header(header::ORIGIN, "https://www.youtube.com")
.header(header::REFERER, "https://www.youtube.com")
.header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
ClientType::Android => self
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL,
endpoint,
ANDROID_API_KEY,
DISABLE_PRETTY_PRINT_PARAMETER
),
)
.header(
header::USER_AGENT,
format!(
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
MOBILE_CLIENT_VERSION, self.opts.country
),
)
.header("X-Goog-Api-Format-Version", "2"),
ClientType::Ios => self
.inner
.http
.request(
method,
format!(
"{}{}?key={}{}",
YOUTUBEI_V1_GAPIS_URL,
endpoint,
IOS_API_KEY,
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"),
}
}
async fn execute_request<
R: DeserializeOwned + MapResponse<M> + Debug,
M,
B: Serialize + ?Sized,
>(
&self,
ctype: ClientType,
operation: &str,
method: Method,
endpoint: &str,
id: &str,
body: &B,
deobf: Option<&Deobfuscator>,
) -> Result<M> {
let request = self
.request_builder(ctype, method.clone(), endpoint)
.await
.json(body)
.build()?;
let request_url = request.url().to_string();
let request_headers = request.headers().to_owned();
let response = self.inner.http.execute(request).await?;
let status = response.status();
let resp_str = response.text().await?;
let create_report = |level: Level, error: Option<String>, msgs: Vec<String>| {
if let Some(reporter) = &self.inner.reporter {
let report = Report {
package: "rustypipe".to_owned(),
version: "0.1.0".to_owned(),
date: chrono::Local::now(),
level,
operation: operation.to_owned(),
error,
msgs,
http_request: crate::report::HTTPRequest {
url: request_url,
method: method.to_string(),
req_header: request_headers
.iter()
.map(|(k, v)| {
(k.to_string(), v.to_str().unwrap_or_default().to_owned())
})
.collect(),
req_body: serde_json::to_string(body).unwrap_or_default(),
status: status.into(),
resp_body: resp_str.to_owned(),
},
};
reporter.report(&report);
}
};
if status.is_client_error() || status.is_server_error() {
let e = anyhow!("Server responded with error code {}", status);
create_report(Level::ERR, Some(e.to_string()), vec![]);
return Err(e);
}
match serde_json::from_str::<R>(&resp_str) {
Ok(deserialized) => match deserialized.map_response(id, self.opts.lang, deobf) {
Ok(mapres) => {
if !mapres.warnings.is_empty() {
create_report(
Level::WRN,
Some("Warnings during deserialization/mapping".to_owned()),
mapres.warnings,
);
} else if self.opts.report {
create_report(Level::DBG, None, vec![]);
}
Ok(mapres.c)
}
Err(e) => {
let emsg = "Could not map reponse";
create_report(Level::ERR, Some(emsg.to_owned()), vec![e.to_string()]);
Err(e).context(emsg)
}
},
Err(e) => {
let emsg = "Could not deserialize response";
create_report(Level::ERR, Some(emsg.to_owned()), vec![e.to_string()]);
Err(e).context(emsg)
}
}
}
}
trait MapResponse<T> {
fn map_response(
self,
id: &str,
lang: Language,
deobf: Option<&Deobfuscator>,
) -> Result<MapResult<T>>;
}
#[derive(Clone)]
pub struct MapResult<T> {
pub c: T,
pub warnings: Vec<String>,
}
impl<T> Debug for MapResult<T>
where
T: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.c.fmt(f)
}
}
/*
#[cfg(test)]
mod tests {
use super::*;
}
*/

807
src/client2/player.rs Normal file
View file

@ -0,0 +1,807 @@
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
};
use anyhow::{anyhow, bail, Result};
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
use fancy_regex::Regex;
use once_cell::sync::Lazy;
use reqwest::{Method, Url};
use serde::Serialize;
use crate::{
deobfuscate::Deobfuscator,
model::{
AudioCodec, AudioFormat, AudioStream, AudioTrack, Channel, Language, Subtitle, VideoCodec,
VideoFormat, VideoInfo, VideoPlayer, VideoStream,
},
util,
};
use super::{
response::{self, player},
ClientType, ContextYT, MapResponse, MapResult, RustyPipe,
};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlayer {
context: ContextYT,
/// Website playback context
#[serde(skip_serializing_if = "Option::is_none")]
playback_context: Option<QPlaybackContext>,
/// Content playback nonce (mobile only, 16 random chars)
#[serde(skip_serializing_if = "Option::is_none")]
cpn: Option<String>,
/// YouTube video ID
video_id: String,
/// Set to true to allow extraction of streams with sensitive content
content_check_ok: bool,
/// Probably refers to allowing sensitive content, too
racy_check_ok: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaybackContext {
content_playback_context: QContentPlaybackContext,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QContentPlaybackContext {
/// Signature timestamp extracted from player.js
signature_timestamp: String,
/// Referer URL from website
referer: String,
}
impl RustyPipe {
pub async fn get_player(&self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
let (context, deobf) = tokio::join!(
self.get_context(client_type, false),
Deobfuscator::from_fetched_info(self.inner.http.clone(), self.inner.cache.clone())
);
let deobf = deobf?;
let request_body = if client_type.is_web() {
QPlayer {
context,
playback_context: Some(QPlaybackContext {
content_playback_context: QContentPlaybackContext {
signature_timestamp: deobf.get_sts(),
referer: format!("https://www.youtube.com/watch?v={}", video_id),
},
}),
cpn: None,
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
} else {
QPlayer {
context,
playback_context: None,
cpn: Some(util::generate_content_playback_nonce()),
video_id: video_id.to_owned(),
content_check_ok: true,
racy_check_ok: true,
}
};
self.execute_request::<response::Player, _, _>(
client_type,
"get_player",
Method::POST,
"player",
video_id,
&request_body,
Some(&deobf),
)
.await
}
}
impl MapResponse<VideoPlayer> for response::Player {
fn map_response(
self,
id: &str,
_lang: Language,
deobf: Option<&Deobfuscator>,
) -> Result<super::MapResult<VideoPlayer>> {
let deobf = deobf.unwrap();
let mut warnings = vec![];
// Check playability status
match self.playability_status {
response::player::PlayabilityStatus::Ok { live_streamability } => {
if live_streamability.is_some() {
bail!("Active livestreams are not supported")
}
}
response::player::PlayabilityStatus::Unplayable { reason } => {
bail!("Video is unplayable. Reason: {}", reason)
}
response::player::PlayabilityStatus::LoginRequired { reason } => {
bail!("Playback requires login. Reason: {}", reason)
}
response::player::PlayabilityStatus::LiveStreamOffline { reason } => {
bail!("Livestream is offline. Reason: {}", reason)
}
response::player::PlayabilityStatus::Error { reason } => {
bail!("Video was deleted. Reason: {}", reason)
}
};
let mut streaming_data = some_or_bail!(
self.streaming_data,
Err(anyhow!("No streaming data was returned"))
);
let video_details = some_or_bail!(
self.video_details,
Err(anyhow!("No video details were returned"))
);
let microformat = self.microformat.map(|m| m.player_microformat_renderer);
let (publish_date, category, tags, is_family_safe) =
microformat.map_or((None, None, None, None), |m| {
(
Local
.from_local_datetime(&NaiveDateTime::new(
m.publish_date,
NaiveTime::from_hms(0, 0, 0),
))
.single(),
Some(m.category),
m.tags,
Some(m.is_family_safe),
)
});
if video_details.video_id != id {
bail!(
"got wrong video id {}, expected {}",
video_details.video_id,
id
);
}
let video_info = VideoInfo {
id: video_details.video_id,
title: video_details.title,
description: video_details.short_description,
length: video_details.length_seconds,
thumbnails: video_details.thumbnail.unwrap_or_default().into(),
channel: Channel {
id: video_details.channel_id,
name: video_details.author,
},
publish_date,
view_count: video_details.view_count,
keywords: match video_details.keywords {
Some(keywords) => keywords,
None => tags.unwrap_or_default(),
},
category,
is_live_content: video_details.is_live_content,
is_family_safe,
};
let mut formats = streaming_data.formats;
formats.append(&mut streaming_data.adaptive_formats);
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
let mut video_streams: Vec<VideoStream> = Vec::new();
let mut video_only_streams: Vec<VideoStream> = Vec::new();
let mut audio_streams: Vec<AudioStream> = Vec::new();
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 format: {}", f.itag)),
}
}
video_streams.sort();
video_only_streams.sort();
audio_streams.sort();
let mut subtitles = vec![];
if let Some(captions) = self.captions {
for c in captions.player_captions_tracklist_renderer.caption_tracks {
let lang_auto = c.name.strip_suffix(" (auto-generated)");
subtitles.push(Subtitle {
url: c.base_url,
lang: c.language_code,
lang_name: lang_auto.unwrap_or(&c.name).to_owned(),
auto_generated: lang_auto.is_some(),
})
}
}
Ok(MapResult {
c: VideoPlayer {
info: video_info,
video_streams,
video_only_streams,
audio_streams,
subtitles,
expires_in_seconds: streaming_data.expires_in_seconds,
},
warnings,
})
}
}
fn cipher_to_url_params(
signature_cipher: &str,
deobf: &Deobfuscator,
) -> Result<(String, BTreeMap<String, String>)> {
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 = some_or_bail!(params.get("s"), Err(anyhow!("no s param")));
let sp = some_or_bail!(params.get("sp"), Err(anyhow!("no sp param")));
let raw_url = some_or_bail!(params.get("url"), Err(anyhow!("no url param")));
let (url_base, mut url_params) = util::url_to_params(raw_url)?;
// println!("sig: {}", sig);
let deobf_sig = deobf.deobfuscate_sig(sig)?;
url_params.insert(sp.to_string(), deobf_sig);
Ok((url_base, url_params))
}
fn deobf_nsig(
url_params: &mut BTreeMap<String, String>,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> Result<()> {
let nsig: String;
match url_params.get("n") {
Some(n) => {
nsig = if n.to_owned() == last_nsig[0] {
last_nsig[1].to_owned()
} else {
let nsig = deobf.deobfuscate_nsig(n)?;
last_nsig[0] = n.to_string();
last_nsig[1] = nsig.to_owned();
nsig
};
url_params.insert("n".to_owned(), nsig);
}
None => {}
};
Ok(())
}
fn map_url(
url: &Option<String>,
signature_cipher: &Option<String>,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<(String, bool)>> {
let (url_base, mut url_params) = match url {
Some(url) => ok_or_bail!(
util::url_to_params(url),
MapResult {
c: None,
warnings: vec![format!("Could not parse url `{}`", url)]
}
),
None => match signature_cipher {
Some(signature_cipher) => match cipher_to_url_params(signature_cipher, deobf) {
Ok(res) => res,
Err(e) => {
return MapResult {
c: None,
warnings: vec![format!(
"Could not deobfuscate signatureCipher `{}`: {}",
signature_cipher, e
)],
};
}
},
None => {
return MapResult {
c: None,
warnings: vec!["stream contained neither url nor cipher".to_owned()],
}
}
},
};
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;
});
MapResult {
c: Some((
ok_or_bail!(
Url::parse_with_params(url_base.as_str(), url_params.iter()),
MapResult {
c: None,
warnings: vec![format!(
"url could not be joined. url: `{}` params: {:?}",
url_base, url_params
)],
}
)
.to_string(),
throttled,
)),
warnings,
}
}
fn map_video_stream(
f: player::Format,
deobf: &Deobfuscator,
last_nsig: &mut [String; 2],
) -> MapResult<Option<VideoStream>> {
let (mtype, codecs) = some_or_bail!(
parse_mime(&f.mime_type),
MapResult {
c: None,
warnings: vec![format!(
"Invalid mime type `{}` in video format {:?}",
&f.mime_type, &f
)]
}
);
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
match map_res.c {
Some((url, throttled)) => MapResult {
c: Some(VideoStream {
url,
itag: f.itag,
bitrate: f.bitrate,
average_bitrate: f.average_bitrate,
size: f.content_length,
index_range: f.index_range,
init_range: f.init_range,
width: some_or_bail!(
f.width,
MapResult {
c: None,
warnings: map_res.warnings
}
),
height: some_or_bail!(
f.height,
MapResult {
c: None,
warnings: map_res.warnings
}
),
fps: some_or_bail!(
f.fps,
MapResult {
c: None,
warnings: map_res.warnings
}
),
quality: some_or_bail!(
f.quality_label,
MapResult {
c: None,
warnings: map_res.warnings
}
),
hdr: f.color_info.unwrap_or_default().primaries
== player::Primaries::ColorPrimariesBt2020,
mime: f.mime_type.to_owned(),
format: some_or_bail!(
get_video_format(mtype),
MapResult {
c: None,
warnings: vec![format!("no valid format in video format")]
}
),
codec: get_video_codec(codecs),
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>> {
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2})\."#).unwrap());
let (mtype, codecs) = some_or_bail!(
parse_mime(&f.mime_type),
MapResult {
c: None,
warnings: vec![format!(
"Invalid mime type `{}` in video format {:?}",
&f.mime_type, &f
)]
}
);
let map_res = map_url(&f.url, &f.signature_cipher, deobf, last_nsig);
match map_res.c {
Some((url, throttled)) => MapResult {
c: Some(AudioStream {
url,
itag: f.itag,
bitrate: f.bitrate,
average_bitrate: f.average_bitrate,
size: f.content_length,
index_range: f.index_range,
init_range: f.init_range,
mime: f.mime_type.to_owned(),
format: some_or_bail!(
get_audio_format(mtype),
MapResult {
c: None,
warnings: vec![format!("invalid format in audio format {}", f.itag)]
}
),
codec: get_audio_codec(codecs),
throttled,
track: match f.audio_track {
Some(t) => {
let lang = LANG_PATTERN
.captures(&t.id)
.ok()
.flatten()
.map(|m| m.get(1).unwrap().as_str().to_owned());
Some(AudioTrack {
id: t.id,
lang,
lang_name: t.display_name,
is_default: t.audio_is_default,
})
}
None => None,
},
}),
warnings: map_res.warnings,
},
None => MapResult {
c: None,
warnings: map_res.warnings,
},
}
}
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
static PATTERN: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
let captures = some_or_bail!(PATTERN.captures(&mime).ok().flatten(), None);
Some((
captures.get(1).unwrap().as_str(),
captures
.get(2)
.unwrap()
.as_str()
.split(", ")
.collect::<Vec<&str>>(),
))
}
fn get_video_format(mtype: &str) -> Option<VideoFormat> {
match mtype {
"video/3gpp" => Some(VideoFormat::ThreeGp),
"video/mp4" => Some(VideoFormat::Mp4),
"video/webm" => Some(VideoFormat::Webm),
_ => None,
}
}
fn get_video_codec(codecs: Vec<&str>) -> VideoCodec {
for codec in codecs {
if codec.starts_with("avc1") {
return VideoCodec::Avc1;
} else if codec.starts_with("vp9") || codec.starts_with("vp09") {
return VideoCodec::Vp9;
} else if codec.starts_with("av01") {
return VideoCodec::Av01;
} else if codec.starts_with("mp4v") {
return VideoCodec::Mp4v;
}
}
VideoCodec::Unknown
}
fn get_audio_format(mtype: &str) -> Option<AudioFormat> {
match mtype {
"audio/mp4" => Some(AudioFormat::M4a),
"audio/webm" => Some(AudioFormat::Webm),
_ => None,
}
}
fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
for codec in codecs {
if codec.starts_with("mp4a") {
return AudioCodec::Mp4a;
} else if codec.starts_with("opus") {
return AudioCodec::Opus;
}
}
AudioCodec::Unknown
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use crate::{cache::DeobfData, client2::CLIENT_TYPES, report::TestFileReporter};
use super::*;
use rstest::rstest;
static DEOBFUSCATOR: Lazy<Deobfuscator> = Lazy::new(|| {
Deobfuscator::from(DeobfData {
js_url: "https://www.youtube.com/s/player/c8b8a173/player_ias.vflset/en_US/base.js".to_owned(),
sig_fn: "var oB={B4:function(a){a.reverse()},xm:function(a,b){a.splice(0,b)},dC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c}};var Vva=function(a){a=a.split(\"\");oB.dC(a,42);oB.xm(a,3);oB.dC(a,48);oB.B4(a,68);return a.join(\"\")};function deobfuscate(a){return Vva(a);}".to_owned(),
nsig_fn: "Ska=function(a){var b=a.split(\"\"),c=[-1505243983,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(-e).reverse().forEach(function(f){d.unshift(f)})},\n-1692381986,function(d,e){e=(e%d.length+d.length)%d.length;var f=d[0];d[0]=d[e];d[e]=f},\n-262444939,\"unshift\",function(d){for(var e=d.length;e;)d.push(d.splice(--e,1)[0])},\n1201502951,-546377604,-504264123,-1978377336,1042456724,function(d,e){for(e=(e%d.length+d.length)%d.length;e--;)d.unshift(d.pop())},\n711986897,406699922,-1842537993,-1678108293,1803491779,1671716087,12778705,-718839990,null,null,-1617525823,342523552,-1338406651,-399705108,-696713950,b,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},\nfunction(d,e){e=(e%d.length+d.length)%d.length;d.splice(e,1)},\n-980602034,356396192,null,-1617525823,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n-1029864222,-641353250,-1681901809,-1391247867,1707415199,-1957855835,b,function(){for(var d=64,e=[];++d-e.length-32;)switch(d){case 58:d=96;continue;case 91:d=44;break;case 65:d=47;continue;case 46:d=153;case 123:d-=58;default:e.push(String.fromCharCode(d))}return e},\n-1936558978,-1505243983,function(d){d.reverse()},\n1296889058,-1813915420,-943019300,function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(\"\"))},\n\"join\",b,-2061642263];c[21]=c;c[22]=c;c[33]=c;try{c[3](c[33],c[9]),c[29](c[22],c[25]),c[29](c[22],c[19]),c[29](c[33],c[17]),c[29](c[21],c[2]),c[29](c[42],c[10]),c[1](c[52],c[40]),c[12](c[28],c[8]),c[29](c[21],c[45]),c[1](c[21],c[48]),c[44](c[26]),c[39](c[5],c[2]),c[31](c[53],c[16]),c[30](c[29],c[8]),c[51](c[29],c[6],c[44]()),c[4](c[43],c[1]),c[2](c[23],c[42]),c[2](c[0],c[46]),c[38](c[14],c[52]),c[32](c[5]),c[26](c[29],c[46]),c[26](c[5],c[13]),c[28](c[1],c[37]),c[26](c[31],c[13]),c[26](c[1],c[34]),\nc[46](c[1],c[32],c[40]()),c[26](c[50],c[44]),c[17](c[50],c[51]),c[0](c[3],c[24]),c[32](c[13]),c[43](c[3],c[51]),c[0](c[34],c[17]),c[16](c[45],c[53]),c[29](c[44],c[13]),c[42](c[1],c[50]),c[47](c[22],c[53]),c[37](c[22]),c[13](c[52],c[21]),c[6](c[43],c[34]),c[6](c[31],c[46])}catch(d){return\"enhanced_except_gZYB_un-_w8_\"+a}return b.join(\"\")};function deobfuscate(a){return Ska(a);}".to_owned(),
sts: "19201".to_owned(),
})
});
#[test_log::test(tokio::test)]
async fn download_response_testfiles() {
let tf_dir = Path::new("testfiles/player");
let video_id = "pPvd8UxmSbQ";
for client_type in CLIENT_TYPES {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("{:?}_video.json", client_type).to_lowercase());
if json_path.exists() {
continue;
}
let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None).report(true);
rp.get_player(video_id, client_type).await.unwrap();
}
}
#[test_log::test(tokio::test)]
async fn download_model_testfiles() {
let tf_dir = Path::new("testfiles/player_model");
let rp = RustyPipe::default();
for (name, id) in [("multilanguage", "tVWWp1PqDus"), ("hdr", "LXb3EKWsInQ")] {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("{}.json", name).to_lowercase());
if json_path.exists() {
continue;
}
let player_data = rp.get_player(id, ClientType::Desktop).await.unwrap();
let file = File::create(json_path).unwrap();
serde_json::to_writer_pretty(file, &player_data).unwrap();
}
}
#[rstest]
#[case::desktop("desktop")]
#[case::desktop_music("desktopmusic")]
#[case::tv_html5_embed("tvhtml5embed")]
#[case::android("android")]
#[case::ios("ios")]
fn t_map_player_data(#[case] name: &str) {
let filename = format!("testfiles/player/{}_video.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = resp
.map_response("pPvd8UxmSbQ", Language::En, Some(&DEOBFUSCATOR))
.unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
let is_desktop = name == "desktop" || name == "desktopmusic";
insta::assert_yaml_snapshot!(format!("map_player_data_{}", name), map_res.c, {
".info.publish_date" => insta::dynamic_redaction(move |value, _path| {
if is_desktop {
assert!(value.as_str().unwrap().starts_with("2019-05-30T00:00:00"));
"2019-05-30T00:00:00"
} else {
assert_eq!(value, insta::internals::Content::None);
"~"
}
}),
});
}
/// Assert equality within 10% margin
fn assert_approx(left: f64, right: f64) {
if left != right {
let f = left / right;
assert!(
0.9 < f && f < 1.1,
"{} not within 10% margin of {}",
left,
right
);
}
}
#[rstest]
#[case::desktop(ClientType::Desktop)]
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
#[case::android(ClientType::Android)]
#[case::ios(ClientType::Ios)]
#[test_log::test(tokio::test)]
async fn t_get_player(#[case] client_type: ClientType) {
let rp = RustyPipe::default();
let player_data = rp.get_player("n4tK7LYFxI0", client_type).await.unwrap();
// dbg!(&player_data);
assert_eq!(player_data.info.id, "n4tK7LYFxI0");
assert_eq!(player_data.info.title, "Spektrem - Shine [NCS Release]");
if client_type == ClientType::DesktopMusic {
assert!(player_data.info.description.is_none());
} else {
assert!(player_data.info.description.unwrap().starts_with(
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music"
));
}
assert_eq!(player_data.info.length, 259);
assert!(!player_data.info.thumbnails.is_empty());
assert_eq!(player_data.info.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
assert_eq!(player_data.info.channel.name, "NoCopyrightSounds");
assert!(player_data.info.view_count > 146818808);
assert_eq!(player_data.info.keywords[0], "spektrem");
assert_eq!(player_data.info.is_live_content, false);
if client_type == ClientType::Desktop || client_type == ClientType::DesktopMusic {
assert!(player_data
.info
.publish_date
.unwrap()
.to_string()
.starts_with("2013-05-05 00:00:00"));
assert_eq!(player_data.info.category.unwrap(), "Music");
assert_eq!(player_data.info.is_family_safe.unwrap(), true);
}
if client_type == ClientType::Ios {
let video = player_data
.video_only_streams
.iter()
.find(|s| s.itag == 247)
.unwrap();
let audio = player_data
.audio_streams
.iter()
.find(|s| s.itag == 140)
.unwrap();
// Bitrates may change between requests
assert_approx(video.bitrate as f64, 1507068.0);
assert_eq!(video.average_bitrate, 1345149);
assert_eq!(video.size, 43553412);
assert_eq!(video.width, 1280);
assert_eq!(video.height, 720);
assert_eq!(video.fps, 30);
assert_eq!(video.quality, "720p");
assert_eq!(video.hdr, false);
assert_eq!(video.mime, "video/webm; codecs=\"vp09.00.31.08\"");
assert_eq!(video.format, VideoFormat::Webm);
assert_eq!(video.codec, VideoCodec::Vp9);
assert_approx(audio.bitrate as f64, 130685.0);
assert_eq!(audio.average_bitrate, 129496);
assert_eq!(audio.size, 4193863);
assert_eq!(audio.mime, "audio/mp4; codecs=\"mp4a.40.2\"");
assert_eq!(audio.format, AudioFormat::M4a);
assert_eq!(audio.codec, AudioCodec::Mp4a);
} else {
let video = player_data
.video_only_streams
.iter()
.find(|s| s.itag == 398)
.unwrap();
let audio = player_data
.audio_streams
.iter()
.find(|s| s.itag == 251)
.unwrap();
assert_approx(video.bitrate as f64, 1340829.0);
assert_approx(video.average_bitrate as f64, 1233444.0);
assert_approx(video.size as f64, 39936630.0);
assert_eq!(video.width, 1280);
assert_eq!(video.height, 720);
assert_eq!(video.fps, 30);
assert_eq!(video.quality, "720p");
assert_eq!(video.hdr, false);
assert_eq!(video.mime, "video/mp4; codecs=\"av01.0.05M.08\"");
assert_eq!(video.format, VideoFormat::Mp4);
assert_eq!(video.codec, VideoCodec::Av01);
assert_eq!(video.throttled, false);
assert_approx(audio.bitrate as f64, 142718.0);
assert_approx(audio.average_bitrate as f64, 130708.0);
assert_approx(audio.size as f64, 4232344.0);
assert_eq!(audio.mime, "audio/webm; codecs=\"opus\"");
assert_eq!(audio.format, AudioFormat::Webm);
assert_eq!(audio.codec, AudioCodec::Opus);
assert_eq!(audio.throttled, false);
}
assert!(player_data.expires_in_seconds > 10000);
}
#[test]
fn t_cipher_to_url() {
let signature_cipher = "s=w%3DAe%3DA6aDNQLkViKS7LOm9QtxZJHKwb53riq9qEFw-ecBWJCAiA%3DcEg0tn3dty9jEHszfzh4Ud__bg9CEHVx4ix-7dKsIPAhIQRw8JQ0qOA&sp=sig&url=https://rr5---sn-h0jelnez.googlevideo.com/videoplayback%3Fexpire%3D1659376413%26ei%3Dvb7nYvH5BMK8gAfBj7ToBQ%26ip%3D2003%253Ade%253Aaf06%253A6300%253Ac750%253A1b77%253Ac74a%253A80e3%26id%3Do-AB_BABwrXZJN428ZwDxq5ScPn2AbcGODnRlTVhCQ3mj2%26itag%3D251%26source%3Dyoutube%26requiressl%3Dyes%26mh%3DhH%26mm%3D31%252C26%26mn%3Dsn-h0jelnez%252Csn-4g5ednsl%26ms%3Dau%252Conr%26mv%3Dm%26mvi%3D5%26pl%3D37%26initcwndbps%3D1588750%26spc%3DlT-Khi831z8dTejFIRCvCEwx_6romtM%26vprv%3D1%26mime%3Daudio%252Fwebm%26ns%3Db_Mq_qlTFcSGlG9RpwpM9xQH%26gir%3Dyes%26clen%3D3781277%26dur%3D229.301%26lmt%3D1655510291473933%26mt%3D1659354538%26fvip%3D5%26keepalive%3Dyes%26fexp%3D24001373%252C24007246%26c%3DWEB%26rbqsm%3Dfr%26txp%3D4532434%26n%3Dd2g6G2hVqWIXxedQ%26sparams%3Dexpire%252Cei%252Cip%252Cid%252Citag%252Csource%252Crequiressl%252Cspc%252Cvprv%252Cmime%252Cns%252Cgir%252Cclen%252Cdur%252Clmt%26lsparams%3Dmh%252Cmm%252Cmn%252Cms%252Cmv%252Cmvi%252Cpl%252Cinitcwndbps%26lsig%3DAG3C_xAwRQIgCKCGJ1iu4wlaGXy3jcJyU3inh9dr1FIfqYOZEG_MdmACIQCbungkQYFk7EhD6K2YvLaHFMjKOFWjw001_tLb0lPDtg%253D%253D";
let mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
let map_res = map_url(
&None,
&Some(signature_cipher.to_owned()),
&DEOBFUSCATOR,
&mut last_nsig,
);
let (url, throttled) = 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!(throttled, false);
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
}
}

427
src/client2/playlist.rs Normal file
View file

@ -0,0 +1,427 @@
use anyhow::{anyhow, bail, Result};
use reqwest::Method;
use serde::Serialize;
use crate::{
deobfuscate::Deobfuscator,
model::{Channel, Language, Playlist, Thumbnail, Video},
serializer::text::{PageType, TextLink},
timeago, util,
};
use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipe};
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaylist {
context: ContextYT,
browse_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct QPlaylistCont {
context: ContextYT,
continuation: String,
}
impl RustyPipe {
pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QPlaylist {
context,
browse_id: "VL".to_owned() + playlist_id,
};
self.execute_request::<response::Playlist, _, _>(
ClientType::Desktop,
"get_playlist",
Method::POST,
"browse",
playlist_id,
&request_body,
None,
)
.await
}
pub async fn get_playlist_cont(&self, playlist: &mut Playlist) -> Result<()> {
match &playlist.ctoken {
Some(ctoken) => {
let context = self.get_context(ClientType::Desktop, true).await;
let request_body = QPlaylistCont {
context,
continuation: ctoken.to_owned(),
};
let (mut videos, ctoken) = self
.execute_request::<response::PlaylistCont, _, _>(
ClientType::Desktop,
"get_playlist_cont",
Method::POST,
"browse",
&playlist.id,
&request_body,
None,
)
.await?;
playlist.videos.append(&mut videos);
playlist.ctoken = ctoken;
if playlist.ctoken.is_none() {
playlist.n_videos = playlist.videos.len() as u32;
}
Ok(())
}
None => Err(anyhow!("no ctoken")),
}
}
}
impl MapResponse<Playlist> for response::Playlist {
fn map_response(
self,
id: &str,
lang: Language,
_deobf: Option<&Deobfuscator>,
) -> Result<MapResult<Playlist>> {
let video_items = &some_or_bail!(
some_or_bail!(
some_or_bail!(
self.contents
.two_column_browse_results_renderer
.contents
.get(0),
Err(anyhow!("twoColumnBrowseResultsRenderer empty"))
)
.tab_renderer
.content
.section_list_renderer
.contents
.get(0),
Err(anyhow!("sectionListRenderer empty"))
)
.item_section_renderer
.contents
.get(0),
Err(anyhow!("itemSectionRenderer empty"))
)
.playlist_video_list_renderer
.contents;
let (videos, ctoken) = map_playlist_items(&video_items.c);
let (thumbnails, last_update_txt) = match &self.sidebar {
Some(sidebar) => {
let primary = some_or_bail!(
sidebar.playlist_sidebar_renderer.items.get(0),
Err(anyhow!("no primary sidebar"))
);
(
&primary
.playlist_sidebar_primary_info_renderer
.thumbnail_renderer
.playlist_video_thumbnail_renderer
.thumbnail
.thumbnails,
primary
.playlist_sidebar_primary_info_renderer
.stats
.get(2)
.map(|t| t.to_owned()),
)
}
None => {
let header_banner = some_or_bail!(
&self.header.playlist_header_renderer.playlist_header_banner,
Err(anyhow!("no thumbnail found"))
);
let last_update_txt = self
.header
.playlist_header_renderer
.byline
.get(1)
.map(|b| b.playlist_byline_renderer.text.to_owned());
(
&header_banner
.hero_playlist_thumbnail_renderer
.thumbnail
.thumbnails,
last_update_txt,
)
}
};
let thumbnails = thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect::<Vec<_>>();
let n_videos = match ctoken {
Some(_) => {
ok_or_bail!(
util::parse_numeric(&self.header.playlist_header_renderer.num_videos_text),
Err(anyhow!("no video count"))
)
}
None => videos.len() as u32,
};
let playlist_id = self.header.playlist_header_renderer.playlist_id;
if playlist_id != id {
bail!("got wrong playlist id {}, expected {}", playlist_id, id);
}
let name = self.header.playlist_header_renderer.title;
let description = self.header.playlist_header_renderer.description_text;
let channel = match self.header.playlist_header_renderer.owner_text {
Some(owner_text) => match owner_text {
TextLink::Browse {
text,
page_type,
browse_id,
} => match page_type {
PageType::Channel => Some(Channel {
id: browse_id,
name: text,
}),
_ => None,
},
_ => None,
},
None => None,
};
let mut warnings = video_items.warnings.to_owned();
let last_update = match &last_update_txt {
Some(textual_date) => {
let parsed = timeago::parse_textual_date_to_dt(lang, textual_date);
if parsed.is_none() {
warnings.push(format!("could not parse textual date `{}`", textual_date));
}
parsed
}
None => None,
};
Ok(MapResult {
c: Playlist {
id: playlist_id,
name,
videos,
n_videos,
ctoken,
thumbnails,
description,
channel,
last_update,
last_update_txt,
},
warnings,
})
}
}
impl MapResponse<(Vec<Video>, Option<String>)> for response::PlaylistCont {
fn map_response(
self,
id: &str,
_lang: Language,
_deobf: Option<&Deobfuscator>,
) -> Result<MapResult<(Vec<Video>, Option<String>)>> {
let action = some_or_bail!(
self.on_response_received_actions
.iter()
.find(|a| a.append_continuation_items_action.target_id == id),
Err(anyhow!("no continuation action"))
);
Ok(MapResult {
c: map_playlist_items(&action.append_continuation_items_action.continuation_items.c),
warnings: action
.append_continuation_items_action
.continuation_items
.warnings
.to_owned(),
})
}
}
fn map_playlist_items(
items: &Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
) -> (Vec<Video>, Option<String>) {
let mut ctoken: Option<String> = None;
let videos = items
.iter()
.filter_map(|it| match it {
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
TextLink::Browse {
text,
page_type,
browse_id,
} => match page_type {
PageType::Channel => Some(Video {
id: video.video_id.to_owned(),
title: video.title.to_owned(),
length: video.length_seconds,
thumbnails: video
.thumbnail
.thumbnails
.iter()
.map(|t| Thumbnail {
url: t.url.to_owned(),
width: t.width,
height: t.height,
})
.collect(),
channel: Channel {
id: browse_id.to_string(),
name: text.to_owned(),
},
}),
_ => None,
},
_ => None,
},
response::VideoListItem::ContinuationItemRenderer {
continuation_endpoint,
} => {
ctoken = Some(continuation_endpoint.continuation_command.token.to_owned());
None
}
})
.collect::<Vec<_>>();
(videos, ctoken)
}
#[cfg(test)]
mod tests {
use std::{fs::File, io::BufReader, path::Path};
use rstest::rstest;
use crate::report::TestFileReporter;
use super::*;
#[rstest]
#[case::long(
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
"Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022",
true,
None,
Some(Channel {
id: "UCIekuFeMaV78xYfvpmoCnPg".to_owned(),
name: "Best Music".to_owned(),
})
)]
#[case::short(
"RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk",
"Easy Pop",
false,
None,
None
)]
#[case::nomusic(
"PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe",
"Minecraft SHINE",
false,
Some("SHINE - Survival Hardcore in New Environment: Auf einem Server machen sich tapfere Spieler auf, mystische Welten zu erkunden, magische Technologien zu erforschen und vorallem zu überleben...".to_owned()),
Some(Channel {
id: "UCQM0bS4_04-Y4JuYrgmnpZQ".to_owned(),
name: "Chaosflo44".to_owned(),
})
)]
#[test_log::test(tokio::test)]
async fn t_get_playlist(
#[case] id: &str,
#[case] name: &str,
#[case] is_long: bool,
#[case] description: Option<String>,
#[case] channel: Option<Channel>,
) {
let rp = RustyPipe::default();
let playlist = rp.get_playlist(id).await.unwrap();
assert_eq!(playlist.id, id);
assert_eq!(playlist.name, name);
assert!(!playlist.videos.is_empty());
assert_eq!(playlist.ctoken.is_some(), is_long);
assert!(playlist.n_videos > 10);
assert_eq!(playlist.n_videos > 100, is_long);
assert_eq!(playlist.description, description);
if channel.is_some() {
assert_eq!(playlist.channel, channel);
}
assert!(!playlist.thumbnails.is_empty());
}
#[test_log::test(tokio::test)]
async fn download_testfiles() {
let tf_dir = Path::new("testfiles/playlist");
for (name, id) in [
("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk"),
("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ"),
("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe"),
] {
let mut json_path = tf_dir.to_path_buf();
json_path.push(format!("playlist_{}.json", name));
if json_path.exists() {
continue;
}
let reporter = TestFileReporter::new(json_path);
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None).report(true);
rp.get_playlist(id).await.unwrap();
}
}
#[rstest]
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
fn t_map_playlist_data(#[case] name: &str, #[case] id: &str) {
let filename = format!("testfiles/playlist/playlist_{}.json", name);
let json_path = Path::new(&filename);
let json_file = File::open(json_path).unwrap();
let playlist: response::Playlist =
serde_json::from_reader(BufReader::new(json_file)).unwrap();
let map_res = playlist.map_response(id, Language::En, None).unwrap();
assert!(
map_res.warnings.is_empty(),
"deserialization/mapping warnings: {:?}",
map_res.warnings
);
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), map_res.c, {
".last_update" => "[date]"
});
}
#[test_log::test(tokio::test)]
async fn t_playlist_cont() {
let rp = RustyPipe::default();
let mut playlist = rp
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
.await
.unwrap();
while playlist.ctoken.is_some() {
rp.get_playlist_cont(&mut playlist).await.unwrap();
}
assert!(playlist.videos.len() > 100);
}
}

View file

@ -0,0 +1,74 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::VecSkipError;
use super::TimeOverlay;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Channel {
pub contents: Contents,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub two_column_browse_results_renderer: TabsRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TabsRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub tabs: Vec<TabRendererWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TabRendererWrap {
pub tab_renderer: ContentRenderer<SectionListRendererWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionListRendererWrap {
pub section_list_renderer: ContentsRenderer<ItemSectionRendererWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemSectionRendererWrap {
pub item_section_renderer: ContentsRenderer<GridRendererWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GridRendererWrap {
pub grid_renderer: GridRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GridRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub items: Vec<VideoListItem<ChannelVideo>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChannelVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count_text: String,
#[serde_as(as = "VecSkipError<_>")]
pub thumbnail_overlays: Vec<TimeOverlay>,
}

236
src/client2/response/mod.rs Normal file
View file

@ -0,0 +1,236 @@
pub mod channel;
pub mod player;
pub mod playlist;
pub mod playlist_music;
pub mod video;
pub use channel::Channel;
pub use player::Player;
pub use playlist::Playlist;
pub use playlist::PlaylistCont;
pub use playlist_music::PlaylistMusic;
pub use video::Video;
pub use video::VideoComments;
pub use video::VideoRecommendations;
use serde::Deserialize;
use serde_with::{serde_as, DefaultOnError, VecSkipError};
use crate::serializer::text::TextLink;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentRenderer<T> {
pub content: T,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContentsRenderer<T> {
#[serde(alias = "tabs")]
pub contents: Vec<T>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ThumbnailsWrap {
pub thumbnail: Thumbnails,
}
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Thumbnails {
pub thumbnails: Vec<Thumbnail>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Thumbnail {
pub url: String,
pub width: u32,
pub height: u32,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VideoListItem<T> {
#[serde(alias = "playlistVideoRenderer", alias = "compactVideoRenderer")]
GridVideoRenderer {
#[serde(flatten)]
video: T,
},
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContinuationEndpoint {
pub continuation_command: ContinuationCommand,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ContinuationCommand {
pub token: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Icon {
pub icon_type: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwner {
pub video_owner_renderer: VideoOwnerRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoOwnerRenderer {
#[serde_as(as = "crate::serializer::text::TextLink")]
pub title: TextLink,
pub thumbnail: Thumbnails,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub subscriber_count_text: Option<String>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<UserBadge>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserBadge {
pub metadata_badge_renderer: UserBadgeRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserBadgeRenderer {
pub style: UserBadgeStyle,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum UserBadgeStyle {
BadgeStyleTypeVerified,
BadgeStyleTypeVerifiedArtist,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlay {
pub thumbnail_overlay_time_status_renderer: TimeOverlayRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TimeOverlayRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub style: TimeOverlayStyle,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum TimeOverlayStyle {
#[default]
Default,
Live,
Shorts,
}
// YouTube Music
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicItem {
pub thumbnail: MusicThumbnailRenderer,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub playlist_item_data: Option<PlaylistItemData>,
#[serde_as(as = "VecSkipError<_>")]
pub flex_columns: Vec<MusicColumn>,
#[serde_as(as = "VecSkipError<_>")]
pub fixed_columns: Vec<MusicColumn>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicThumbnailRenderer {
#[serde(alias = "croppedSquareThumbnailRenderer")]
pub music_thumbnail_renderer: ThumbnailsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistItemData {
pub video_id: String,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicContentsRenderer<T> {
pub contents: Vec<T>,
#[serde_as(as = "Option<VecSkipError<_>>")]
pub continuations: Option<Vec<MusicContinuation>>,
}
#[derive(Clone, Debug, Deserialize)]
pub struct MusicColumn {
#[serde(
rename = "musicResponsiveListItemFlexColumnRenderer",
alias = "musicResponsiveListItemFixedColumnRenderer"
)]
pub renderer: MusicColumnRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
pub struct MusicColumnRenderer {
#[serde_as(as = "crate::serializer::text::TextLinks")]
pub text: Vec<TextLink>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicContinuation {
pub next_continuation_data: MusicContinuationData,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicContinuationData {
pub continuation: String,
}
impl Into<crate::model::Thumbnail> for Thumbnail {
fn into(self) -> crate::model::Thumbnail {
crate::model::Thumbnail {
url: self.url,
width: self.width,
height: self.height,
}
}
}
impl Into<Vec<crate::model::Thumbnail>> for Thumbnails {
fn into(self) -> Vec<crate::model::Thumbnail> {
let mut thumbnails = vec![];
for t in self.thumbnails {
thumbnails.push(t.into());
}
thumbnails
}
}

View file

@ -0,0 +1,231 @@
use std::ops::Range;
use chrono::NaiveDate;
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use super::Thumbnails;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Player {
pub playability_status: PlayabilityStatus,
pub streaming_data: Option<StreamingData>,
pub captions: Option<Captions>,
pub video_details: Option<VideoDetails>,
pub microformat: Option<Microformat>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(tag = "status", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlayabilityStatus {
#[serde(rename_all = "camelCase")]
Ok { live_streamability: Option<Empty> },
/// Video cant be played because of DRM / Geoblock
#[serde(rename_all = "camelCase")]
Unplayable { reason: String },
/// Age limit / Private video
#[serde(rename_all = "camelCase")]
LoginRequired { reason: String },
#[serde(rename_all = "camelCase")]
LiveStreamOffline { reason: String },
/// Video was censored / deleted
#[serde(rename_all = "camelCase")]
Error { reason: String },
}
#[derive(Clone, Debug, Deserialize)]
pub struct Empty {}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StreamingData {
#[serde_as(as = "JsonString")]
pub expires_in_seconds: u32,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub formats: Vec<Format>,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub adaptive_formats: Vec<Format>,
/// Only on livestreams
pub dash_manifest_url: Option<String>,
/// Only on livestreams
pub hls_manifest_url: Option<String>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Format {
pub itag: u32,
pub url: Option<String>,
#[serde(default, rename = "type")]
pub format_type: FormatType,
pub mime_type: String,
pub bitrate: u32,
pub width: Option<u32>,
pub height: Option<u32>,
#[serde_as(as = "Option<crate::serializer::range::Range>")]
pub index_range: Option<Range<u32>>,
#[serde_as(as = "Option<crate::serializer::range::Range>")]
pub init_range: Option<Range<u32>>,
#[serde_as(as = "JsonString")]
pub content_length: u64,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub quality: Option<Quality>,
pub fps: Option<u8>,
pub quality_label: Option<String>,
pub average_bitrate: u32,
pub color_info: Option<ColorInfo>,
// Audio only
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
pub audio_quality: Option<AudioQuality>,
#[serde_as(as = "Option<JsonString>")]
pub audio_sample_rate: Option<u32>,
pub audio_channels: Option<u8>,
pub loudness_db: Option<f64>,
pub audio_track: Option<AudioTrack>,
pub signature_cipher: Option<String>,
}
impl Format {
pub fn is_audio(&self) -> bool {
self.audio_quality.is_some() && self.audio_sample_rate.is_some()
}
pub fn is_video(&self) -> bool {
self.quality.is_some()
&& self.quality_label.is_some()
&& self.fps.is_some()
&& self.height.is_some()
&& self.width.is_some()
}
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Quality {
Tiny,
Small,
Medium,
Large,
Highres,
Hd720,
Hd1080,
Hd1440,
Hd2160,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AudioQuality {
#[serde(rename = "AUDIO_QUALITY_LOW", alias = "low")]
Low,
#[serde(rename = "AUDIO_QUALITY_MEDIUM", alias = "medium")]
Medium,
#[serde(rename = "AUDIO_QUALITY_HIGH", alias = "high")]
High,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum FormatType {
#[default]
Default,
/// This stream only works via DASH and not via progressive HTTP.
FormatStreamTypeOtf,
}
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct ColorInfo {
pub primaries: Primaries,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum Primaries {
#[default]
ColorPrimariesBt709,
ColorPrimariesBt2020,
}
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct AudioTrack {
pub id: String,
pub display_name: String,
pub audio_is_default: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Captions {
pub player_captions_tracklist_renderer: PlayerCaptionsTracklistRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerCaptionsTracklistRenderer {
pub caption_tracks: Vec<CaptionTrack>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CaptionTrack {
pub base_url: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub name: String,
pub language_code: String,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoDetails {
pub video_id: String,
pub title: String,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
pub keywords: Option<Vec<String>>,
pub channel_id: String,
pub short_description: Option<String>,
pub thumbnail: Option<Thumbnails>,
#[serde_as(as = "JsonString")]
pub view_count: u64,
pub author: String,
pub is_live_content: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Microformat {
#[serde(alias = "microformatDataRenderer")]
pub player_microformat_renderer: PlayerMicroformatRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerMicroformatRenderer {
#[serde(alias = "familySafe")]
pub is_family_safe: bool,
pub category: String,
pub publish_date: NaiveDate,
// Only on YT Music
pub tags: Option<Vec<String>>,
}

View file

@ -0,0 +1,179 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
use crate::client2::MapResult;
use crate::serializer::text::TextLink;
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Playlist {
pub contents: Contents,
pub header: Header,
pub sidebar: Option<Sidebar>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistCont {
#[serde_as(as = "VecSkipError<_>")]
pub on_response_received_actions: Vec<OnResponseReceivedAction>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub two_column_browse_results_renderer: ContentsRenderer<Tab>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tab {
pub tab_renderer: ContentRenderer<SectionList>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionList {
pub section_list_renderer: ContentsRenderer<ItemSection>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemSection {
pub item_section_renderer: ContentsRenderer<PlaylistVideoListRenderer>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideoListRenderer {
pub playlist_video_list_renderer: PlaylistVideoList,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideoList {
#[serde_as(as = "crate::serializer::VecLogError<_>")]
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
#[serde_as(as = "crate::serializer::text::TextLink")]
pub channel: TextLink,
#[serde_as(as = "JsonString")]
pub length_seconds: u32,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Header {
pub playlist_header_renderer: HeaderRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HeaderRenderer {
pub playlist_id: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(default)]
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
pub description_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")]
pub num_videos_text: String,
#[serde_as(as = "Option<crate::serializer::text::TextLink>")]
pub owner_text: Option<TextLink>,
// Alternative layout
pub playlist_header_banner: Option<PlaylistHeaderBanner>,
#[serde(default)]
pub byline: Vec<Byline>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistHeaderBanner {
pub hero_playlist_thumbnail_renderer: ThumbnailsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Byline {
pub playlist_byline_renderer: BylineRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BylineRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub text: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Sidebar {
pub playlist_sidebar_renderer: SidebarRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SidebarRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub items: Vec<SidebarItemPrimary>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SidebarItemPrimary {
pub playlist_sidebar_primary_info_renderer: SidebarPrimaryInfoRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SidebarPrimaryInfoRenderer {
pub thumbnail_renderer: PlaylistThumbnailRenderer,
// - `"495", " videos"`
// - `"3,310,996 views"`
// - `"Last updated on ", "Aug 7, 2022"`
#[serde_as(as = "Vec<crate::serializer::text::Text>")]
pub stats: Vec<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistThumbnailRenderer {
// the alternative field name is used by YTM playlists
#[serde(alias = "playlistCustomThumbnailRenderer")]
pub playlist_video_thumbnail_renderer: ThumbnailsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OnResponseReceivedAction {
pub append_continuation_items_action: AppendAction,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendAction {
#[serde_as(as = "crate::serializer::VecLogError<_>")]
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
pub target_id: String,
}

View file

@ -0,0 +1,95 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::VecSkipError;
use crate::serializer::text::Text;
use super::MusicThumbnailRenderer;
use super::{
ContentRenderer, ContentsRenderer, MusicContentsRenderer, MusicContinuation, MusicItem,
};
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistMusic {
pub contents: Contents,
pub header: Header,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub single_column_browse_results_renderer: ContentsRenderer<Tab>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tab {
pub tab_renderer: ContentRenderer<SectionList>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SectionList {
/// Includes a continuation token for fetching recommendations
pub section_list_renderer: MusicContentsRenderer<ItemSection>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemSection {
#[serde(alias = "musicPlaylistShelfRenderer")]
pub music_shelf_renderer: MusicShelf,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MusicShelf {
/// Playlist ID (only for playlists)
pub playlist_id: Option<String>,
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<PlaylistMusicItem>,
/// Continuation token for fetching more (>100) playlist items
#[serde_as(as = "Option<VecSkipError<_>>")]
pub continuations: Option<Vec<MusicContinuation>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlaylistMusicItem {
pub music_responsive_list_item_renderer: MusicItem,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Header {
pub music_detail_header_renderer: HeaderRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HeaderRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
/// Content type + Channel/Artist + Year.
/// Missing on artist_tracks view.
///
/// `"Playlist", " • ", <"Best Music">, " • ", "2022"`
///
/// `"Album", " • ", <"Helene Fischer">, " • ", "2021"`
pub subtitle: Option<Text>,
/// Playlist description. May contain hashtags which are
/// displayed as search links on the YouTube website.
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub description: Option<String>,
/// Playlist thumbnail / album cover.
/// Missing on artist_tracks view.
pub thumbnail: Option<MusicThumbnailRenderer>,
/// Number of tracks + playtime.
/// Missing on artist_tracks view.
///
/// `"64 songs", " • ", "3 hours, 40 minutes"`
pub second_subtitle: Option<Text>,
}

View file

@ -0,0 +1,432 @@
use serde::Deserialize;
use serde_with::serde_as;
use serde_with::{DefaultOnError, VecSkipError};
use crate::serializer::text::TextLink;
use super::{ContinuationEndpoint, Icon, Thumbnails, VideoListItem, VideoOwner};
/// Video info response
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Video {
pub contents: Contents,
#[serde_as(as = "VecSkipError<_>")]
pub engagement_panels: Vec<EngagementPanel>,
}
/// Video recommendations response
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoRecommendations {
pub on_response_received_endpoints: Vec<RecommendationsContItem>,
}
/// Video comments response
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoComments {
#[serde_as(as = "VecSkipError<_>")]
pub on_response_received_endpoints: Vec<CommentsContItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contents {
pub two_column_watch_next_results: TwoColumnWatchNextResults,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TwoColumnWatchNextResults {
pub results: VideoResultsWrap,
pub secondary_results: RecommendationResultsWrap,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoResultsWrap {
pub results: VideoResults,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoResults {
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<VideoResultsItem>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VideoResultsItem {
#[serde(rename_all = "camelCase")]
VideoPrimaryInfoRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
title: String,
view_count: ViewCountWrap,
video_actions: VideoActions,
#[serde_as(as = "crate::serializer::text::Text")]
date_text: String,
},
#[serde(rename_all = "camelCase")]
VideoSecondaryInfoRenderer {
owner: VideoOwner,
#[serde_as(as = "crate::serializer::text::Text")]
description: String,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
metadata_row_container: Option<MetadataRowContainer>,
},
#[serde(rename_all = "camelCase")]
ItemSectionRenderer {
#[serde_as(as = "VecSkipError<_>")]
contents: Vec<ItemSection>,
section_identifier: String,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewCountWrap {
pub video_view_count_renderer: ViewCount,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ViewCount {
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoActions {
pub menu_renderer: VideoActionsMenu,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoActionsMenu {
#[serde_as(as = "VecSkipError<_>")]
pub top_level_buttons: Vec<ToggleButtonWrap>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToggleButtonWrap {
pub toggle_button_renderer: ToggleButton,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToggleButton {
pub default_icon: Icon,
#[serde_as(as = "crate::serializer::text::Text")]
pub default_text: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowContainer {
pub metadata_row_container_renderer: MetadataRowContainerRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowContainerRenderer {
pub rows: Vec<MetadataRow>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRow {
pub metadata_row_renderer: MetadataRowRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MetadataRowRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde_as(as = "Vec<crate::serializer::text::TextLinks>")]
pub contents: Vec<Vec<TextLink>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ItemSection {
#[serde(rename_all = "camelCase")]
CommentsEntryPointHeaderRenderer {
#[serde_as(as = "crate::serializer::text::Text")]
header_text: String,
#[serde_as(as = "crate::serializer::text::Text")]
comment_count: String,
},
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationResultsWrap {
pub secondary_results: RecommendationResults,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationResults {
#[serde_as(as = "VecSkipError<_>")]
pub results: Vec<VideoListItem<RecommendedVideo>>,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendedVideo {
pub video_id: String,
pub thumbnail: Thumbnails,
#[serde_as(as = "crate::serializer::text::Text")]
pub title: String,
#[serde(rename = "shortBylineText")]
#[serde_as(as = "crate::serializer::text::TextLink")]
pub channel: TextLink,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub length_text: Option<String>,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub published_time_text: Option<String>,
#[serde_as(as = "crate::serializer::text::Text")]
pub view_count_text: String,
#[serde(default)]
#[serde_as(as = "VecSkipError<_>")]
pub badges: Vec<VideoBadge>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoBadge {
pub metadata_badge_renderer: VideoBadgeRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoBadgeRenderer {
pub style: VideoBadgeStyle,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum VideoBadgeStyle {
BadgeStyleTypeLiveNow,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanel {
pub engagement_panel_section_list_renderer: EngagementPanelRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelRenderer {
pub header: EngagementPanelHeader,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelHeader {
pub engagement_panel_title_header_renderer: EngagementPanelHeaderRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelHeaderRenderer {
pub menu: EngagementPanelMenu,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenu {
pub sort_filter_sub_menu_renderer: EngagementPanelMenuRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenuRenderer {
pub sub_menu_items: Vec<EngagementPanelMenuItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EngagementPanelMenuItem {
pub service_endpoint: ContinuationEndpoint,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecommendationsContItem {
pub append_continuation_items_action: AppendRecommendations,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendRecommendations {
pub continuation_items: Vec<VideoListItem<RecommendedVideo>>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentsContItem {
#[serde(alias = "reloadContinuationItemsCommand")]
pub append_continuation_items_action: AppendComments,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppendComments {
#[serde_as(as = "VecSkipError<_>")]
pub continuation_items: Vec<CommentListItem>,
pub target_id: String,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CommentListItem {
#[serde(rename_all = "camelCase")]
CommentThreadRenderer {
comment: Comment,
#[serde(default)]
replies: Replies,
#[serde(default)]
#[serde_as(deserialize_as = "DefaultOnError")]
rendering_priority: CommentPriority,
},
CommentRenderer {
#[serde(flatten)]
comment: CommentRenderer,
},
#[serde(rename_all = "camelCase")]
ContinuationItemRenderer {
continuation_endpoint: ContinuationEndpoint,
},
// TODO: TMP
#[serde(rename_all = "camelCase")]
CommentsHeaderRenderer { count_text: Option<String> },
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Comment {
pub comment_renderer: CommentRenderer,
}
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentRenderer {
// There may be comments with missing authors (possibly deleted users?)
#[serde(default)]
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
pub author_text: Option<String>,
pub author_thumbnail: Thumbnails,
#[serde(default)]
#[serde_as(as = "DefaultOnError")]
pub author_endpoint: Option<AuthorEndpoint>,
#[serde_as(as = "crate::serializer::text::Text")]
pub content_text: String,
#[serde_as(as = "crate::serializer::text::Text")]
pub published_time_text: String,
pub comment_id: String,
pub author_is_channel_owner: bool,
#[serde_as(as = "Option<crate::serializer::text::Text>")]
pub vote_count: Option<String>,
pub author_comment_badge: Option<AuthorCommentBadge>,
#[serde(default)]
pub reply_count: u32,
pub action_buttons: CommentActionButtons,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorEndpoint {
pub browse_endpoint: BrowseEndpoint,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BrowseEndpoint {
pub browse_id: String,
}
#[derive(Default, Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CommentPriority {
#[default]
RenderingPriorityUnknown,
RenderingPriorityPinnedComment,
}
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Replies {
pub comment_replies_renderer: RepliesRenderer,
}
#[serde_as]
#[derive(Default, Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RepliesRenderer {
#[serde_as(as = "VecSkipError<_>")]
pub contents: Vec<CommentListItem>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentActionButtons {
pub comment_action_buttons_renderer: CommentActionButtonsRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CommentActionButtonsRenderer {
pub creator_heart: Option<CreatorHeart>,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatorHeart {
pub creator_heart_renderer: CreatorHeartRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreatorHeartRenderer {
pub is_hearted: bool,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorCommentBadge {
pub author_comment_badge_renderer: AuthorCommentBadgeRenderer,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorCommentBadgeRenderer {
pub icon: Icon,
}

View file

@ -0,0 +1,334 @@
---
source: src/client/player.rs
expression: player_data
---
info:
id: pPvd8UxmSbQ
title: Inspiring Cinematic Uplifting (Creative Commons)
description: "► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let's make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it's not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"
length: 163
thumbnails:
- url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/default.webp"
width: 120
height: 90
- url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/mqdefault.webp"
width: 320
height: 180
- url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/hqdefault.webp"
width: 480
height: 360
- url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/sddefault.webp"
width: 640
height: 480
channel:
id: UCbxxEi-ImPlbLx5F-fHetEg
name: RomanSenykMusic - Royalty Free Music
publish_date: "~"
view_count: 426567
keywords:
- no copyright music
- background music
- copyright free music
- non copyrighted music
- free music
- no copyright music cinematic
- inspiring music
- inspiring background music
- cinematic music
- cinematic background music
- no copyright music inspiring
- free music no copyright
- uplifting music
- trailer music no copyright
- trailer music
- download music
- free background music
- orchestral music
- romansenykmusic
- motivational music
- montage music
category: ~
is_live_content: false
is_family_safe: ~
video_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1619781&dur=163.143&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=17&lmt=1580005480199246&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=video%2F3gpp&mm=31%2C29&mn=sn-h0jeenek%2Csn-h0jelnez&ms=au%2Crdu&mt=1659459429&mv=m&mvi=5&pl=37&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhAJ2s7Pm4w42X3u3PWYViDeqIaE2tE9J6oIGpd0KB9gFsAiAH84QaJ4oUNivdRDUBi1ZYI7JSxESsPQ53mLInajKzcQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cdur%2Clmt&txp=2211222&vprv=1"
itag: 17
bitrate: 79452
average_bitrate: 79428
size: 1619781
index_range: ~
init_range: ~
width: 176
height: 144
fps: 7
quality: 144p
hdr: false
mime: "video/3gpp; codecs=\"mp4v.20.3, mp4a.40.2\""
format: 3gp
codec: mp4v
throttled: false
- 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"
itag: 18
bitrate: 561339
average_bitrate: 561109
size: 11439331
index_range: ~
init_range: ~
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\""
format: mp4
codec: avc1
throttled: false
video_only_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1224002&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=394&keepalive=yes&lmt=1608045375671513&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=AOq0QJ8wRQIgYCPleG9F86UoDRvzFL2xSUUI-HLZGw_P7qBOixlcmKsCIQChdmrJ1NvKo5Ra4QJ9ivR5V8fEcQs0f_3aUiqMhGB5DQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 394
bitrate: 67637
average_bitrate: 60063
size: 1224002
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 395
bitrate: 135747
average_bitrate: 109867
size: 2238952
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 134
bitrate: 538143
average_bitrate: 383195
size: 7808990
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.4d401e\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 396
bitrate: 258097
average_bitrate: 202682
size: 4130385
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"av01.0.01M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 397
bitrate: 436843
average_bitrate: 337281
size: 6873325
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/mp4; codecs=\"av01.0.04M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 398
bitrate: 1348419
average_bitrate: 1097369
size: 22365208
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.08M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 299
bitrate: 4190323
average_bitrate: 3208919
size: 65400181
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"avc1.64002a\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 399
bitrate: 2572342
average_bitrate: 2088624
size: 42567727
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.09M.08\""
format: mp4
codec: av01
throttled: false
audio_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=995840&dur=163.189&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=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhALhtrbIL9_CQBeXsEwxFqyPY1XqBCOceQc7y00h7XBS9AiAH9VkMnkuFCU1ACkYU__uApTwcYeDoYNU74VYmKED3Gw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"
itag: 139
bitrate: 49724
average_bitrate: 48818
size: 995840
index_range:
start: 641
end: 876
init_range:
start: 0
end: 640
mime: "audio/mp4; codecs=\"mp4a.40.5\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=934449&dur=163.061&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=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&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=AOq0QJ8wRAIgPmadKd9As393GMRmu1Ow4RfQkDQhY6SbPRnkLMYyZOoCIE9AIgMMJ7n5HD2gKv3c8-HrnkMeakq_uWUOivnWquJX&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 249
bitrate: 53039
average_bitrate: 45845
size: 934449
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=1245866&dur=163.061&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=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&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=AOq0QJ8wRQIge8uetzhejNg3DegY9EQkpvVam1gp8Jm-q6oqtb6Rn9wCIQD_VeQle7Z2H1uXB6qsYMGDU4OWA4h6YTTwMDmw5DDvuA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 250
bitrate: 71268
average_bitrate: 61123
size: 1245866
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2640283&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=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhAI8YylDImOPxxRo251u_RX6ir_j0p-gP_yQPcI6SxareAiArCxOcgrF9pxYS5bNYEnLGEQxRiEFJ0sT2Ydpa1G7x5A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"
itag: 140
bitrate: 130268
average_bitrate: 129508
size: 2640283
index_range:
start: 632
end: 867
init_range:
start: 0
end: 631
mime: "audio/mp4; codecs=\"mp4a.40.2\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=ANDROID&clen=2476314&dur=163.061&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=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAOiL-qJ04sA8FSOkEJfOYl3gFe4SzwYu_rAf3DMLHYigAiEA0Upi1HqqIu7NH_LTDL0jT1R5TTozQypL5FiSP9RoqtU%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%2Fwebm&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=AOq0QJ8wRQIgErpt4HOgIybMGrMD2qg9JB4O53n0jsCxkiI9JBxbz8ECIQDixyFJ54m4NsxhyFtIYPscMVp_G6RyvwrfKzdoya-62Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cvprv%2Cmime%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 251
bitrate: 140633
average_bitrate: 121491
size: 2476314
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
subtitles:
- url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&caps=asr&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1659484955&sparams=ip,ipbits,expire,v,caps,xoaf&signature=EBFE9BB6A8D01D674157DE1F45A3BEDCAB35496B.3ED931CA7233F0FF5B19062AA22F803CC3D78215&key=yt8&lang=en&fmt=srv3"
lang: en
lang_name: English
auto_generated: false
expires_in_seconds: 21540

View file

@ -0,0 +1,445 @@
---
source: src/client/player.rs
expression: player_data
---
info:
id: pPvd8UxmSbQ
title: Inspiring Cinematic Uplifting (Creative Commons)
description: "► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let's make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it's not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"
length: 163
thumbnails:
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLBSNHImLtGal2a95M5oyTT_uuTZlw"
width: 168
height: 94
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLANEm-jLrG1ec9hFpfa2rovJR4uqg"
width: 196
height: 110
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLA4_mATyxRz68LdA8oWuPKlS2gIUw"
width: 246
height: 138
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLB0IUWkEMwrYEtP_4pX2uuOBv1JPA"
width: 336
height: 188
- url: "https://i.ytimg.com/vi_webp/pPvd8UxmSbQ/maxresdefault.webp"
width: 1920
height: 1080
channel:
id: UCbxxEi-ImPlbLx5F-fHetEg
name: RomanSenykMusic - Royalty Free Music
publish_date: "2019-05-30T00:00:00"
view_count: 426567
keywords:
- no copyright music
- background music
- copyright free music
- non copyrighted music
- free music
- no copyright music cinematic
- inspiring music
- inspiring background music
- cinematic music
- cinematic background music
- no copyright music inspiring
- free music no copyright
- uplifting music
- trailer music no copyright
- trailer music
- download music
- free background music
- orchestral music
- romansenykmusic
- motivational music
- montage music
category: Music
is_live_content: false
is_family_safe: true
video_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=11439331&dur=163.096&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=18&lmt=1580005476071743&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=ig_QojS86GYHYg&ns=cOm8mnsR9HFwfq55dDyGyqYH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgNqstD2C4HG7Vn5En5Z4aUyH2mk7gAB9cyfOAWGCaWeoCIQDbxxJZuOnz_3RJNviFYADvgTO7u8YBYKtpFtp9Ujmk2w%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"
itag: 18
bitrate: 561339
average_bitrate: 561109
size: 11439331
index_range: ~
init_range: ~
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\""
format: mp4
codec: avc1
throttled: false
video_only_streams:
- 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=1484736&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=278&keepalive=yes&lmt=1608509388295661&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=AOq0QJ8wRQIgDYs7xrSqi-Co90zypk9zutEJ-aaEpNAHWnE57zVIfxgCIQDE0exs9SN8JH5OEJ8728Ke6bfa0CsUucFETHLk3IFF7w%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"
itag: 278
bitrate: 87458
average_bitrate: 72857
size: 1484736
index_range:
start: 218
end: 751
init_range:
start: 0
end: 217
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 394
bitrate: 67637
average_bitrate: 60063
size: 1224002
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 242
bitrate: 184064
average_bitrate: 145902
size: 2973283
index_range:
start: 219
end: 753
init_range:
start: 0
end: 218
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 395
bitrate: 135747
average_bitrate: 109867
size: 2238952
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 134
bitrate: 538143
average_bitrate: 383195
size: 7808990
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.4d401e\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 243
bitrate: 319085
average_bitrate: 253673
size: 5169510
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 396
bitrate: 258097
average_bitrate: 202682
size: 4130385
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"av01.0.01M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 244
bitrate: 539056
average_bitrate: 436270
size: 8890590
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 397
bitrate: 436843
average_bitrate: 337281
size: 6873325
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/mp4; codecs=\"av01.0.04M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 247
bitrate: 982813
average_bitrate: 812006
size: 16547577
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 1280
height: 720
fps: 30
quality: 720p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 302
bitrate: 2354009
average_bitrate: 1764202
size: 35955780
index_range:
start: 219
end: 771
init_range:
start: 0
end: 218
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 398
bitrate: 1348419
average_bitrate: 1097369
size: 22365208
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.08M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 299
bitrate: 4190323
average_bitrate: 3208919
size: 65400181
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"avc1.64002a\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 303
bitrate: 3832648
average_bitrate: 3090839
size: 62993617
index_range:
start: 219
end: 776
init_range:
start: 0
end: 218
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 399
bitrate: 2572342
average_bitrate: 2088624
size: 42567727
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.09M.08\""
format: mp4
codec: av01
throttled: false
audio_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=934449&dur=163.061&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=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhAOdVu1woKNveQspV4WPm1PHrOBuzlrnPu2ZLvyiYZCSbAiAYODN_y5t1oU334SHUqqgyc4Wnt-9If-W98Fd966fy2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"
itag: 249
bitrate: 53039
average_bitrate: 45845
size: 934449
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=1245866&dur=163.061&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=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhAMJJ-uGQureE70LIxHjHP9hFxqcWwsSlxXX6EjGKmFfEAiAvQ98YKkqUrweNnBZOI7pXJk1kuU_1hSsQ0KeNU4CbyQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"
itag: 250
bitrate: 71268
average_bitrate: 61123
size: 1245866
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2640283&dur=163.096&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=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhANXIw4pIwIPvMGWnJSrA_bnmBX6KPBPqak18aPtKsI8jAiBvisRnEtFax7OTrwKbOiktCihoMraLkCi7Rnnu6JGmeQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=2211222&vprv=1"
itag: 140
bitrate: 130268
average_bitrate: 129508
size: 2640283
index_range:
start: 632
end: 867
init_range:
start: 0
end: 631
mime: "audio/mp4; codecs=\"mp4a.40.2\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=WEB&clen=2476314&dur=163.061&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=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhANxHzq0WC6OvdTpPJp52z3eGAm-jzUX7fcKiWlJ0T9kEAiEA02Bjesi_an2-pUh0kHdKQe0s_7micbcv3JKiBlxsYGs%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhAKP_KjT_SSnz5WGXaveO56pJAEw166qT3cpBdAZI1zwCAiBWZKVQZxfOPWnqSp5FnRDyQBQ-6a2nZopyA1eHicgHtw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&spc=lT-KhrZGE2opztWyVdAtyUNlb8dXPDs&txp=1311222&vprv=1"
itag: 251
bitrate: 140633
average_bitrate: 121491
size: 2476314
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
subtitles:
- url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&caps=asr&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1659484955&sparams=ip,ipbits,expire,v,caps,xoaf&signature=AEE666C3CB25B28A0990FAF6483CAEFCC3F3BCEA.BBAD928634677A2CF95FB67B14F362750449F8F5&key=yt8&lang=en"
lang: en
lang_name: English
auto_generated: false
expires_in_seconds: 21540

View file

@ -0,0 +1,319 @@
---
source: src/client/player.rs
expression: player_data
---
info:
id: pPvd8UxmSbQ
title: Inspiring Cinematic Uplifting
description: ~
length: 163
thumbnails:
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/sddefault.jpg?sqp=-oaymwEWCJADEOEBIAQqCghqEJQEGHgg6AJIWg&rs=AOn4CLC-0nIQMyPuy8CtzqTMl6z1rmG_XQ"
width: 400
height: 225
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hq720.jpg?sqp=-oaymwEXCKAGEMIDIAQqCwjVARCqCBh4INgESFo&rs=AOn4CLD5woRwkLSroWdO5l5XPprkZc7sGQ"
width: 800
height: 450
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hq720.jpg?sqp=-oaymwEXCNUGEOADIAQqCwjVARCqCBh4INgESFo&rs=AOn4CLA1wPf8NLXqHitgwoNBp4ydFCtDiA"
width: 853
height: 480
channel:
id: UCbxxEi-ImPlbLx5F-fHetEg
name: Romansenykmusic
publish_date: "2019-05-30T00:00:00"
view_count: 426583
keywords:
- no copyright music
- background music
- copyright free music
- non copyrighted music
- free music
- no copyright music cinematic
- inspiring music
- inspiring background music
- cinematic music
- cinematic background music
- no copyright music inspiring
- free music no copyright
- uplifting music
- trailer music no copyright
- trailer music
- download music
- free background music
- orchestral music
- romansenykmusic
- motivational music
- montage music
category: Music
is_live_content: false
is_family_safe: true
video_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=11439331&dur=163.096&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=18&lmt=1580005476071743&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=mAEwZepBJSQPkQ&ns=orl5qWACo00YlHHyQZ7a6awH&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIhANfXubbDcXpc25m3F5xQ97ygJRjrTvm8ruVxgnxgFAUBAiAEnj_3KacDNTTLUk-6ZEbL-52zxmBLU1iuTEDx0NvJzA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&spc=lT-Khox4YuJQ2wmH79zYALRvsWTPCUc&txp=2211222&vprv=1"
itag: 18
bitrate: 561339
average_bitrate: 561109
size: 11439331
index_range: ~
init_range: ~
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\""
format: mp4
codec: avc1
throttled: false
video_only_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1484736&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=278&keepalive=yes&lmt=1608509388295661&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=AOq0QJ8wRAIgEQ0-VVvo41T4l2X26p5zP8Wo8sXOPmBWvCf2OW33ilgCIH2bIFOYgpmsml7FvRQj_SoLzPh7yBvmLZ61Kgsj4FUe&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"
itag: 278
bitrate: 87458
average_bitrate: 72857
size: 1484736
index_range:
start: 218
end: 751
init_range:
start: 0
end: 217
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 242
bitrate: 184064
average_bitrate: 145902
size: 2973283
index_range:
start: 219
end: 753
init_range:
start: 0
end: 218
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 134
bitrate: 538143
average_bitrate: 383195
size: 7808990
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.4d401e\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 243
bitrate: 319085
average_bitrate: 253673
size: 5169510
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 244
bitrate: 539056
average_bitrate: 436270
size: 8890590
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 247
bitrate: 982813
average_bitrate: 812006
size: 16547577
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 1280
height: 720
fps: 30
quality: 720p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 302
bitrate: 2354009
average_bitrate: 1764202
size: 35955780
index_range:
start: 219
end: 771
init_range:
start: 0
end: 218
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 299
bitrate: 4190323
average_bitrate: 3208919
size: 65400181
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"avc1.64002a\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 303
bitrate: 3832648
average_bitrate: 3090839
size: 62993617
index_range:
start: 219
end: 776
init_range:
start: 0
end: 218
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
audio_streams:
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=934449&dur=163.061&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=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRgIhAMUfr2X-eQJt1abn-IK1H4d5DtvKZuBaETo4opNi6mqCAiEAvBmrmuaoFjB1CJ2Kug87w-Uv6OCdxyrJ_3HIaHX9KBI%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"
itag: 249
bitrate: 53039
average_bitrate: 45845
size: 934449
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=1245866&dur=163.061&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=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIhAIflUU4t4Ukf4CXW3ttB5c8SnP4z4z3ef-7EFVMFv4U8AiAlcKmmofCTzzr2NRRsRVosQdpDBphUqWyVzS7noGrixw%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"
itag: 250
bitrate: 71268
average_bitrate: 61123
size: 1245866
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2640283&dur=163.096&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=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRgIhALBveArAZ7DP9r1BIpNz6ZXst5MtzvUM72jhtYMrildCAiEArvwqaqcowZwR_UrRM-O7jq2CMxgBtbnmul27AEcBqEI%3D&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"
itag: 140
bitrate: 130268
average_bitrate: 129508
size: 2640283
index_range:
start: 632
end: 867
init_range:
start: 0
end: 631
mime: "audio/mp4; codecs=\"mp4a.40.2\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?c=WEB_REMIX&clen=2476314&dur=163.061&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=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAMwYJqxve8BSujC-oaSFBbq67p-rFi7saU5V8Yb3qrjLAiEAlrMKR_sadHrkFpy7o7lGzKOCmU1OQazCNBbXjDT2a-o%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRAIgARPqD-6172OshMHeV8DpONV7tnPvdsxcg8QlaIGxcuMCICSe8LWvhRTEO2bdAQ43OzOoc5XfJcj3veyhScVXVz8O&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"
itag: 251
bitrate: 140633
average_bitrate: 121491
size: 2476314
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
subtitles:
- url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&caps=asr&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1659491074&sparams=ip,ipbits,expire,v,caps,xoaf&signature=3CE2AD03E73FC43D83E992060A96D7B12F8E08E5.66CE90B1F3337004848429E1091E8423497239BF&key=yt8&lang=en"
lang: en
lang_name: English
auto_generated: false
expires_in_seconds: 21540

View file

@ -0,0 +1,131 @@
---
source: src/client/player.rs
expression: player_data
---
info:
id: pPvd8UxmSbQ
title: Inspiring Cinematic Uplifting (Creative Commons)
description: "► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let's make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it's not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"
length: 163
thumbnails:
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/mqdefault.jpg"
width: 320
height: 180
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg"
width: 480
height: 360
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/sddefault.jpg"
width: 640
height: 480
channel:
id: UCbxxEi-ImPlbLx5F-fHetEg
name: RomanSenykMusic - Royalty Free Music
publish_date: "~"
view_count: 426567
keywords:
- no copyright music
- background music
- copyright free music
- non copyrighted music
- free music
- no copyright music cinematic
- inspiring music
- inspiring background music
- cinematic music
- cinematic background music
- no copyright music inspiring
- free music no copyright
- uplifting music
- trailer music no copyright
- trailer music
- download music
- free background music
- orchestral music
- romansenykmusic
- motivational music
- montage music
category: ~
is_live_content: false
is_family_safe: ~
video_streams: []
video_only_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=7808990&dur=163.029&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=134&keepalive=yes&lmt=1580005649163759&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=AOq0QJ8wRgIhAMFQo_RyC3Ud44QVGtKckMcuq5UQ3J7CgYsYl0bXaWMUAiEAhMi1h0ru4zoIGX0jBZT23dozvtrhf_m61p4qbAfEhZo%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"
itag: 134
bitrate: 538143
average_bitrate: 383195
size: 7808990
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.4D401E\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 299
bitrate: 4190323
average_bitrate: 3208919
size: 65400181
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"avc1.64002A\""
format: mp4
codec: avc1
throttled: false
audio_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=995840&dur=163.189&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=139&keepalive=yes&lmt=1580005582214385&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRgIhAMXDvZZznm1LafIKh_pdGf-TjY5Kz-F9N67o6gXenKouAiEA5qji45i5ABmAytPDOORjw0OaBmwX88S7bgUWcmF-_LU%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"
itag: 139
bitrate: 49724
average_bitrate: 48818
size: 995840
index_range:
start: 641
end: 876
init_range:
start: 0
end: 640
mime: "audio/mp4; codecs=\"mp4a.40.5\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=IOS&clen=2640283&dur=163.096&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=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRQIgWKVoDpyI6QmVnkdGzdirFtjMAXhmLex64VTO7UUJd-4CIQDoJKkT2-Kpa7j0merJJoZDs4IkkXSjdNm3bvdCL8t2Pg%3D%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIgcsPm6rrUAwCi1VTGf0FMDTjzjM01__iTC13PnzDTFeQCIQCJ_EGeKVZztkmK3Cr7gVuxUP83XCSlP01YLx5FO-PPcQ%3D%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"
itag: 140
bitrate: 130268
average_bitrate: 129508
size: 2640283
index_range:
start: 632
end: 867
init_range:
start: 0
end: 631
mime: "audio/mp4; codecs=\"mp4a.40.2\""
format: m4a
codec: mp4a
throttled: false
track: ~
subtitles:
- url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&caps=asr&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1659484955&sparams=ip,ipbits,expire,v,caps,xoaf&signature=A9D9281218AFF99DC34B5AA2F44D33455F419673.44DBC65B8479A5F088D6136FC4364894EECB3E0E&key=yt8&lang=en"
lang: en
lang_name: English
auto_generated: false
expires_in_seconds: 21540

View file

@ -0,0 +1,445 @@
---
source: src/client/player.rs
expression: player_data
---
info:
id: pPvd8UxmSbQ
title: Inspiring Cinematic Uplifting (Creative Commons)
description: "► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let's make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it's not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"
length: 163
thumbnails:
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/default.jpg"
width: 120
height: 90
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/mqdefault.jpg"
width: 320
height: 180
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg"
width: 480
height: 360
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/sddefault.jpg"
width: 640
height: 480
- url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/maxresdefault.jpg"
width: 1920
height: 1080
channel:
id: UCbxxEi-ImPlbLx5F-fHetEg
name: RomanSenykMusic - Royalty Free Music
publish_date: "~"
view_count: 426567
keywords:
- no copyright music
- background music
- copyright free music
- non copyrighted music
- free music
- no copyright music cinematic
- inspiring music
- inspiring background music
- cinematic music
- cinematic background music
- no copyright music inspiring
- free music no copyright
- uplifting music
- trailer music no copyright
- trailer music
- download music
- free background music
- orchestral music
- romansenykmusic
- motivational music
- montage music
category: ~
is_live_content: false
is_family_safe: ~
video_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=11439331&dur=163.096&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=18&lmt=1580005476071743&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=nyEBf7PFRQzT2Q&ns=K7tV6wZuXWbiwSsCxjud4dwH&pcm2=yes&pl=37&ratebypass=yes&rbqsm=fr&requiressl=yes&sig=AOq0QJ8wRQIgA7RsBrMSenhLIt0g53KA8JKsUoMfcUilAk8IWpN7Rw0CIQDrRhd7fw58T5vBIqMJk8kXvvHbVODatGsSMbl6CL_s6A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&txp=2211222&vprv=1"
itag: 18
bitrate: 561339
average_bitrate: 561109
size: 11439331
index_range: ~
init_range: ~
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\""
format: mp4
codec: avc1
throttled: false
video_only_streams:
- 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=1484736&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=278&keepalive=yes&lmt=1608509388295661&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=AOq0QJ8wRQIgUfAx1SnRXZMjOZ9y8K-PFD5_vaoqQOhCT-fgK6iX8W4CIQDaAZnRtrvXNNQYLzh87R9UUbL6swXSECd0onuWwxpOjA%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"
itag: 278
bitrate: 87458
average_bitrate: 72857
size: 1484736
index_range:
start: 218
end: 751
init_range:
start: 0
end: 217
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 394
bitrate: 67637
average_bitrate: 60063
size: 1224002
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 256
height: 144
fps: 30
quality: 144p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 242
bitrate: 184064
average_bitrate: 145902
size: 2973283
index_range:
start: 219
end: 753
init_range:
start: 0
end: 218
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 395
bitrate: 135747
average_bitrate: 109867
size: 2238952
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 426
height: 240
fps: 30
quality: 240p
hdr: false
mime: "video/mp4; codecs=\"av01.0.00M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 134
bitrate: 538143
average_bitrate: 383195
size: 7808990
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"avc1.4d401e\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 243
bitrate: 319085
average_bitrate: 253673
size: 5169510
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 396
bitrate: 258097
average_bitrate: 202682
size: 4130385
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 640
height: 360
fps: 30
quality: 360p
hdr: false
mime: "video/mp4; codecs=\"av01.0.01M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 244
bitrate: 539056
average_bitrate: 436270
size: 8890590
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 397
bitrate: 436843
average_bitrate: 337281
size: 6873325
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 854
height: 480
fps: 30
quality: 480p
hdr: false
mime: "video/mp4; codecs=\"av01.0.04M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 247
bitrate: 982813
average_bitrate: 812006
size: 16547577
index_range:
start: 220
end: 754
init_range:
start: 0
end: 219
width: 1280
height: 720
fps: 30
quality: 720p
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 302
bitrate: 2354009
average_bitrate: 1764202
size: 35955780
index_range:
start: 219
end: 771
init_range:
start: 0
end: 218
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 398
bitrate: 1348419
average_bitrate: 1097369
size: 22365208
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1280
height: 720
fps: 60
quality: 720p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.08M.08\""
format: mp4
codec: av01
throttled: false
- 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"
itag: 299
bitrate: 4190323
average_bitrate: 3208919
size: 65400181
index_range:
start: 740
end: 1155
init_range:
start: 0
end: 739
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"avc1.64002a\""
format: mp4
codec: avc1
throttled: false
- 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"
itag: 303
bitrate: 3832648
average_bitrate: 3090839
size: 62993617
index_range:
start: 219
end: 776
init_range:
start: 0
end: 218
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/webm; codecs=\"vp9\""
format: webm
codec: vp9
throttled: false
- 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"
itag: 399
bitrate: 2572342
average_bitrate: 2088624
size: 42567727
index_range:
start: 700
end: 1115
init_range:
start: 0
end: 699
width: 1920
height: 1080
fps: 60
quality: 1080p60
hdr: false
mime: "video/mp4; codecs=\"av01.0.09M.08\""
format: mp4
codec: av01
throttled: false
audio_streams:
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=934449&dur=163.061&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=249&keepalive=yes&lmt=1608509101590706&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRgIhAKvEZsn0-D0htBi8J0Eu97RHLDnsiv4QrcG6-IGxnvrJAiEA90KJc7FhPTsFG5h_nXnDgyAeH1XrN6K-DjpIClZFSLw%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 249
bitrate: 53039
average_bitrate: 45845
size: 934449
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=1245866&dur=163.061&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=250&keepalive=yes&lmt=1608509101111096&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRQIgdXM4noOweo7ObtzNi89kUq6sJ3zVxwtqYRSGoTG5nx8CIQDYchUdbpIodrPu6p1LDPr-fMogyV5qHLhKG68u3hXoLw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 250
bitrate: 71268
average_bitrate: 61123
size: 1245866
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2640283&dur=163.096&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=140&keepalive=yes&lmt=1580005579712232&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRgIhAMYwUTZ3JEkIJFY-hwYPGvGZw3M0IzvFHenqSGb6ksrkAiEA6kBUIvpILbdtJFpB6KHjbLG2nGdAS0MaodDvaEC5nwU%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=2211222&vprv=1"
itag: 140
bitrate: 130268
average_bitrate: 129508
size: 2640283
index_range:
start: 632
end: 867
init_range:
start: 0
end: 631
mime: "audio/mp4; codecs=\"mp4a.40.2\""
format: m4a
codec: mp4a
throttled: false
track: ~
- url: "https://rr4---sn-h0jelnez.googlevideo.com/videoplayback?c=TVHTML5_SIMPLY_EMBEDDED_PLAYER&clen=2476314&dur=163.061&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=251&keepalive=yes&lmt=1608509101894140&lsig=AG3C_xAwRgIhAIWRo8U-MB6jhlipPec3A3m5-StMaX64EEGBEE3LWaNiAiEA_8QPrTStO0ISMA5Jex-G2JfPpqyw-vltC8nAFXyPz98%3D&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&mh=mQ&mime=audio%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=AOq0QJ8wRAIgFTu4utWltpNE2oh6bB_y36RqUxl-B47UPoShqGF56QoCIHb4_DD2-sLJv9x_NPp1j_NyU4J6cqShl3rUJNuigwuh&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cpcm2%2Cvprv%2Cmime%2Cns%2Cgir%2Cclen%2Cotfp%2Cdur%2Clmt&txp=1311222&vprv=1"
itag: 251
bitrate: 140633
average_bitrate: 121491
size: 2476314
index_range:
start: 266
end: 551
init_range:
start: 0
end: 265
mime: "audio/webm; codecs=\"opus\""
format: webm
codec: opus
throttled: false
track: ~
subtitles:
- url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&caps=asr&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1659484955&sparams=ip,ipbits,expire,v,caps,xoaf&signature=DAB4A9E85FEBC1ED6E527CD113FB58F798859727.32748FCE40F82BE8A271BF5FF248C2B283B57F89&key=yt8&lang=en"
lang: en
lang_name: English
auto_generated: false
expires_in_seconds: 21540

View file

@ -3,47 +3,43 @@ use fancy_regex::Regex;
use log::debug;
use once_cell::sync::Lazy;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use std::result::Result::Ok;
use crate::cache::{Cache, DeobfData};
use crate::util;
pub struct Deobfuscator {
data: DeobfData,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DeobfData {
pub js_url: String,
pub sig_fn: String,
pub nsig_fn: String,
pub sts: String,
}
impl Deobfuscator {
pub async fn new(http: Client) -> Result<Self> {
let js_url = get_player_js_url(&http)
.await
.context("Failed to retrieve player.js URL")?;
pub async fn from_fetched_info(http: Client, cache: Cache) -> Result<Self> {
let data = cache
.get_deobf_data(async move {
let js_url = get_player_js_url(&http)
.await
.context("Failed to retrieve player.js URL")?;
let player_js = get_response(&http, &js_url)
.await
.context("Failed to download player.js")?;
let player_js = get_response(&http, &js_url)
.await
.context("Failed to download player.js")?;
debug!("Downloaded player.js from {}", js_url);
debug!("Downloaded player.js from {}", js_url);
let sig_fn = get_sig_fn(&player_js)?;
let nsig_fn = get_nsig_fn(&player_js)?;
let sts = get_sts(&player_js)?;
let sig_fn = get_sig_fn(&player_js)?;
let nsig_fn = get_nsig_fn(&player_js)?;
let sts = get_sts(&player_js)?;
Ok(Self {
data: DeobfData {
js_url,
nsig_fn,
sig_fn,
sts,
},
})
Ok(DeobfData {
js_url,
nsig_fn,
sig_fn,
sts,
})
})
.await?;
Ok(Self { data })
}
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
@ -57,10 +53,6 @@ impl Deobfuscator {
pub fn get_sts(&self) -> String {
self.data.sts.to_owned()
}
pub fn get_data(&self) -> DeobfData {
self.data.to_owned()
}
}
impl From<DeobfData> for Deobfuscator {
@ -88,7 +80,7 @@ fn get_sig_fn_name(player_js: &str) -> Result<String> {
}
fn caller_function(fn_name: &str) -> String {
format!("var {}={};", DEOBFUSCATION_FUNC_NAME, fn_name)
"var ".to_owned() + DEOBFUSCATION_FUNC_NAME + "=" + &fn_name + ";"
}
fn get_sig_fn(player_js: &str) -> Result<String> {
@ -353,7 +345,7 @@ fn get_sts(player_js: &str) -> Result<String> {
Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());
Ok(some_or_bail!(
STS_PATTERN.captures(player_js)?,
STS_PATTERN.captures(&player_js)?,
Err(anyhow!("could not find sts"))
)
.get(1)
@ -480,7 +472,10 @@ 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(tokio::test)]
async fn t_update() {
let client = Client::new();
let deobf = Deobfuscator::new(client).await.unwrap();
let cache = Cache::default();
let deobf = Deobfuscator::from_fetched_info(client, cache)
.await
.unwrap();
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
println!("{}", deobf_sig);

View file

@ -27,8 +27,8 @@ fn get_download_range(offset: u64, size: Option<u64>) -> Range<u64> {
let chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
let mut chunk_end = offset + chunk_size;
if let Some(size) = size {
chunk_end = chunk_end.min(size - 1)
if size.is_some() {
chunk_end = chunk_end.min(size.unwrap() - 1)
}
Range {
@ -41,7 +41,7 @@ fn parse_cr_header(cr_header: &str) -> Result<(u64, u64)> {
static PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"bytes (\d+)-(\d+)/(\d+)"#).unwrap());
let captures = some_or_bail!(
PATTERN.captures(cr_header).ok().flatten(),
PATTERN.captures(&cr_header).ok().flatten(),
Err(anyhow!(
"Content-Range header '{}' does not match pattern.",
cr_header
@ -77,7 +77,10 @@ async fn download_single_file<P: Into<PathBuf>>(
let (url_base, url_params) = util::url_to_params(url)?;
let is_gvideo = url_base.ends_with(".googlevideo.com/videoplayback");
if is_gvideo {
size = url_params.get("clen").and_then(|s| s.parse::<u64>().ok());
size = url_params
.get("clen")
.map(|s| s.parse::<u64>().ok())
.flatten();
}
// Check if file is partially downloaded
@ -254,7 +257,6 @@ struct StreamDownload {
video_codec: Option<VideoCodec>,
}
#[allow(clippy::too_many_arguments)]
pub async fn download_video(
player_data: &VideoPlayer,
output_dir: &str,
@ -325,7 +327,7 @@ pub async fn download_video(
_ => {
let mut downloads: Vec<StreamDownload> = Vec::new();
if let Some(v) = video {
video.map(|v| {
downloads.push(StreamDownload {
file: download_dir.join(format!(
"{}.video{}",
@ -336,8 +338,8 @@ pub async fn download_video(
video_codec: Some(v.codec),
audio_codec: None,
});
}
if let Some(a) = audio {
});
audio.map(|a| {
downloads.push(StreamDownload {
file: download_dir.join(format!(
"{}.audio{}",
@ -348,7 +350,7 @@ pub async fn download_video(
video_codec: None,
audio_codec: Some(a.codec),
})
}
});
pb.set_message(format!("Downloading {}", title));
download_streams(&downloads, http, pb.clone()).await?;

View file

@ -1,5 +1,4 @@
#![allow(dead_code)]
#![warn(clippy::todo)]
#[macro_use]
mod macros;
@ -15,7 +14,7 @@ mod util;
// pub mod client;
pub mod cache;
pub mod client;
pub mod client2;
pub mod download;
pub mod model;
pub mod report;

View file

@ -59,7 +59,7 @@ pub struct VideoStream {
pub itag: u32,
pub bitrate: u32,
pub average_bitrate: u32,
pub size: Option<u64>,
pub size: u64,
pub index_range: Option<Range<u32>>,
pub init_range: Option<Range<u32>>,
pub width: u32,

View file

@ -9,8 +9,6 @@ use chrono::{DateTime, Local};
use log::error;
use serde::{Deserialize, Serialize};
use crate::deobfuscate::DeobfData;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Report {
/// Rust package name (`rustypipe`)
@ -27,9 +25,9 @@ pub struct Report {
pub error: Option<String>,
/// Detailed error/warning messages
pub msgs: Vec<String>,
/// Deobfuscation data (only for player requests)
#[serde(skip_serializing_if = "Option::is_none")]
pub deobf_data: Option<DeobfData>,
// /// Deobfuscation data (only for player requests)
// #[serde(skip_serializing_if = "Option::is_none")]
// pub deobf_data: Option<DeobfData>,
/// HTTP request data
pub http_request: HTTPRequest,
}
@ -86,7 +84,7 @@ impl JsonFileReporter {
impl Default for JsonFileReporter {
fn default() -> Self {
Self {
path: Path::new("rustypipe_reports").to_path_buf(),
path: Path::new("RustyPipeReports").to_path_buf(),
}
}
}
@ -98,12 +96,10 @@ impl Reporter for JsonFileReporter {
}
}
#[cfg(feature = "yaml")]
pub struct YamlFileReporter {
path: PathBuf,
}
#[cfg(feature = "yaml")]
impl YamlFileReporter {
pub fn new<P: AsRef<Path>>(path: P) -> Self {
Self {
@ -118,16 +114,14 @@ impl YamlFileReporter {
}
}
#[cfg(feature = "yaml")]
impl Default for YamlFileReporter {
fn default() -> Self {
Self {
path: Path::new("rustypipe_reports").to_path_buf(),
path: Path::new("RustyPipeReports").to_path_buf(),
}
}
}
#[cfg(feature = "yaml")]
impl Reporter for YamlFileReporter {
fn report(&self, report: &Report) {
self._report(report)

View file

@ -1,36 +1,5 @@
pub mod range;
pub mod text;
mod range;
mod vec_log_err;
pub use range::Range;
pub use vec_log_err::VecLogError;
use std::fmt::Debug;
#[derive(Clone)]
pub struct MapResult<T> {
pub c: T,
pub warnings: Vec<String>,
}
impl<T> Debug for MapResult<T>
where
T: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.c.fmt(f)
}
}
impl<T> Default for MapResult<T>
where
T: Default,
{
fn default() -> Self {
Self {
c: Default::default(),
warnings: Vec::new(),
}
}
}

View file

@ -223,7 +223,11 @@ impl<'de> DeserializeAs<'de, Vec<TextLink>> for TextLinks {
D: Deserializer<'de>,
{
let link = TextLinkInternal::deserialize(deserializer)?;
Ok(link.runs.iter().filter_map(map_text_linkrun).collect())
Ok(link
.runs
.iter()
.filter_map(|r| map_text_linkrun(r))
.collect())
}
}

View file

@ -6,7 +6,7 @@ use serde::{
};
use serde_with::{de::DeserializeAsWrap, DeserializeAs};
use super::MapResult;
use crate::client2::MapResult;
pub struct VecLogError<T>(PhantomData<T>);
@ -89,7 +89,7 @@ mod tests {
use serde::Deserialize;
use serde_with::serde_as;
use crate::serializer::MapResult;
use crate::client2::MapResult;
#[serde_as]
#[derive(Debug, Deserialize)]

View file

@ -52,24 +52,24 @@ impl Mul<u8> for TimeAgo {
}
}
impl From<TimeAgo> for DateTime<Local> {
fn from(ta: TimeAgo) -> Self {
impl Into<DateTime<Local>> for TimeAgo {
fn into(self) -> DateTime<Local> {
let ts = Local::now();
match ta.unit {
TimeUnit::Second => ts - Duration::seconds(ta.n as i64),
TimeUnit::Minute => ts - Duration::minutes(ta.n as i64),
TimeUnit::Hour => ts - Duration::hours(ta.n as i64),
TimeUnit::Day => ts - Duration::days(ta.n as i64),
TimeUnit::Week => ts - Duration::weeks(ta.n as i64),
TimeUnit::Month => chronoutil::shift_months(ts, -(ta.n as i32)),
TimeUnit::Year => chronoutil::shift_years(ts, -(ta.n as i32)),
match self.unit {
TimeUnit::Second => ts - Duration::seconds(self.n as i64),
TimeUnit::Minute => ts - Duration::minutes(self.n as i64),
TimeUnit::Hour => ts - Duration::hours(self.n as i64),
TimeUnit::Day => ts - Duration::days(self.n as i64),
TimeUnit::Week => ts - Duration::weeks(self.n as i64),
TimeUnit::Month => chronoutil::shift_months(ts, -(self.n as i32)),
TimeUnit::Year => chronoutil::shift_years(ts, -(self.n as i32)),
}
}
}
impl From<ParsedDate> for DateTime<Local> {
fn from(date: ParsedDate) -> Self {
match date {
impl Into<DateTime<Local>> for ParsedDate {
fn into(self) -> DateTime<Local> {
match self {
ParsedDate::Absolute(date) => Local
.from_local_datetime(&NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)))
.unwrap(),
@ -103,23 +103,29 @@ fn parse_ta_token(entry: &dictionary::Entry, nd: bool, filtered_str: &str) -> Op
if entry.by_char {
filtered_str.chars().find_map(|word| {
tokens.get(&word.to_string()).and_then(|t| match t.unit {
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
None => {
qu = t.n;
None
}
})
tokens
.get(&word.to_string())
.map(|t| match t.unit {
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
None => {
qu = t.n;
None
}
})
.flatten()
})
} else {
filtered_str.split_whitespace().find_map(|word| {
tokens.get(word).and_then(|t| match t.unit {
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
None => {
qu = t.n;
None
}
})
tokens
.get(word)
.map(|t| match t.unit {
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
None => {
qu = t.n;
None
}
})
.flatten()
})
}
}
@ -131,7 +137,7 @@ fn parse_textual_month(entry: &dictionary::Entry, filtered_str: &str) -> Option<
} else {
filtered_str
.split_whitespace()
.find_map(|word| entry.months.get(word).copied())
.find_map(|word| entry.months.get(word).map(|n| *n))
}
}
@ -139,7 +145,7 @@ pub fn parse_timeago(lang: Language, textual_date: &str) -> Option<TimeAgo> {
let entry = dictionary::entry(lang);
let filtered_str = filter_str(textual_date);
let qu: u8 = util::parse_numeric(textual_date).unwrap_or(1);
let qu: u8 = util::parse_numeric(&textual_date).unwrap_or(1);
parse_ta_token(&entry, false, &filtered_str).map(|ta| ta * qu)
}
@ -157,7 +163,8 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
match nums.len() {
0 => match parse_ta_token(&entry, true, &filtered_str) {
Some(timeago) => Some(ParsedDate::Relative(timeago)),
None => parse_ta_token(&entry, false, &filtered_str).map(ParsedDate::Relative),
None => parse_ta_token(&entry, false, &filtered_str)
.map(|timeago| ParsedDate::Relative(timeago)),
},
1 => parse_ta_token(&entry, false, &filtered_str)
.map(|timeago| ParsedDate::Relative(timeago * nums[0] as u8)),
@ -182,7 +189,7 @@ pub fn parse_textual_date(lang: Language, textual_date: &str) -> Option<ParsedDa
match (y, m, d) {
(Some(y), Some(m), Some(d)) => {
NaiveDate::from_ymd_opt(y.into(), m.into(), d.into())
.map(ParsedDate::Absolute)
.map(|d| ParsedDate::Absolute(d))
}
_ => None,
}

View file

@ -41,15 +41,16 @@ pub fn generate_content_playback_nonce() -> String {
///
/// `example.com/api?k1=v1&k2=v2 => example.com/api; {k1: v1, k2: v2}`
pub fn url_to_params(url: &str) -> Result<(String, BTreeMap<String, String>)> {
let mut parsed_url = Url::parse(url)?;
let parsed_url = Url::parse(url)?;
let url_params: BTreeMap<String, String> = parsed_url
.query_pairs()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
parsed_url.set_query(None);
let mut url_base = parsed_url.clone();
url_base.set_query(None);
Ok((parsed_url.to_string(), url_params))
Ok((url_base.to_string(), url_params))
}
/// Parse a string after removing all non-numeric characters