Compare commits
No commits in common. "8548bc81e923e33f0e6013751aecd3e1e6ef6305" and "925652acdd1c5ab8a040f9681f318be8b1a0ec3d" have entirely different histories.
8548bc81e9
...
925652acdd
50 changed files with 11548 additions and 1622 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
/target
|
/target
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
|
|
||||||
rustypipe_reports
|
RustyPipeReports
|
||||||
rustypipe_cache.json
|
RustyPipeCache.json
|
||||||
|
rusty-tube.json
|
||||||
|
|
|
@ -7,16 +7,12 @@ edition = "2021"
|
||||||
members = [".", "cli"]
|
members = [".", "cli"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["default-tls", "yaml"]
|
default = ["default-tls"]
|
||||||
|
|
||||||
# Reqwest TLS
|
|
||||||
default-tls = ["reqwest/default-tls"]
|
default-tls = ["reqwest/default-tls"]
|
||||||
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
|
rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
|
||||||
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
|
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
|
||||||
|
|
||||||
# Error reports in yaml format
|
|
||||||
yaml = ["serde_yaml"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# quick-js = "0.4.1"
|
# quick-js = "0.4.1"
|
||||||
quick-js = { path = "../quickjs-rs" }
|
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"]}
|
tokio = {version = "1.20.0", features = ["macros", "fs", "process"]}
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0.82"
|
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"] }
|
serde_with = {version = "2.0.0", features = ["json"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
async-trait = "0.1.56"
|
||||||
chrono = {version = "0.4.19", features = ["serde"]}
|
chrono = {version = "0.4.19", features = ["serde"]}
|
||||||
chronoutil = "0.2.3"
|
chronoutil = "0.2.3"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
|
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]}
|
rustypipe = {path = "../", default_features = false, features = ["rustls-tls-native-roots"]}
|
||||||
reqwest = {version = "0.11.11", default_features = false}
|
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"
|
indicatif = "0.17.0"
|
||||||
futures = "0.3.21"
|
futures = "0.3.21"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
|
|
|
@ -6,7 +6,7 @@ use futures::stream::{self, StreamExt};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use reqwest::{Client, ClientBuilder};
|
use reqwest::{Client, ClientBuilder};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe},
|
client2::{ClientType, RustyPipe},
|
||||||
model::stream_filter::Filter,
|
model::stream_filter::Filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +59,6 @@ async fn download_single_video(
|
||||||
|
|
||||||
let res = async {
|
let res = async {
|
||||||
let player_data = rp
|
let player_data = rp
|
||||||
.query()
|
|
||||||
.get_player(video_id.as_str(), ClientType::TvHtml5Embed)
|
.get_player(video_id.as_str(), ClientType::TvHtml5Embed)
|
||||||
.await
|
.await
|
||||||
.context(format!(
|
.context(format!(
|
||||||
|
@ -149,7 +148,7 @@ async fn download_playlist(
|
||||||
.expect("unable to build the HTTP client");
|
.expect("unable to build the HTTP client");
|
||||||
|
|
||||||
let rp = RustyPipe::default();
|
let rp = RustyPipe::default();
|
||||||
let playlist = rp.query().get_playlist(id).await.unwrap();
|
let playlist = rp.get_playlist(id).await.unwrap();
|
||||||
|
|
||||||
// Indicatif setup
|
// Indicatif setup
|
||||||
let multi = MultiProgress::new();
|
let multi = MultiProgress::new();
|
||||||
|
|
365
src/cache.rs
365
src/cache.rs
|
@ -1,57 +1,364 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
fs::File,
|
||||||
|
future::Future,
|
||||||
|
io::BufReader,
|
||||||
path::{Path, PathBuf},
|
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 {
|
#[derive(Default, Debug, Clone)]
|
||||||
fn write(&self, data: &str);
|
pub struct Cache {
|
||||||
fn read(&self) -> Option<String>;
|
file: Option<PathBuf>,
|
||||||
|
data: Arc<Mutex<CacheData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FileStorage {
|
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||||
path: PathBuf,
|
struct CacheData {
|
||||||
|
desktop_client: Option<CacheEntry<ClientData>>,
|
||||||
|
music_client: Option<CacheEntry<ClientData>>,
|
||||||
|
deobf: Option<CacheEntry<DeobfData>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileStorage {
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
struct CacheEntry<T> {
|
||||||
|
last_update: DateTime<Utc>,
|
||||||
|
data: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for CacheEntry<T> {
|
||||||
|
fn from(f: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: path.as_ref().to_path_buf(),
|
last_update: Utc::now(),
|
||||||
|
data: f,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FileStorage {
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
fn default() -> Self {
|
pub struct ClientData {
|
||||||
Self {
|
pub version: String,
|
||||||
path: Path::new("rustypipe_cache.json").into(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CacheStorage for FileStorage {
|
pub async fn get_music_client_data<F>(&self, updater: F) -> Result<ClientData>
|
||||||
fn write(&self, data: &str) {
|
where
|
||||||
fs::write(&self.path, data).unwrap_or_else(|e| {
|
F: Future<Output = Result<ClientData>> + Send + 'static,
|
||||||
error!(
|
{
|
||||||
"Could not write cache to file `{}`. Error: {}",
|
let mut cache = self.data.lock().await;
|
||||||
self.path.to_string_lossy(),
|
|
||||||
e
|
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read(&self) -> Option<String> {
|
pub async fn get_deobf_data<F>(&self, updater: F) -> Result<DeobfData>
|
||||||
match fs::read_to_string(&self.path) {
|
where
|
||||||
Ok(data) => Some(data),
|
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) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"Could not load cache from file `{}`. Error: {}",
|
"Could not load cache from json, falling back to default. Error: {}",
|
||||||
self.path.to_string_lossy(),
|
|
||||||
e
|
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
41
src/client/channel.rs
Normal 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 {
|
||||||
|
|
||||||
|
}
|
1222
src/client/mod.rs
1222
src/client/mod.rs
File diff suppressed because it is too large
Load diff
|
@ -1,28 +1,20 @@
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
|
use chrono::{Local, NaiveDateTime, NaiveTime, TimeZone};
|
||||||
use fancy_regex::Regex;
|
use fancy_regex::Regex;
|
||||||
|
use log::{error, warn};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::{Method, Url};
|
use reqwest::Method;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use super::{response, ClientType, ContextYT, RustyTube, YTClient};
|
||||||
deobfuscate::Deobfuscator,
|
use crate::{client::response::player, deobfuscate::Deobfuscator, model::*, util};
|
||||||
model::{
|
|
||||||
AudioCodec, AudioFormat, AudioStream, AudioTrack, Channel, Language, Subtitle, VideoCodec,
|
|
||||||
VideoFormat, VideoInfo, VideoPlayer, VideoStream,
|
|
||||||
},
|
|
||||||
util,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{
|
|
||||||
response::{self, player},
|
|
||||||
ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -57,18 +49,36 @@ struct QContentPlaybackContext {
|
||||||
referer: String,
|
referer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyTube {
|
||||||
pub async fn get_player(self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
|
pub async fn get_player(&self, video_id: &str, client_type: ClientType) -> Result<VideoPlayer> {
|
||||||
let q1 = self.clone();
|
let client = self.get_ytclient(client_type);
|
||||||
let t_context = tokio::spawn(async move { q1.get_context(client_type, false).await });
|
let (context, deobf) = tokio::join!(
|
||||||
let q2 = self.clone();
|
client.get_context(false),
|
||||||
let t_deobf = tokio::spawn(async move { q2.get_deobf().await });
|
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 resp = client
|
||||||
let context = context.unwrap();
|
.request_builder(Method::POST, "player")
|
||||||
let deobf = deobf.unwrap()?;
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
|
||||||
let request_body = if client_type.is_web() {
|
let player_response = resp.json::<response::Player>().await?;
|
||||||
|
map_player_data(player_response, &deobf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request_body(
|
||||||
|
client: Arc<dyn YTClient>,
|
||||||
|
deobf: &Deobfuscator,
|
||||||
|
context: ContextYT,
|
||||||
|
video_id: &str,
|
||||||
|
) -> QPlayer {
|
||||||
|
if client.get_type().is_web() {
|
||||||
QPlayer {
|
QPlayer {
|
||||||
context,
|
context,
|
||||||
playback_context: Some(QPlaybackContext {
|
playback_context: Some(QPlaybackContext {
|
||||||
|
@ -91,177 +101,6 @@ impl RustyPipeQuery {
|
||||||
content_check_ok: true,
|
content_check_ok: true,
|
||||||
racy_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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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.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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
let nsig: String;
|
||||||
match url_params.get("n") {
|
match url_params.get("n") {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
nsig = if n == &last_nsig[0] {
|
nsig = if n.to_owned() == last_nsig[0] {
|
||||||
last_nsig[1].to_owned()
|
last_nsig[1].to_owned()
|
||||||
} else {
|
} else {
|
||||||
let nsig = deobf.deobfuscate_nsig(n)?;
|
let nsig = deobf.deobfuscate_nsig(n)?;
|
||||||
|
@ -318,192 +157,108 @@ fn map_url(
|
||||||
signature_cipher: &Option<String>,
|
signature_cipher: &Option<String>,
|
||||||
deobf: &Deobfuscator,
|
deobf: &Deobfuscator,
|
||||||
last_nsig: &mut [String; 2],
|
last_nsig: &mut [String; 2],
|
||||||
) -> MapResult<Option<(String, bool)>> {
|
) -> Option<(String, bool)> {
|
||||||
let (url_base, mut url_params) = match url {
|
let (url_base, mut url_params) = match url {
|
||||||
Some(url) => ok_or_bail!(
|
Some(url) => ok_or_bail!(util::url_to_params(url), None),
|
||||||
util::url_to_params(url),
|
|
||||||
MapResult {
|
|
||||||
c: None,
|
|
||||||
warnings: vec![format!("Could not parse url `{}`", url)]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
None => match signature_cipher {
|
None => match signature_cipher {
|
||||||
Some(signature_cipher) => match cipher_to_url_params(signature_cipher, deobf) {
|
Some(signature_cipher) => match cipher_to_url_params(signature_cipher, deobf) {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return MapResult {
|
error!("Could not deobfuscate signatureCipher: {}", e);
|
||||||
c: None,
|
return None;
|
||||||
warnings: vec![format!(
|
|
||||||
"Could not deobfuscate signatureCipher `{}`: {}",
|
|
||||||
signature_cipher, e
|
|
||||||
)],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => {
|
None => return None,
|
||||||
return MapResult {
|
|
||||||
c: None,
|
|
||||||
warnings: vec!["stream contained neither url nor cipher".to_owned()],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut warnings = vec![];
|
|
||||||
let mut throttled = false;
|
let mut throttled = false;
|
||||||
deobf_nsig(&mut url_params, deobf, last_nsig).unwrap_or_else(|e| {
|
deobf_nsig(&mut url_params, deobf, last_nsig).unwrap_or_else(|e| {
|
||||||
warnings.push(format!(
|
warn!("Could not deobfuscate nsig: {}", e);
|
||||||
"Could not deobfuscate nsig (params: {:?}): {}",
|
|
||||||
url_params, e
|
|
||||||
));
|
|
||||||
throttled = true;
|
throttled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
MapResult {
|
Some((
|
||||||
c: Some((
|
|
||||||
ok_or_bail!(
|
ok_or_bail!(
|
||||||
Url::parse_with_params(url_base.as_str(), url_params.iter()),
|
Url::parse_with_params(url_base.as_str(), url_params.iter()),
|
||||||
MapResult {
|
None
|
||||||
c: None,
|
|
||||||
warnings: vec![format!(
|
|
||||||
"url could not be joined. url: `{}` params: {:?}",
|
|
||||||
url_base, url_params
|
|
||||||
)],
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
.to_string(),
|
.to_string(),
|
||||||
throttled,
|
throttled,
|
||||||
)),
|
))
|
||||||
warnings,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_video_stream(
|
fn map_video_stream(
|
||||||
f: player::Format,
|
f: &player::Format,
|
||||||
deobf: &Deobfuscator,
|
deobf: &Deobfuscator,
|
||||||
last_nsig: &mut [String; 2],
|
last_nsig: &mut [String; 2],
|
||||||
) -> MapResult<Option<VideoStream>> {
|
) -> Option<VideoStream> {
|
||||||
let (mtype, codecs) = some_or_bail!(
|
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
|
||||||
parse_mime(&f.mime_type),
|
let (url, throttled) =
|
||||||
MapResult {
|
some_or_bail!(map_url(&f.url, &f.signature_cipher, deobf, last_nsig), None);
|
||||||
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(VideoStream {
|
||||||
Some((url, throttled)) => MapResult {
|
|
||||||
c: Some(VideoStream {
|
|
||||||
url,
|
url,
|
||||||
itag: f.itag,
|
itag: f.itag,
|
||||||
bitrate: f.bitrate,
|
bitrate: f.bitrate,
|
||||||
average_bitrate: f.average_bitrate.unwrap_or(f.bitrate),
|
average_bitrate: f.average_bitrate,
|
||||||
size: f.content_length,
|
size: f.content_length,
|
||||||
index_range: f.index_range,
|
index_range: f.index_range.clone(),
|
||||||
init_range: f.init_range,
|
init_range: f.init_range.clone(),
|
||||||
// Note that the format has already been verified using
|
width: some_or_bail!(f.width, None),
|
||||||
// is_video(), so these unwraps are safe
|
height: some_or_bail!(f.height, None),
|
||||||
width: f.width.unwrap(),
|
fps: some_or_bail!(f.fps, None),
|
||||||
height: f.height.unwrap(),
|
quality: some_or_bail!(f.quality_label.clone(), None),
|
||||||
fps: f.fps.unwrap(),
|
hdr: f.color_info.clone().unwrap_or_default().primaries
|
||||||
quality: f.quality_label.unwrap(),
|
|
||||||
hdr: f.color_info.unwrap_or_default().primaries
|
|
||||||
== player::Primaries::ColorPrimariesBt2020,
|
== player::Primaries::ColorPrimariesBt2020,
|
||||||
mime: f.mime_type.to_owned(),
|
mime: f.mime_type.to_owned(),
|
||||||
format: some_or_bail!(
|
format: some_or_bail!(get_video_format(mtype), None),
|
||||||
get_video_format(mtype),
|
|
||||||
MapResult {
|
|
||||||
c: None,
|
|
||||||
warnings: vec![format!("invalid video format. itag: {}", f.itag)]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
codec: get_video_codec(codecs),
|
codec: get_video_codec(codecs),
|
||||||
throttled,
|
throttled,
|
||||||
}),
|
})
|
||||||
warnings: map_res.warnings,
|
|
||||||
},
|
|
||||||
None => MapResult {
|
|
||||||
c: None,
|
|
||||||
warnings: map_res.warnings,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn map_audio_stream(
|
fn map_audio_stream(
|
||||||
f: player::Format,
|
f: &player::Format,
|
||||||
deobf: &Deobfuscator,
|
deobf: &Deobfuscator,
|
||||||
last_nsig: &mut [String; 2],
|
last_nsig: &mut [String; 2],
|
||||||
) -> MapResult<Option<AudioStream>> {
|
) -> Option<AudioStream> {
|
||||||
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2})\."#).unwrap());
|
static LANG_PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"^([a-z]{2})\."#).unwrap());
|
||||||
|
|
||||||
let (mtype, codecs) = some_or_bail!(
|
let (mtype, codecs) = some_or_bail!(parse_mime(&f.mime_type), None);
|
||||||
parse_mime(&f.mime_type),
|
let (url, throttled) =
|
||||||
MapResult {
|
some_or_bail!(map_url(&f.url, &f.signature_cipher, deobf, last_nsig), None);
|
||||||
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(AudioStream {
|
||||||
Some((url, throttled)) => MapResult {
|
|
||||||
c: Some(AudioStream {
|
|
||||||
url,
|
url,
|
||||||
itag: f.itag,
|
itag: f.itag,
|
||||||
bitrate: f.bitrate,
|
bitrate: f.bitrate,
|
||||||
average_bitrate: f.average_bitrate.unwrap_or(f.bitrate),
|
average_bitrate: f.average_bitrate,
|
||||||
size: f.content_length.unwrap(),
|
size: f.content_length,
|
||||||
index_range: f.index_range,
|
index_range: f.index_range.to_owned(),
|
||||||
init_range: f.init_range,
|
init_range: f.init_range.to_owned(),
|
||||||
mime: f.mime_type.to_owned(),
|
mime: f.mime_type.to_owned(),
|
||||||
format: some_or_bail!(
|
format: some_or_bail!(get_audio_format(mtype), None),
|
||||||
get_audio_format(mtype),
|
|
||||||
MapResult {
|
|
||||||
c: None,
|
|
||||||
warnings: vec![format!("invalid audio format. itag: {}", f.itag)]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
codec: get_audio_codec(codecs),
|
codec: get_audio_codec(codecs),
|
||||||
throttled,
|
throttled,
|
||||||
track: match f.audio_track {
|
track: f.audio_track.as_ref().map(|t| AudioTrack {
|
||||||
Some(t) => {
|
id: t.id.to_owned(),
|
||||||
let lang = LANG_PATTERN
|
lang: LANG_PATTERN
|
||||||
.captures(&t.id)
|
.captures(&t.id)
|
||||||
.ok()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.map(|m| m.get(1).unwrap().as_str().to_owned());
|
.map(|m| m.get(1).unwrap().as_str().to_owned()),
|
||||||
|
lang_name: t.display_name.to_owned(),
|
||||||
Some(AudioTrack {
|
|
||||||
id: t.id,
|
|
||||||
lang,
|
|
||||||
lang_name: t.display_name,
|
|
||||||
is_default: t.audio_is_default,
|
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>)> {
|
fn parse_mime(mime: &str) -> Option<(&str, Vec<&str>)> {
|
||||||
static PATTERN: Lazy<Regex> =
|
static PATTERN: Lazy<Regex> =
|
||||||
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
|
Lazy::new(|| Regex::new(r#"(\w+/\w+);\scodecs="([a-zA-Z-0-9.,\s]*)""#).unwrap());
|
||||||
|
|
||||||
let captures = some_or_bail!(PATTERN.captures(mime).ok().flatten(), None);
|
let captures = some_or_bail!(PATTERN.captures(&mime).ok().flatten(), None);
|
||||||
Some((
|
Some((
|
||||||
captures.get(1).unwrap().as_str(),
|
captures.get(1).unwrap().as_str(),
|
||||||
captures
|
captures
|
||||||
|
@ -558,16 +313,140 @@ fn get_audio_codec(codecs: Vec<&str>) -> AudioCodec {
|
||||||
AudioCodec::Unknown
|
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(test)]
|
||||||
#[cfg(feature = "yaml")]
|
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::{fs::File, io::BufReader, path::Path};
|
use std::{fs::File, io::BufReader, path::Path};
|
||||||
|
|
||||||
use crate::{
|
use crate::{cache::DeobfData, client::CLIENT_TYPES};
|
||||||
client::{RustyPipe, CLIENT_TYPES},
|
|
||||||
deobfuscate::DeobfData,
|
|
||||||
report::TestFileReporter,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
@ -586,6 +465,8 @@ mod tests {
|
||||||
let tf_dir = Path::new("testfiles/player");
|
let tf_dir = Path::new("testfiles/player");
|
||||||
let video_id = "pPvd8UxmSbQ";
|
let video_id = "pPvd8UxmSbQ";
|
||||||
|
|
||||||
|
let rt = RustyTube::new();
|
||||||
|
|
||||||
for client_type in CLIENT_TYPES {
|
for client_type in CLIENT_TYPES {
|
||||||
let mut json_path = tf_dir.to_path_buf();
|
let mut json_path = tf_dir.to_path_buf();
|
||||||
json_path.push(format!("{:?}_video.json", client_type).to_lowercase());
|
json_path.push(format!("{:?}_video.json", client_type).to_lowercase());
|
||||||
|
@ -593,20 +474,31 @@ mod tests {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let reporter = TestFileReporter::new(json_path);
|
let client = rt.get_ytclient(client_type);
|
||||||
let rp = RustyPipe::new(None, Some(Box::new(reporter)), None);
|
let context = client.get_context(false).await;
|
||||||
rp.test_query()
|
|
||||||
.report(true)
|
let request_body = build_request_body(client.clone(), &DEOBFUSCATOR, context, video_id);
|
||||||
.get_player(video_id, client_type)
|
|
||||||
|
let resp = client
|
||||||
|
.request_builder(Method::POST, "player")
|
||||||
.await
|
.await
|
||||||
|
.json(&request_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.error_for_status()
|
||||||
.unwrap();
|
.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)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn download_model_testfiles() {
|
async fn download_model_testfiles() {
|
||||||
let tf_dir = Path::new("testfiles/player_model");
|
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")] {
|
for (name, id) in [("multilanguage", "tVWWp1PqDus"), ("hdr", "LXb3EKWsInQ")] {
|
||||||
let mut json_path = tf_dir.to_path_buf();
|
let mut json_path = tf_dir.to_path_buf();
|
||||||
|
@ -615,11 +507,7 @@ mod tests {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let player_data = rp
|
let player_data = rt.get_player(id, ClientType::Desktop).await.unwrap();
|
||||||
.test_query()
|
|
||||||
.get_player(id, ClientType::Desktop)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let file = File::create(json_path).unwrap();
|
let file = File::create(json_path).unwrap();
|
||||||
serde_json::to_writer_pretty(file, &player_data).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 json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
let resp: response::Player = serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = resp
|
let player_data = map_player_data(resp, &DEOBFUSCATOR).unwrap();
|
||||||
.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";
|
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| {
|
".info.publish_date" => insta::dynamic_redaction(move |value, _path| {
|
||||||
if is_desktop {
|
if is_desktop {
|
||||||
assert!(value.as_str().unwrap().starts_with("2019-05-30T00:00:00"));
|
assert!(value.as_str().unwrap().starts_with("2019-05-30T00:00:00"));
|
||||||
|
@ -680,12 +561,8 @@ mod tests {
|
||||||
#[case::ios(ClientType::Ios)]
|
#[case::ios(ClientType::Ios)]
|
||||||
#[test_log::test(tokio::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn t_get_player(#[case] client_type: ClientType) {
|
async fn t_get_player(#[case] client_type: ClientType) {
|
||||||
let rp = RustyPipe::new_test();
|
let rt = RustyTube::new();
|
||||||
let player_data = rp
|
let player_data = rt.get_player("n4tK7LYFxI0", client_type).await.unwrap();
|
||||||
.test_query()
|
|
||||||
.get_player("n4tK7LYFxI0", client_type)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// dbg!(&player_data);
|
// dbg!(&player_data);
|
||||||
|
|
||||||
|
@ -707,12 +584,7 @@ mod tests {
|
||||||
assert_eq!(player_data.info.is_live_content, false);
|
assert_eq!(player_data.info.is_live_content, false);
|
||||||
|
|
||||||
if client_type == ClientType::Desktop || client_type == ClientType::DesktopMusic {
|
if client_type == ClientType::Desktop || client_type == ClientType::DesktopMusic {
|
||||||
assert!(player_data
|
assert!(player_data.info.publish_date.unwrap().to_string().starts_with("2013-05-05 00:00:00"));
|
||||||
.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.category.unwrap(), "Music");
|
||||||
assert_eq!(player_data.info.is_family_safe.unwrap(), true);
|
assert_eq!(player_data.info.is_family_safe.unwrap(), true);
|
||||||
}
|
}
|
||||||
|
@ -732,7 +604,7 @@ mod tests {
|
||||||
// Bitrates may change between requests
|
// Bitrates may change between requests
|
||||||
assert_approx(video.bitrate as f64, 1507068.0);
|
assert_approx(video.bitrate as f64, 1507068.0);
|
||||||
assert_eq!(video.average_bitrate, 1345149);
|
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.width, 1280);
|
||||||
assert_eq!(video.height, 720);
|
assert_eq!(video.height, 720);
|
||||||
assert_eq!(video.fps, 30);
|
assert_eq!(video.fps, 30);
|
||||||
|
@ -762,7 +634,7 @@ mod tests {
|
||||||
|
|
||||||
assert_approx(video.bitrate as f64, 1340829.0);
|
assert_approx(video.bitrate as f64, 1340829.0);
|
||||||
assert_approx(video.average_bitrate as f64, 1233444.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.width, 1280);
|
||||||
assert_eq!(video.height, 720);
|
assert_eq!(video.height, 720);
|
||||||
assert_eq!(video.fps, 30);
|
assert_eq!(video.fps, 30);
|
||||||
|
@ -789,20 +661,15 @@ mod tests {
|
||||||
fn t_cipher_to_url() {
|
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 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 mut last_nsig: [String; 2] = ["".to_owned(), "".to_owned()];
|
||||||
let map_res = map_url(
|
let (url, throttled) = map_url(
|
||||||
&None,
|
&None,
|
||||||
&Some(signature_cipher.to_owned()),
|
&Some(signature_cipher.to_owned()),
|
||||||
&DEOBFUSCATOR,
|
&DEOBFUSCATOR,
|
||||||
&mut last_nsig,
|
&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!(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_eq!(throttled, false);
|
||||||
assert!(
|
|
||||||
map_res.warnings.is_empty(),
|
|
||||||
"deserialization/mapping warnings: {:?}",
|
|
||||||
map_res.warnings
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
deobfuscate::Deobfuscator,
|
|
||||||
model::{Channel, Language, Playlist, Thumbnail, Video},
|
model::{Channel, Language, Playlist, Thumbnail, Video},
|
||||||
serializer::text::{PageType, TextLink},
|
serializer::text::{PageType, TextLink},
|
||||||
timeago, util,
|
timeago, util,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{response, ClientType, ContextYT, MapResponse, MapResult, RustyPipeQuery};
|
use super::{response, ClientType, ContextYT, RustyTube};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -25,44 +24,62 @@ struct QPlaylistCont {
|
||||||
continuation: String,
|
continuation: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RustyPipeQuery {
|
impl RustyTube {
|
||||||
pub async fn get_playlist(self, playlist_id: &str) -> Result<Playlist> {
|
pub async fn get_playlist(&self, playlist_id: &str) -> Result<Playlist> {
|
||||||
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 = QPlaylist {
|
let request_body = QPlaylist {
|
||||||
context,
|
context,
|
||||||
browse_id: "VL".to_owned() + playlist_id,
|
browse_id: "VL".to_owned() + playlist_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.execute_request::<response::Playlist, _, _>(
|
let resp = client
|
||||||
ClientType::Desktop,
|
.request_builder(Method::POST, "browse")
|
||||||
"get_playlist",
|
|
||||||
Method::POST,
|
|
||||||
"browse",
|
|
||||||
playlist_id,
|
|
||||||
&request_body,
|
|
||||||
)
|
|
||||||
.await
|
.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 {
|
match &playlist.ctoken {
|
||||||
Some(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 {
|
let request_body = QPlaylistCont {
|
||||||
context,
|
context,
|
||||||
continuation: ctoken.to_owned(),
|
continuation: ctoken.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut videos, ctoken) = self
|
let resp = client
|
||||||
.execute_request::<response::PlaylistCont, _, _>(
|
.request_builder(Method::POST, "browse")
|
||||||
ClientType::Desktop,
|
.await
|
||||||
"get_playlist_cont",
|
.json(&request_body)
|
||||||
Method::POST,
|
.send()
|
||||||
"browse",
|
.await?
|
||||||
&playlist.id,
|
.error_for_status()?;
|
||||||
&request_body,
|
|
||||||
)
|
let cont_response = resp.json::<response::playlist::PlaylistCont>().await?;
|
||||||
.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.videos.append(&mut videos);
|
||||||
playlist.ctoken = ctoken;
|
playlist.ctoken = ctoken;
|
||||||
|
@ -78,17 +95,12 @@ impl RustyPipeQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MapResponse<Playlist> for response::Playlist {
|
fn map_playlist(response: &response::Playlist, lang: Language) -> Result<Playlist> {
|
||||||
fn map_response(
|
|
||||||
self,
|
|
||||||
id: &str,
|
|
||||||
lang: Language,
|
|
||||||
_deobf: Option<&Deobfuscator>,
|
|
||||||
) -> Result<MapResult<Playlist>> {
|
|
||||||
let video_items = &some_or_bail!(
|
let video_items = &some_or_bail!(
|
||||||
some_or_bail!(
|
some_or_bail!(
|
||||||
some_or_bail!(
|
some_or_bail!(
|
||||||
self.contents
|
response
|
||||||
|
.contents
|
||||||
.two_column_browse_results_renderer
|
.two_column_browse_results_renderer
|
||||||
.contents
|
.contents
|
||||||
.get(0),
|
.get(0),
|
||||||
|
@ -109,9 +121,9 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
.playlist_video_list_renderer
|
.playlist_video_list_renderer
|
||||||
.contents;
|
.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 {
|
let (thumbnails, last_update_txt) = match &response.sidebar {
|
||||||
Some(sidebar) => {
|
Some(sidebar) => {
|
||||||
let primary = some_or_bail!(
|
let primary = some_or_bail!(
|
||||||
sidebar.playlist_sidebar_renderer.items.get(0),
|
sidebar.playlist_sidebar_renderer.items.get(0),
|
||||||
|
@ -134,11 +146,14 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let header_banner = some_or_bail!(
|
let header_banner = some_or_bail!(
|
||||||
&self.header.playlist_header_renderer.playlist_header_banner,
|
&response
|
||||||
|
.header
|
||||||
|
.playlist_header_renderer
|
||||||
|
.playlist_header_banner,
|
||||||
Err(anyhow!("no thumbnail found"))
|
Err(anyhow!("no thumbnail found"))
|
||||||
);
|
);
|
||||||
|
|
||||||
let last_update_txt = self
|
let last_update_txt = response
|
||||||
.header
|
.header
|
||||||
.playlist_header_renderer
|
.playlist_header_renderer
|
||||||
.byline
|
.byline
|
||||||
|
@ -167,48 +182,45 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
let n_videos = match ctoken {
|
let n_videos = match ctoken {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
ok_or_bail!(
|
ok_or_bail!(
|
||||||
util::parse_numeric(&self.header.playlist_header_renderer.num_videos_text),
|
util::parse_numeric(&response.header.playlist_header_renderer.num_videos_text),
|
||||||
Err(anyhow!("no video count"))
|
Err(anyhow!("no video count"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
None => videos.len() as u32,
|
None => videos.len() as u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
let playlist_id = self.header.playlist_header_renderer.playlist_id;
|
let id = response
|
||||||
if playlist_id != id {
|
.header
|
||||||
bail!("got wrong playlist id {}, expected {}", playlist_id, id);
|
.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 name = self.header.playlist_header_renderer.title;
|
let channel = match &response.header.playlist_header_renderer.owner_text {
|
||||||
let description = self.header.playlist_header_renderer.description_text;
|
Some(owner_text) => match owner_text {
|
||||||
|
TextLink::Browse {
|
||||||
let channel = match self.header.playlist_header_renderer.owner_text {
|
|
||||||
Some(TextLink::Browse {
|
|
||||||
text,
|
text,
|
||||||
page_type: PageType::Channel,
|
page_type,
|
||||||
browse_id,
|
browse_id,
|
||||||
}) => Some(Channel {
|
} => match page_type {
|
||||||
id: browse_id,
|
PageType::Channel => Some(Channel {
|
||||||
name: text,
|
id: browse_id.to_owned(),
|
||||||
|
name: text.to_owned(),
|
||||||
}),
|
}),
|
||||||
_ => 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,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(MapResult {
|
Ok(Playlist {
|
||||||
c: Playlist {
|
id,
|
||||||
id: playlist_id,
|
|
||||||
name,
|
name,
|
||||||
videos,
|
videos,
|
||||||
n_videos,
|
n_videos,
|
||||||
|
@ -216,41 +228,16 @@ impl MapResponse<Playlist> for response::Playlist {
|
||||||
thumbnails,
|
thumbnails,
|
||||||
description,
|
description,
|
||||||
channel,
|
channel,
|
||||||
last_update,
|
last_update: match &last_update_txt {
|
||||||
last_update_txt,
|
Some(textual_date) => timeago::parse_textual_date_to_dt(lang, textual_date),
|
||||||
|
None => None,
|
||||||
},
|
},
|
||||||
warnings,
|
last_update_txt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
fn map_playlist_items(
|
||||||
items: &[response::VideoListItem<response::playlist::PlaylistVideo>],
|
items: &Vec<response::VideoListItem<response::playlist::PlaylistVideo>>,
|
||||||
) -> (Vec<Video>, Option<String>) {
|
) -> (Vec<Video>, Option<String>) {
|
||||||
let mut ctoken: Option<String> = None;
|
let mut ctoken: Option<String> = None;
|
||||||
let videos = items
|
let videos = items
|
||||||
|
@ -259,9 +246,10 @@ fn map_playlist_items(
|
||||||
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
|
response::VideoListItem::GridVideoRenderer { video } => match &video.channel {
|
||||||
TextLink::Browse {
|
TextLink::Browse {
|
||||||
text,
|
text,
|
||||||
page_type: PageType::Channel,
|
page_type,
|
||||||
browse_id,
|
browse_id,
|
||||||
} => Some(Video {
|
} => match page_type {
|
||||||
|
PageType::Channel => Some(Video {
|
||||||
id: video.video_id.to_owned(),
|
id: video.video_id.to_owned(),
|
||||||
title: video.title.to_owned(),
|
title: video.title.to_owned(),
|
||||||
length: video.length_seconds,
|
length: video.length_seconds,
|
||||||
|
@ -282,6 +270,8 @@ fn map_playlist_items(
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
response::VideoListItem::ContinuationItemRenderer {
|
response::VideoListItem::ContinuationItemRenderer {
|
||||||
continuation_endpoint,
|
continuation_endpoint,
|
||||||
} => {
|
} => {
|
||||||
|
@ -299,10 +289,49 @@ mod tests {
|
||||||
|
|
||||||
use rstest::rstest;
|
use rstest::rstest;
|
||||||
|
|
||||||
use crate::{client::RustyPipe, report::TestFileReporter};
|
|
||||||
|
|
||||||
use super::*;
|
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]
|
#[rstest]
|
||||||
#[case::long(
|
#[case::long(
|
||||||
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
|
"PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ",
|
||||||
|
@ -339,8 +368,8 @@ mod tests {
|
||||||
#[case] description: Option<String>,
|
#[case] description: Option<String>,
|
||||||
#[case] channel: Option<Channel>,
|
#[case] channel: Option<Channel>,
|
||||||
) {
|
) {
|
||||||
let rp = RustyPipe::new_test();
|
let rt = RustyTube::new();
|
||||||
let playlist = rp.test_query().get_playlist(id).await.unwrap();
|
let playlist = rt.get_playlist(id).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(playlist.id, id);
|
assert_eq!(playlist.id, id);
|
||||||
assert_eq!(playlist.name, name);
|
assert_eq!(playlist.name, name);
|
||||||
|
@ -349,70 +378,37 @@ mod tests {
|
||||||
assert!(playlist.n_videos > 10);
|
assert!(playlist.n_videos > 10);
|
||||||
assert_eq!(playlist.n_videos > 100, is_long);
|
assert_eq!(playlist.n_videos > 100, is_long);
|
||||||
assert_eq!(playlist.description, description);
|
assert_eq!(playlist.description, description);
|
||||||
if channel.is_some() {
|
|
||||||
assert_eq!(playlist.channel, channel);
|
assert_eq!(playlist.channel, channel);
|
||||||
}
|
|
||||||
assert!(!playlist.thumbnails.is_empty());
|
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]
|
#[rstest]
|
||||||
#[case::short("short", "RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk")]
|
#[case::long("long")]
|
||||||
#[case::long("long", "PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ")]
|
#[case::short("short")]
|
||||||
#[case::nomusic("nomusic", "PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe")]
|
#[case::nomusic("nomusic")]
|
||||||
fn t_map_playlist_data(#[case] name: &str, #[case] id: &str) {
|
fn t_map_playlist_data(#[case] name: &str) {
|
||||||
let filename = format!("testfiles/playlist/playlist_{}.json", name);
|
let filename = format!("testfiles/playlist/playlist_{}.json", name);
|
||||||
let json_path = Path::new(&filename);
|
let json_path = Path::new(&filename);
|
||||||
let json_file = File::open(json_path).unwrap();
|
let json_file = File::open(json_path).unwrap();
|
||||||
|
|
||||||
let playlist: response::Playlist =
|
let playlist: response::Playlist =
|
||||||
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
serde_json::from_reader(BufReader::new(json_file)).unwrap();
|
||||||
let map_res = playlist.map_response(id, Language::En, None).unwrap();
|
let playlist_data = map_playlist(&playlist, Language::En).unwrap();
|
||||||
|
insta::assert_yaml_snapshot!(format!("map_playlist_data_{}", name), playlist_data, {
|
||||||
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]"
|
".last_update" => "[date]"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test_log::test(tokio::test)]
|
#[test_log::test(tokio::test)]
|
||||||
async fn t_playlist_cont() {
|
async fn t_playlist_cont() {
|
||||||
let rp = RustyPipe::new_test();
|
let rt = RustyTube::new();
|
||||||
let mut playlist = rp
|
let mut playlist = rt
|
||||||
.test_query()
|
|
||||||
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
.get_playlist("PLbZIPy20-1pN7mqjckepWF78ndb6ci_qi")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
while playlist.ctoken.is_some() {
|
while playlist.ctoken.is_some() {
|
||||||
rp.test_query()
|
rt.get_playlist_cont(&mut playlist).await.unwrap();
|
||||||
.get_playlist_cont(&mut playlist)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(playlist.videos.len() > 100);
|
assert!(playlist.videos.len() > 100);
|
||||||
|
|
|
@ -4,7 +4,6 @@ use serde_with::VecSkipError;
|
||||||
|
|
||||||
use super::TimeOverlay;
|
use super::TimeOverlay;
|
||||||
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
|
use super::{ContentRenderer, ContentsRenderer, Thumbnails, VideoListItem};
|
||||||
use crate::serializer::text::Text;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -64,11 +63,11 @@ pub struct GridRenderer {
|
||||||
pub struct ChannelVideo {
|
pub struct ChannelVideo {
|
||||||
pub video_id: String,
|
pub video_id: String,
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde_as(as = "Option<Text>")]
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
pub published_time_text: Option<String>,
|
pub published_time_text: Option<String>,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub view_count_text: String,
|
pub view_count_text: String,
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub thumbnail_overlays: Vec<TimeOverlay>,
|
pub thumbnail_overlays: Vec<TimeOverlay>,
|
||||||
|
|
|
@ -7,7 +7,6 @@ pub mod video;
|
||||||
pub use channel::Channel;
|
pub use channel::Channel;
|
||||||
pub use player::Player;
|
pub use player::Player;
|
||||||
pub use playlist::Playlist;
|
pub use playlist::Playlist;
|
||||||
pub use playlist::PlaylistCont;
|
|
||||||
pub use playlist_music::PlaylistMusic;
|
pub use playlist_music::PlaylistMusic;
|
||||||
pub use video::Video;
|
pub use video::Video;
|
||||||
pub use video::VideoComments;
|
pub use video::VideoComments;
|
||||||
|
@ -16,7 +15,7 @@ pub use video::VideoRecommendations;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
use serde_with::{serde_as, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::serializer::text::{Text, TextLink, TextLinks};
|
use crate::serializer::text::TextLink;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -94,10 +93,10 @@ pub struct VideoOwner {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VideoOwnerRenderer {
|
pub struct VideoOwnerRenderer {
|
||||||
#[serde_as(as = "TextLink")]
|
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||||
pub title: TextLink,
|
pub title: TextLink,
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
#[serde_as(as = "Option<Text>")]
|
#[serde_as(as = "Option<crate::serializer::text::Text>")]
|
||||||
pub subscriber_count_text: Option<String>,
|
pub subscriber_count_text: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecSkipError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
|
@ -133,7 +132,7 @@ pub struct TimeOverlay {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TimeOverlayRenderer {
|
pub struct TimeOverlayRenderer {
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
|
@ -199,7 +198,7 @@ pub struct MusicColumn {
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
pub struct MusicColumnRenderer {
|
pub struct MusicColumnRenderer {
|
||||||
#[serde_as(as = "TextLinks")]
|
#[serde_as(as = "crate::serializer::text::TextLinks")]
|
||||||
pub text: Vec<TextLink>,
|
pub text: Vec<TextLink>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,23 +213,3 @@ pub struct MusicContinuation {
|
||||||
pub struct MusicContinuationData {
|
pub struct MusicContinuationData {
|
||||||
pub continuation: String,
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,10 +3,9 @@ use std::ops::Range;
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{json::JsonString, DefaultOnError};
|
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use super::Thumbnails;
|
use super::Thumbnails;
|
||||||
use crate::serializer::{text::Text, MapResult, VecLogError};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -46,11 +45,11 @@ pub struct StreamingData {
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "JsonString")]
|
||||||
pub expires_in_seconds: u32,
|
pub expires_in_seconds: u32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub formats: MapResult<Vec<Format>>,
|
pub formats: Vec<Format>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub adaptive_formats: MapResult<Vec<Format>>,
|
pub adaptive_formats: Vec<Format>,
|
||||||
/// Only on livestreams
|
/// Only on livestreams
|
||||||
pub dash_manifest_url: Option<String>,
|
pub dash_manifest_url: Option<String>,
|
||||||
/// Only on livestreams
|
/// Only on livestreams
|
||||||
|
@ -74,20 +73,20 @@ pub struct Format {
|
||||||
pub width: Option<u32>,
|
pub width: Option<u32>,
|
||||||
pub height: 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>>,
|
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>>,
|
pub init_range: Option<Range<u32>>,
|
||||||
|
|
||||||
#[serde_as(as = "Option<JsonString>")]
|
#[serde_as(as = "JsonString")]
|
||||||
pub content_length: Option<u64>,
|
pub content_length: u64,
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(deserialize_as = "DefaultOnError")]
|
#[serde_as(deserialize_as = "DefaultOnError")]
|
||||||
pub quality: Option<Quality>,
|
pub quality: Option<Quality>,
|
||||||
pub fps: Option<u8>,
|
pub fps: Option<u8>,
|
||||||
pub quality_label: Option<String>,
|
pub quality_label: Option<String>,
|
||||||
pub average_bitrate: Option<u32>,
|
pub average_bitrate: u32,
|
||||||
pub color_info: Option<ColorInfo>,
|
pub color_info: Option<ColorInfo>,
|
||||||
|
|
||||||
// Audio only
|
// Audio only
|
||||||
|
@ -105,9 +104,7 @@ pub struct Format {
|
||||||
|
|
||||||
impl Format {
|
impl Format {
|
||||||
pub fn is_audio(&self) -> bool {
|
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 {
|
pub fn is_video(&self) -> bool {
|
||||||
|
@ -191,7 +188,7 @@ pub struct PlayerCaptionsTracklistRenderer {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CaptionTrack {
|
pub struct CaptionTrack {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub language_code: String,
|
pub language_code: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,7 @@ use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
use serde_with::{json::JsonString, DefaultOnError, VecSkipError};
|
||||||
|
|
||||||
use crate::serializer::text::{Text, TextLink};
|
use crate::serializer::text::TextLink;
|
||||||
use crate::serializer::{MapResult, VecLogError};
|
|
||||||
|
|
||||||
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
|
use super::{ContentRenderer, ContentsRenderer, Thumbnails, ThumbnailsWrap, VideoListItem};
|
||||||
|
|
||||||
|
@ -57,8 +56,8 @@ pub struct PlaylistVideoListRenderer {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct PlaylistVideoList {
|
pub struct PlaylistVideoList {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub contents: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
|
pub contents: Vec<VideoListItem<PlaylistVideo>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -67,10 +66,10 @@ pub struct PlaylistVideoList {
|
||||||
pub struct PlaylistVideo {
|
pub struct PlaylistVideo {
|
||||||
pub video_id: String,
|
pub video_id: String,
|
||||||
pub thumbnail: Thumbnails,
|
pub thumbnail: Thumbnails,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(rename = "shortBylineText")]
|
#[serde(rename = "shortBylineText")]
|
||||||
#[serde_as(as = "TextLink")]
|
#[serde_as(as = "crate::serializer::text::TextLink")]
|
||||||
pub channel: TextLink,
|
pub channel: TextLink,
|
||||||
#[serde_as(as = "JsonString")]
|
#[serde_as(as = "JsonString")]
|
||||||
pub length_seconds: u32,
|
pub length_seconds: u32,
|
||||||
|
@ -87,14 +86,14 @@ pub struct Header {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct HeaderRenderer {
|
pub struct HeaderRenderer {
|
||||||
pub playlist_id: String,
|
pub playlist_id: String,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[serde_as(as = "DefaultOnError<Option<Text>>")]
|
#[serde_as(as = "DefaultOnError<Option<crate::serializer::text::Text>>")]
|
||||||
pub description_text: Option<String>,
|
pub description_text: Option<String>,
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub num_videos_text: String,
|
pub num_videos_text: String,
|
||||||
#[serde_as(as = "Option<TextLink>")]
|
#[serde_as(as = "Option<crate::serializer::text::TextLink>")]
|
||||||
pub owner_text: Option<TextLink>,
|
pub owner_text: Option<TextLink>,
|
||||||
|
|
||||||
// Alternative layout
|
// Alternative layout
|
||||||
|
@ -119,7 +118,7 @@ pub struct Byline {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BylineRenderer {
|
pub struct BylineRenderer {
|
||||||
#[serde_as(as = "Text")]
|
#[serde_as(as = "crate::serializer::text::Text")]
|
||||||
pub text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +150,7 @@ pub struct SidebarPrimaryInfoRenderer {
|
||||||
// - `"495", " videos"`
|
// - `"495", " videos"`
|
||||||
// - `"3,310,996 views"`
|
// - `"3,310,996 views"`
|
||||||
// - `"Last updated on ", "Aug 7, 2022"`
|
// - `"Last updated on ", "Aug 7, 2022"`
|
||||||
#[serde_as(as = "Vec<Text>")]
|
#[serde_as(as = "Vec<crate::serializer::text::Text>")]
|
||||||
pub stats: Vec<String>,
|
pub stats: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +172,7 @@ pub struct OnResponseReceivedAction {
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AppendAction {
|
pub struct AppendAction {
|
||||||
#[serde_as(as = "VecLogError<_>")]
|
#[serde_as(as = "VecSkipError<_>")]
|
||||||
pub continuation_items: MapResult<Vec<VideoListItem<PlaylistVideo>>>,
|
pub continuation_items: Vec<VideoListItem<PlaylistVideo>>,
|
||||||
pub target_id: String,
|
pub target_id: String,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
#![allow(clippy::enum_variant_names)]
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
use serde_with::{DefaultOnError, VecSkipError};
|
use serde_with::{DefaultOnError, VecSkipError};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/player.rs
|
source: src/client/player.rs
|
||||||
expression: map_res.c
|
expression: player_data
|
||||||
---
|
---
|
||||||
info:
|
info:
|
||||||
id: pPvd8UxmSbQ
|
id: pPvd8UxmSbQ
|
||||||
|
@ -184,22 +184,6 @@ video_only_streams:
|
||||||
format: mp4
|
format: mp4
|
||||||
codec: av01
|
codec: av01
|
||||||
throttled: false
|
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"
|
- 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
|
itag: 398
|
||||||
bitrate: 1348419
|
bitrate: 1348419
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/player.rs
|
source: src/client/player.rs
|
||||||
expression: map_res.c
|
expression: player_data
|
||||||
---
|
---
|
||||||
info:
|
info:
|
||||||
id: pPvd8UxmSbQ
|
id: pPvd8UxmSbQ
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/player.rs
|
source: src/client/player.rs
|
||||||
expression: map_res.c
|
expression: player_data
|
||||||
---
|
---
|
||||||
info:
|
info:
|
||||||
id: pPvd8UxmSbQ
|
id: pPvd8UxmSbQ
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/player.rs
|
source: src/client/player.rs
|
||||||
expression: map_res.c
|
expression: player_data
|
||||||
---
|
---
|
||||||
info:
|
info:
|
||||||
id: pPvd8UxmSbQ
|
id: pPvd8UxmSbQ
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/player.rs
|
source: src/client/player.rs
|
||||||
expression: map_res.c
|
expression: player_data
|
||||||
---
|
---
|
||||||
info:
|
info:
|
||||||
id: pPvd8UxmSbQ
|
id: pPvd8UxmSbQ
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: map_res.c
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
id: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
id: PL5dDx681T4bR7ZF1IuWzOv1omlRbE7PiJ
|
||||||
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
name: Die schönsten deutschen Lieder | Beliebteste Lieder | Beste Deutsche Musik 2022
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: map_res.c
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
id: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
id: PL1J-6JOckZtE_P9Xx8D3b2O6w0idhuKBe
|
||||||
name: Minecraft SHINE
|
name: Minecraft SHINE
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
source: src/client/playlist.rs
|
source: src/client/playlist.rs
|
||||||
expression: map_res.c
|
expression: playlist_data
|
||||||
---
|
---
|
||||||
id: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk
|
id: RDCLAK5uy_kFQXdnqMaQCVx2wpUM4ZfbsGCDibZtkJk
|
||||||
name: Easy Pop
|
name: Easy Pop
|
||||||
|
|
111
src/client/video.rs
Normal file
111
src/client/video.rs
Normal 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
534
src/client2/mod.rs
Normal 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
807
src/client2/player.rs
Normal 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
427
src/client2/playlist.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
74
src/client2/response/channel.rs
Normal file
74
src/client2/response/channel.rs
Normal 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
236
src/client2/response/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
231
src/client2/response/player.rs
Normal file
231
src/client2/response/player.rs
Normal 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>>,
|
||||||
|
}
|
179
src/client2/response/playlist.rs
Normal file
179
src/client2/response/playlist.rs
Normal 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,
|
||||||
|
}
|
95
src/client2/response/playlist_music.rs
Normal file
95
src/client2/response/playlist_music.rs
Normal 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>,
|
||||||
|
}
|
432
src/client2/response/video.rs
Normal file
432
src/client2/response/video.rs
Normal 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,
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -3,25 +3,19 @@ use fancy_regex::Regex;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::result::Result::Ok;
|
use std::result::Result::Ok;
|
||||||
|
|
||||||
|
use crate::cache::{Cache, DeobfData};
|
||||||
use crate::util;
|
use crate::util;
|
||||||
|
|
||||||
pub struct Deobfuscator {
|
pub struct Deobfuscator {
|
||||||
data: DeobfData,
|
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 {
|
impl Deobfuscator {
|
||||||
pub async fn new(http: Client) -> Result<Self> {
|
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)
|
let js_url = get_player_js_url(&http)
|
||||||
.await
|
.await
|
||||||
.context("Failed to retrieve player.js URL")?;
|
.context("Failed to retrieve player.js URL")?;
|
||||||
|
@ -36,14 +30,16 @@ impl Deobfuscator {
|
||||||
let nsig_fn = get_nsig_fn(&player_js)?;
|
let nsig_fn = get_nsig_fn(&player_js)?;
|
||||||
let sts = get_sts(&player_js)?;
|
let sts = get_sts(&player_js)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(DeobfData {
|
||||||
data: DeobfData {
|
|
||||||
js_url,
|
js_url,
|
||||||
nsig_fn,
|
nsig_fn,
|
||||||
sig_fn,
|
sig_fn,
|
||||||
sts,
|
sts,
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Self { data })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
|
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String> {
|
||||||
|
@ -57,10 +53,6 @@ impl Deobfuscator {
|
||||||
pub fn get_sts(&self) -> String {
|
pub fn get_sts(&self) -> String {
|
||||||
self.data.sts.to_owned()
|
self.data.sts.to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_data(&self) -> DeobfData {
|
|
||||||
self.data.to_owned()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DeobfData> for Deobfuscator {
|
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 {
|
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> {
|
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());
|
Lazy::new(|| Regex::new("signatureTimestamp[=:](\\d+)").unwrap());
|
||||||
|
|
||||||
Ok(some_or_bail!(
|
Ok(some_or_bail!(
|
||||||
STS_PATTERN.captures(player_js)?,
|
STS_PATTERN.captures(&player_js)?,
|
||||||
Err(anyhow!("could not find sts"))
|
Err(anyhow!("could not find sts"))
|
||||||
)
|
)
|
||||||
.get(1)
|
.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)]
|
#[test(tokio::test)]
|
||||||
async fn t_update() {
|
async fn t_update() {
|
||||||
let client = Client::new();
|
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();
|
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
||||||
println!("{}", deobf_sig);
|
println!("{}", deobf_sig);
|
||||||
|
|
|
@ -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 chunk_size = rng.gen_range(CHUNK_SIZE_MIN..CHUNK_SIZE_MAX);
|
||||||
let mut chunk_end = offset + chunk_size;
|
let mut chunk_end = offset + chunk_size;
|
||||||
|
|
||||||
if let Some(size) = size {
|
if size.is_some() {
|
||||||
chunk_end = chunk_end.min(size - 1)
|
chunk_end = chunk_end.min(size.unwrap() - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Range {
|
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());
|
static PATTERN: Lazy<Regex> = Lazy::new(|| Regex::new(r#"bytes (\d+)-(\d+)/(\d+)"#).unwrap());
|
||||||
|
|
||||||
let captures = some_or_bail!(
|
let captures = some_or_bail!(
|
||||||
PATTERN.captures(cr_header).ok().flatten(),
|
PATTERN.captures(&cr_header).ok().flatten(),
|
||||||
Err(anyhow!(
|
Err(anyhow!(
|
||||||
"Content-Range header '{}' does not match pattern.",
|
"Content-Range header '{}' does not match pattern.",
|
||||||
cr_header
|
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 (url_base, url_params) = util::url_to_params(url)?;
|
||||||
let is_gvideo = url_base.ends_with(".googlevideo.com/videoplayback");
|
let is_gvideo = url_base.ends_with(".googlevideo.com/videoplayback");
|
||||||
if is_gvideo {
|
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
|
// Check if file is partially downloaded
|
||||||
|
@ -254,7 +257,6 @@ struct StreamDownload {
|
||||||
video_codec: Option<VideoCodec>,
|
video_codec: Option<VideoCodec>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub async fn download_video(
|
pub async fn download_video(
|
||||||
player_data: &VideoPlayer,
|
player_data: &VideoPlayer,
|
||||||
output_dir: &str,
|
output_dir: &str,
|
||||||
|
@ -325,7 +327,7 @@ pub async fn download_video(
|
||||||
_ => {
|
_ => {
|
||||||
let mut downloads: Vec<StreamDownload> = Vec::new();
|
let mut downloads: Vec<StreamDownload> = Vec::new();
|
||||||
|
|
||||||
if let Some(v) = video {
|
video.map(|v| {
|
||||||
downloads.push(StreamDownload {
|
downloads.push(StreamDownload {
|
||||||
file: download_dir.join(format!(
|
file: download_dir.join(format!(
|
||||||
"{}.video{}",
|
"{}.video{}",
|
||||||
|
@ -336,8 +338,8 @@ pub async fn download_video(
|
||||||
video_codec: Some(v.codec),
|
video_codec: Some(v.codec),
|
||||||
audio_codec: None,
|
audio_codec: None,
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
if let Some(a) = audio {
|
audio.map(|a| {
|
||||||
downloads.push(StreamDownload {
|
downloads.push(StreamDownload {
|
||||||
file: download_dir.join(format!(
|
file: download_dir.join(format!(
|
||||||
"{}.audio{}",
|
"{}.audio{}",
|
||||||
|
@ -348,7 +350,7 @@ pub async fn download_video(
|
||||||
video_codec: None,
|
video_codec: None,
|
||||||
audio_codec: Some(a.codec),
|
audio_codec: Some(a.codec),
|
||||||
})
|
})
|
||||||
}
|
});
|
||||||
|
|
||||||
pb.set_message(format!("Downloading {}", title));
|
pb.set_message(format!("Downloading {}", title));
|
||||||
download_streams(&downloads, http, pb.clone()).await?;
|
download_streams(&downloads, http, pb.clone()).await?;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
#![warn(clippy::todo)]
|
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod macros;
|
mod macros;
|
||||||
|
@ -15,7 +14,7 @@ mod util;
|
||||||
|
|
||||||
// pub mod client;
|
// pub mod client;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod client;
|
pub mod client2;
|
||||||
pub mod download;
|
pub mod download;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod report;
|
pub mod report;
|
||||||
|
|
|
@ -59,7 +59,7 @@ pub struct VideoStream {
|
||||||
pub itag: u32,
|
pub itag: u32,
|
||||||
pub bitrate: u32,
|
pub bitrate: u32,
|
||||||
pub average_bitrate: u32,
|
pub average_bitrate: u32,
|
||||||
pub size: Option<u64>,
|
pub size: u64,
|
||||||
pub index_range: Option<Range<u32>>,
|
pub index_range: Option<Range<u32>>,
|
||||||
pub init_range: Option<Range<u32>>,
|
pub init_range: Option<Range<u32>>,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
|
|
|
@ -9,8 +9,6 @@ use chrono::{DateTime, Local};
|
||||||
use log::error;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::deobfuscate::DeobfData;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
/// Rust package name (`rustypipe`)
|
/// Rust package name (`rustypipe`)
|
||||||
|
@ -27,9 +25,9 @@ pub struct Report {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
/// Detailed error/warning messages
|
/// Detailed error/warning messages
|
||||||
pub msgs: Vec<String>,
|
pub msgs: Vec<String>,
|
||||||
/// Deobfuscation data (only for player requests)
|
// /// Deobfuscation data (only for player requests)
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
// #[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub deobf_data: Option<DeobfData>,
|
// pub deobf_data: Option<DeobfData>,
|
||||||
/// HTTP request data
|
/// HTTP request data
|
||||||
pub http_request: HTTPRequest,
|
pub http_request: HTTPRequest,
|
||||||
}
|
}
|
||||||
|
@ -86,7 +84,7 @@ impl JsonFileReporter {
|
||||||
impl Default for JsonFileReporter {
|
impl Default for JsonFileReporter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
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 {
|
pub struct YamlFileReporter {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "yaml")]
|
|
||||||
impl YamlFileReporter {
|
impl YamlFileReporter {
|
||||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
@ -118,16 +114,14 @@ impl YamlFileReporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "yaml")]
|
|
||||||
impl Default for YamlFileReporter {
|
impl Default for YamlFileReporter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path: Path::new("rustypipe_reports").to_path_buf(),
|
path: Path::new("RustyPipeReports").to_path_buf(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "yaml")]
|
|
||||||
impl Reporter for YamlFileReporter {
|
impl Reporter for YamlFileReporter {
|
||||||
fn report(&self, report: &Report) {
|
fn report(&self, report: &Report) {
|
||||||
self._report(report)
|
self._report(report)
|
||||||
|
|
|
@ -1,36 +1,5 @@
|
||||||
|
pub mod range;
|
||||||
pub mod text;
|
pub mod text;
|
||||||
|
|
||||||
mod range;
|
|
||||||
mod vec_log_err;
|
mod vec_log_err;
|
||||||
|
|
||||||
pub use range::Range;
|
|
||||||
pub use vec_log_err::VecLogError;
|
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -223,7 +223,11 @@ impl<'de> DeserializeAs<'de, Vec<TextLink>> for TextLinks {
|
||||||
D: Deserializer<'de>,
|
D: Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let link = TextLinkInternal::deserialize(deserializer)?;
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ use serde::{
|
||||||
};
|
};
|
||||||
use serde_with::{de::DeserializeAsWrap, DeserializeAs};
|
use serde_with::{de::DeserializeAsWrap, DeserializeAs};
|
||||||
|
|
||||||
use super::MapResult;
|
use crate::client2::MapResult;
|
||||||
|
|
||||||
pub struct VecLogError<T>(PhantomData<T>);
|
pub struct VecLogError<T>(PhantomData<T>);
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ mod tests {
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_with::serde_as;
|
use serde_with::serde_as;
|
||||||
|
|
||||||
use crate::serializer::MapResult;
|
use crate::client2::MapResult;
|
||||||
|
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
@ -52,24 +52,24 @@ impl Mul<u8> for TimeAgo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<TimeAgo> for DateTime<Local> {
|
impl Into<DateTime<Local>> for TimeAgo {
|
||||||
fn from(ta: TimeAgo) -> Self {
|
fn into(self) -> DateTime<Local> {
|
||||||
let ts = Local::now();
|
let ts = Local::now();
|
||||||
match ta.unit {
|
match self.unit {
|
||||||
TimeUnit::Second => ts - Duration::seconds(ta.n as i64),
|
TimeUnit::Second => ts - Duration::seconds(self.n as i64),
|
||||||
TimeUnit::Minute => ts - Duration::minutes(ta.n as i64),
|
TimeUnit::Minute => ts - Duration::minutes(self.n as i64),
|
||||||
TimeUnit::Hour => ts - Duration::hours(ta.n as i64),
|
TimeUnit::Hour => ts - Duration::hours(self.n as i64),
|
||||||
TimeUnit::Day => ts - Duration::days(ta.n as i64),
|
TimeUnit::Day => ts - Duration::days(self.n as i64),
|
||||||
TimeUnit::Week => ts - Duration::weeks(ta.n as i64),
|
TimeUnit::Week => ts - Duration::weeks(self.n as i64),
|
||||||
TimeUnit::Month => chronoutil::shift_months(ts, -(ta.n as i32)),
|
TimeUnit::Month => chronoutil::shift_months(ts, -(self.n as i32)),
|
||||||
TimeUnit::Year => chronoutil::shift_years(ts, -(ta.n as i32)),
|
TimeUnit::Year => chronoutil::shift_years(ts, -(self.n as i32)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ParsedDate> for DateTime<Local> {
|
impl Into<DateTime<Local>> for ParsedDate {
|
||||||
fn from(date: ParsedDate) -> Self {
|
fn into(self) -> DateTime<Local> {
|
||||||
match date {
|
match self {
|
||||||
ParsedDate::Absolute(date) => Local
|
ParsedDate::Absolute(date) => Local
|
||||||
.from_local_datetime(&NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)))
|
.from_local_datetime(&NaiveDateTime::new(date, NaiveTime::from_hms(0, 0, 0)))
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
@ -103,23 +103,29 @@ fn parse_ta_token(entry: &dictionary::Entry, nd: bool, filtered_str: &str) -> Op
|
||||||
|
|
||||||
if entry.by_char {
|
if entry.by_char {
|
||||||
filtered_str.chars().find_map(|word| {
|
filtered_str.chars().find_map(|word| {
|
||||||
tokens.get(&word.to_string()).and_then(|t| match t.unit {
|
tokens
|
||||||
|
.get(&word.to_string())
|
||||||
|
.map(|t| match t.unit {
|
||||||
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
||||||
None => {
|
None => {
|
||||||
qu = t.n;
|
qu = t.n;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
filtered_str.split_whitespace().find_map(|word| {
|
filtered_str.split_whitespace().find_map(|word| {
|
||||||
tokens.get(word).and_then(|t| match t.unit {
|
tokens
|
||||||
|
.get(word)
|
||||||
|
.map(|t| match t.unit {
|
||||||
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
Some(unit) => Some(TimeAgo { n: t.n * qu, unit }),
|
||||||
None => {
|
None => {
|
||||||
qu = t.n;
|
qu = t.n;
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.flatten()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,7 +137,7 @@ fn parse_textual_month(entry: &dictionary::Entry, filtered_str: &str) -> Option<
|
||||||
} else {
|
} else {
|
||||||
filtered_str
|
filtered_str
|
||||||
.split_whitespace()
|
.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 entry = dictionary::entry(lang);
|
||||||
let filtered_str = filter_str(textual_date);
|
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)
|
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() {
|
match nums.len() {
|
||||||
0 => match parse_ta_token(&entry, true, &filtered_str) {
|
0 => match parse_ta_token(&entry, true, &filtered_str) {
|
||||||
Some(timeago) => Some(ParsedDate::Relative(timeago)),
|
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)
|
1 => parse_ta_token(&entry, false, &filtered_str)
|
||||||
.map(|timeago| ParsedDate::Relative(timeago * nums[0] as u8)),
|
.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) {
|
match (y, m, d) {
|
||||||
(Some(y), Some(m), Some(d)) => {
|
(Some(y), Some(m), Some(d)) => {
|
||||||
NaiveDate::from_ymd_opt(y.into(), m.into(), d.into())
|
NaiveDate::from_ymd_opt(y.into(), m.into(), d.into())
|
||||||
.map(ParsedDate::Absolute)
|
.map(|d| ParsedDate::Absolute(d))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`
|
/// `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>)> {
|
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
|
let url_params: BTreeMap<String, String> = parsed_url
|
||||||
.query_pairs()
|
.query_pairs()
|
||||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||||
.collect();
|
.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
|
/// Parse a string after removing all non-numeric characters
|
||||||
|
|
Loading…
Reference in a new issue