Compare commits
12 commits
8536958563
...
d5133247aa
Author | SHA1 | Date | |
---|---|---|---|
|
d5133247aa | ||
d36ba595da | |||
e8324cf3b0 | |||
d053ac3eba | |||
91b020efd4 | |||
114a86a382 | |||
97fb0578b5 | |||
c6bd03fb70 | |||
e1e4fb29c1 | |||
3c83e11e75 | |||
1e1315a837 | |||
e608811e5f |
29 changed files with 4995 additions and 325 deletions
|
@ -17,7 +17,7 @@ jobs:
|
|||
cache-on-failure: "true"
|
||||
|
||||
- name: 📎 Clippy
|
||||
run: cargo clippy --all --features=rss -- -D warnings
|
||||
run: cargo clippy --all --tests --features=rss,indicatif,audiotag -- -D warnings
|
||||
|
||||
- name: 🧪 Test
|
||||
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace
|
||||
|
|
|
@ -10,4 +10,4 @@ repos:
|
|||
hooks:
|
||||
- id: cargo-fmt
|
||||
- id: cargo-clippy
|
||||
args: ["--all", "--tests", "--features=rss", "--", "-D", "warnings"]
|
||||
args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"]
|
||||
|
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"rust-analyzer.cargo.features": ["rss"]
|
||||
"rust-analyzer.cargo.features": ["rss", "indicatif", "audiotag"]
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ dirs = "5.0.0"
|
|||
filenamify = "0.1.0"
|
||||
|
||||
# Testing
|
||||
rstest = "0.21.0"
|
||||
rstest = "0.22.0"
|
||||
tokio-test = "0.4.2"
|
||||
insta = { version = "1.17.1", features = ["ron", "redactions"] }
|
||||
path_macro = "1.0.0"
|
||||
|
@ -74,7 +74,10 @@ tracing-test = "0.2.5"
|
|||
|
||||
# Included crates
|
||||
rustypipe = { path = ".", version = "0.2.0", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.1.0", default-features = false }
|
||||
rustypipe-downloader = { path = "./downloader", version = "0.1.0", default-features = false, features = [
|
||||
"indicatif",
|
||||
"audiotag",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = ["default-tls"]
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg)
|
||||
|
||||
[![Current crates.io version](https://img.shields.io/crates/v/smartcrop2.svg)](https://crates.io/crates/smartcrop2)
|
||||
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
|
||||
[![CI status](https://code.thetadev.de/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://code.thetadev.de/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
|
||||
|
||||
Rust client for the public YouTube / YouTube Music API (Innertube), inspired by
|
||||
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).
|
||||
|
||||
|
|
|
@ -56,3 +56,6 @@ tracing.workspace = true
|
|||
tracing-subscriber.workspace = true
|
||||
serde_yaml.workspace = true
|
||||
dirs.workspace = true
|
||||
|
||||
anstream = "0.6.15"
|
||||
owo-colors = "4.0.0"
|
||||
|
|
1
cli/README.md
Normal file
1
cli/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# RustyPipe CLI
|
592
cli/src/main.rs
592
cli/src/main.rs
|
@ -1,16 +1,23 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![warn(clippy::todo, clippy::dbg_macro)]
|
||||
|
||||
use std::{path::PathBuf, str::FromStr};
|
||||
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
use futures::stream::{self, StreamExt};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use owo_colors::OwoColorize;
|
||||
use rustypipe::{
|
||||
client::{ClientType, RustyPipe},
|
||||
model::{UrlTarget, VideoId, YouTubeItem},
|
||||
model::{
|
||||
richtext::ToPlaintext, traits::YtEntity, ArtistId, MusicSearchResult, TrackItem, UrlTarget,
|
||||
Verification, YouTubeItem,
|
||||
},
|
||||
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
||||
};
|
||||
use rustypipe_downloader::{DownloadQuery, DownloaderBuilder};
|
||||
use rustypipe_downloader::{
|
||||
DownloadError, DownloadQuery, DownloadVideo, Downloader, DownloaderBuilder,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
|
||||
|
@ -81,6 +88,9 @@ enum Commands {
|
|||
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
|
||||
#[clap(short, long)]
|
||||
resolution: Option<u32>,
|
||||
/// Download only the audio track
|
||||
#[clap(long)]
|
||||
audio: bool,
|
||||
/// Number of videos downloaded in parallel
|
||||
#[clap(short, long, default_value_t = 8)]
|
||||
parallel: usize,
|
||||
|
@ -90,8 +100,9 @@ enum Commands {
|
|||
/// Limit the number of videos to download
|
||||
#[clap(long, default_value_t = 1000)]
|
||||
limit: usize,
|
||||
/// YT Client used to fetch player data
|
||||
#[clap(long)]
|
||||
player_type: Option<PlayerType>,
|
||||
client_type: Option<PlayerType>,
|
||||
},
|
||||
/// Extract video, playlist, album or channel data
|
||||
Get {
|
||||
|
@ -103,6 +114,9 @@ enum Commands {
|
|||
/// Pretty-print output
|
||||
#[clap(long)]
|
||||
pretty: bool,
|
||||
/// Output as text
|
||||
#[clap(long)]
|
||||
txt: bool,
|
||||
/// Limit the number of items to fetch
|
||||
#[clap(long, default_value_t = 20)]
|
||||
limit: usize,
|
||||
|
@ -115,14 +129,15 @@ enum Commands {
|
|||
/// Get comments
|
||||
#[clap(long)]
|
||||
comments: Option<CommentsOrder>,
|
||||
/// Get lyrics
|
||||
/// Get lyrics for YTM tracks
|
||||
#[clap(long)]
|
||||
lyrics: bool,
|
||||
/// Get the player
|
||||
/// Get the player data instead of the video details
|
||||
#[clap(long)]
|
||||
player: bool,
|
||||
/// YT Client used to fetch player data
|
||||
#[clap(long)]
|
||||
player_type: Option<PlayerType>,
|
||||
client_type: Option<PlayerType>,
|
||||
},
|
||||
/// Search YouTube
|
||||
Search {
|
||||
|
@ -134,6 +149,9 @@ enum Commands {
|
|||
/// Pretty-print output
|
||||
#[clap(long)]
|
||||
pretty: bool,
|
||||
/// Output as text
|
||||
#[clap(long)]
|
||||
txt: bool,
|
||||
/// Limit the number of items to fetch
|
||||
#[clap(long, default_value_t = 20)]
|
||||
limit: usize,
|
||||
|
@ -165,7 +183,7 @@ enum Format {
|
|||
Yaml,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, ValueEnum)]
|
||||
#[derive(Debug, Copy, Clone, ValueEnum)]
|
||||
enum ChannelTab {
|
||||
Videos,
|
||||
Shorts,
|
||||
|
@ -236,6 +254,7 @@ enum MusicSearchCategory {
|
|||
enum PlayerType {
|
||||
Desktop,
|
||||
Tv,
|
||||
TvEmbed,
|
||||
Android,
|
||||
Ios,
|
||||
}
|
||||
|
@ -286,7 +305,8 @@ impl From<PlayerType> for ClientType {
|
|||
fn from(value: PlayerType) -> Self {
|
||||
match value {
|
||||
PlayerType::Desktop => Self::Desktop,
|
||||
PlayerType::Tv => Self::TvHtml5Embed,
|
||||
PlayerType::TvEmbed => Self::TvHtml5Embed,
|
||||
PlayerType::Tv => Self::Tv,
|
||||
PlayerType::Android => Self::Android,
|
||||
PlayerType::Ios => Self::Ios,
|
||||
}
|
||||
|
@ -307,30 +327,99 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
|||
};
|
||||
}
|
||||
|
||||
async fn download_video(
|
||||
rp: &RustyPipe,
|
||||
id: &str,
|
||||
target: &DownloadTarget,
|
||||
resolution: Option<u32>,
|
||||
player_type: Option<PlayerType>,
|
||||
multi: MultiProgress,
|
||||
) {
|
||||
let mut filter = StreamFilter::new();
|
||||
if let Some(res) = resolution {
|
||||
if res == 0 {
|
||||
filter = filter.no_video();
|
||||
} else {
|
||||
filter = filter.video_max_res(res);
|
||||
fn print_entities(items: &[impl YtEntity]) {
|
||||
for e in items {
|
||||
anstream::print!("[{}] {}", e.id(), e.name().bold());
|
||||
if let Some(n) = e.channel_name() {
|
||||
anstream::print!(" - {}", n.cyan());
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tracks(tracks: &[TrackItem]) {
|
||||
for t in tracks {
|
||||
if let Some(n) = t.track_nr {
|
||||
anstream::print!("{} ", format!("{n:02}").yellow().bold());
|
||||
}
|
||||
anstream::print!("[{}] {} - ", t.id, t.name.bold());
|
||||
print_artists(&t.artists);
|
||||
print_duration(t.duration);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
fn print_artists(artists: &[ArtistId]) {
|
||||
for (i, a) in artists.iter().enumerate() {
|
||||
if i > 0 {
|
||||
print!(", ");
|
||||
}
|
||||
anstream::print!("{}", a.name.cyan());
|
||||
if let Some(id) = &a.id {
|
||||
print!(" [{id}]");
|
||||
}
|
||||
}
|
||||
let dl = DownloaderBuilder::new()
|
||||
.client(rp)
|
||||
.stream_filter(filter)
|
||||
.progress_bar(multi)
|
||||
.build();
|
||||
let mut q = target.apply(dl.download_id(id));
|
||||
if let Some(player_type) = player_type {
|
||||
q = q.player_type(player_type.into());
|
||||
}
|
||||
|
||||
fn print_duration(duration: Option<u32>) {
|
||||
if let Some(d) = duration {
|
||||
print!(" ");
|
||||
let hours = d / 3600;
|
||||
let minutes = (d / 60) % 60;
|
||||
let seconds = d % 60;
|
||||
if hours > 0 {
|
||||
anstream::print!("{}", format!("{hours:02}:").yellow());
|
||||
}
|
||||
anstream::print!("{}", format!("{minutes:02}:{seconds:02}").yellow());
|
||||
}
|
||||
}
|
||||
|
||||
fn print_music_search<T: Serialize + YtEntity>(
|
||||
data: &MusicSearchResult<T>,
|
||||
format: Format,
|
||||
pretty: bool,
|
||||
txt: bool,
|
||||
) {
|
||||
if txt {
|
||||
if let Some(corr) = &data.corrected_query {
|
||||
anstream::println!("Did you mean `{}`?", corr.magenta());
|
||||
}
|
||||
print_entities(&data.items.items);
|
||||
} else {
|
||||
print_data(data, format, pretty)
|
||||
}
|
||||
}
|
||||
|
||||
fn print_description(desc: Option<String>) {
|
||||
if let Some(desc) = desc {
|
||||
if !desc.is_empty() {
|
||||
print_h2("Description");
|
||||
println!("{}", desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_h2(title: &str) {
|
||||
anstream::println!("\n{}", format!("{title}:").green().underline());
|
||||
}
|
||||
|
||||
fn print_verification(verification: Verification) {
|
||||
match verification {
|
||||
Verification::None => {}
|
||||
Verification::Verified => print!(" ✓"),
|
||||
Verification::Artist => print!(" ♪"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_video(
|
||||
dl: &Downloader,
|
||||
id: &str,
|
||||
target: &DownloadTarget,
|
||||
client_type: Option<PlayerType>,
|
||||
) {
|
||||
let mut q = target.apply(dl.id(id));
|
||||
if let Some(client_type) = client_type {
|
||||
q = q.client_type(client_type.into());
|
||||
}
|
||||
let res = q.download().await;
|
||||
if let Err(e) = res {
|
||||
|
@ -339,29 +428,13 @@ async fn download_video(
|
|||
}
|
||||
|
||||
async fn download_videos(
|
||||
rp: &RustyPipe,
|
||||
videos: &[VideoId],
|
||||
dl: &Downloader,
|
||||
videos: Vec<DownloadVideo>,
|
||||
target: &DownloadTarget,
|
||||
resolution: Option<u32>,
|
||||
parallel: usize,
|
||||
player_type: Option<PlayerType>,
|
||||
client_type: Option<PlayerType>,
|
||||
multi: MultiProgress,
|
||||
) {
|
||||
let mut filter = StreamFilter::new();
|
||||
if let Some(res) = resolution {
|
||||
if res == 0 {
|
||||
filter = filter.no_video();
|
||||
} else {
|
||||
filter = filter.video_max_res(res);
|
||||
}
|
||||
}
|
||||
let dl = DownloaderBuilder::new()
|
||||
.client(rp)
|
||||
.stream_filter(filter)
|
||||
.progress_bar(multi.clone())
|
||||
.path_precheck()
|
||||
.build();
|
||||
|
||||
// Indicatif setup
|
||||
let main = multi.add(ProgressBar::new(
|
||||
videos.len().try_into().unwrap_or_default(),
|
||||
|
@ -379,19 +452,20 @@ async fn download_videos(
|
|||
.for_each_concurrent(parallel, |video| {
|
||||
let dl = dl.clone();
|
||||
let main = main.clone();
|
||||
let id = &video.id;
|
||||
let id = video.id().to_owned();
|
||||
|
||||
let mut q = target.apply(dl.download_entity(video));
|
||||
if let Some(player_type) = player_type {
|
||||
q = q.player_type(player_type.into());
|
||||
let mut q = target.apply(dl.video(video));
|
||||
if let Some(client_type) = client_type {
|
||||
q = q.client_type(client_type.into());
|
||||
}
|
||||
|
||||
async move {
|
||||
if let Err(e) = q.download().await {
|
||||
tracing::error!("[{id}]: {e}");
|
||||
} else {
|
||||
main.inc(1);
|
||||
if !matches!(e, DownloadError::Exists(_)) {
|
||||
tracing::error!("[{id}]: {e}");
|
||||
}
|
||||
}
|
||||
main.inc(1);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
@ -433,7 +507,9 @@ async fn main() {
|
|||
.with_writer(ProgWriter(multi.clone()))
|
||||
.init();
|
||||
|
||||
let mut rp = RustyPipe::builder().visitor_data_opt(cli.vdata);
|
||||
let mut rp = RustyPipe::builder()
|
||||
.visitor_data_opt(cli.vdata)
|
||||
.timeout(Duration::from_secs(15));
|
||||
if cli.report {
|
||||
rp = rp.report();
|
||||
} else {
|
||||
|
@ -455,15 +531,35 @@ async fn main() {
|
|||
id,
|
||||
target,
|
||||
resolution,
|
||||
audio,
|
||||
parallel,
|
||||
music,
|
||||
limit,
|
||||
player_type,
|
||||
client_type,
|
||||
} => {
|
||||
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||
|
||||
let mut filter = StreamFilter::new();
|
||||
if let Some(res) = resolution {
|
||||
if res == 0 {
|
||||
filter = filter.no_video();
|
||||
} else {
|
||||
filter = filter.video_max_res(res);
|
||||
}
|
||||
}
|
||||
let mut dl = DownloaderBuilder::new()
|
||||
.rustypipe(&rp)
|
||||
.multi_progress(multi.clone())
|
||||
.path_precheck();
|
||||
if audio {
|
||||
dl = dl.audio_tag().crop_cover();
|
||||
filter = filter.no_video();
|
||||
}
|
||||
let dl = dl.stream_filter(filter).build();
|
||||
|
||||
match url_target {
|
||||
UrlTarget::Video { id, .. } => {
|
||||
download_video(&rp, &id, &target, resolution, player_type, multi).await;
|
||||
download_video(&dl, &id, &target, client_type).await;
|
||||
}
|
||||
UrlTarget::Channel { id } => {
|
||||
target.assert_dir();
|
||||
|
@ -473,27 +569,18 @@ async fn main() {
|
|||
.extend_limit(&rp.query(), limit)
|
||||
.await
|
||||
.unwrap();
|
||||
let videos: Vec<VideoId> = channel
|
||||
let videos = channel
|
||||
.content
|
||||
.items
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(VideoId::from)
|
||||
.map(|v| DownloadVideo::from_entity(&v))
|
||||
.collect();
|
||||
download_videos(
|
||||
&rp,
|
||||
&videos,
|
||||
&target,
|
||||
resolution,
|
||||
parallel,
|
||||
player_type,
|
||||
multi,
|
||||
)
|
||||
.await;
|
||||
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||
}
|
||||
UrlTarget::Playlist { id } => {
|
||||
target.assert_dir();
|
||||
let videos: Vec<VideoId> = if music {
|
||||
let videos = if music {
|
||||
let mut playlist = rp.query().music_playlist(id).await.unwrap();
|
||||
playlist
|
||||
.tracks
|
||||
|
@ -505,7 +592,7 @@ async fn main() {
|
|||
.items
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(VideoId::from)
|
||||
.map(|v| DownloadVideo::from_track(&v))
|
||||
.collect()
|
||||
} else {
|
||||
let mut playlist = rp.query().playlist(id).await.unwrap();
|
||||
|
@ -519,45 +606,28 @@ async fn main() {
|
|||
.items
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(VideoId::from)
|
||||
.map(|v| DownloadVideo::from_entity(&v))
|
||||
.collect()
|
||||
};
|
||||
download_videos(
|
||||
&rp,
|
||||
&videos,
|
||||
&target,
|
||||
resolution,
|
||||
parallel,
|
||||
player_type,
|
||||
multi,
|
||||
)
|
||||
.await;
|
||||
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||
}
|
||||
UrlTarget::Album { id } => {
|
||||
target.assert_dir();
|
||||
let album = rp.query().music_album(id).await.unwrap();
|
||||
let videos: Vec<VideoId> = album
|
||||
let videos = album
|
||||
.tracks
|
||||
.into_iter()
|
||||
.take(limit)
|
||||
.map(VideoId::from)
|
||||
.map(|v| DownloadVideo::from_track(&v))
|
||||
.collect();
|
||||
download_videos(
|
||||
&rp,
|
||||
&videos,
|
||||
&target,
|
||||
resolution,
|
||||
parallel,
|
||||
player_type,
|
||||
multi,
|
||||
)
|
||||
.await;
|
||||
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Get {
|
||||
id,
|
||||
format,
|
||||
txt,
|
||||
pretty,
|
||||
limit,
|
||||
tab,
|
||||
|
@ -565,7 +635,7 @@ async fn main() {
|
|||
comments,
|
||||
lyrics,
|
||||
player,
|
||||
player_type,
|
||||
client_type,
|
||||
} => {
|
||||
let target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||
|
||||
|
@ -576,16 +646,47 @@ async fn main() {
|
|||
match details.lyrics_id {
|
||||
Some(lyrics_id) => {
|
||||
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap();
|
||||
print_data(&lyrics, format, pretty);
|
||||
if txt {
|
||||
println!("{}\n\n{}", lyrics.body, lyrics.footer);
|
||||
} else {
|
||||
print_data(&lyrics, format, pretty);
|
||||
}
|
||||
}
|
||||
None => eprintln!("no lyrics found"),
|
||||
}
|
||||
} else if music {
|
||||
let details = rp.query().music_details(&id).await.unwrap();
|
||||
print_data(&details, format, pretty);
|
||||
if txt {
|
||||
if details.track.is_video {
|
||||
println!("[MV]");
|
||||
} else {
|
||||
println!("[Track]");
|
||||
}
|
||||
print!("{} [{}]", details.track.name, details.track.id);
|
||||
print_duration(details.track.duration);
|
||||
println!();
|
||||
print_artists(&details.track.artists);
|
||||
println!();
|
||||
if !details.track.is_video {
|
||||
println!(
|
||||
"Album: {}",
|
||||
details
|
||||
.track
|
||||
.album
|
||||
.as_ref()
|
||||
.map(|b| b.id.as_str())
|
||||
.unwrap_or("None")
|
||||
)
|
||||
}
|
||||
if let Some(view_count) = details.track.view_count {
|
||||
println!("Views: {view_count}");
|
||||
}
|
||||
} else {
|
||||
print_data(&details, format, pretty);
|
||||
}
|
||||
} else if player {
|
||||
let player = if let Some(player_type) = player_type {
|
||||
rp.query().player_from_client(&id, player_type.into()).await
|
||||
let player = if let Some(client_type) = client_type {
|
||||
rp.query().player_from_client(&id, client_type.into()).await
|
||||
} else {
|
||||
rp.query().player(&id).await
|
||||
}
|
||||
|
@ -612,13 +713,129 @@ async fn main() {
|
|||
None => {}
|
||||
}
|
||||
|
||||
print_data(&details, format, pretty);
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n{} [{}]",
|
||||
"[Video]".on_green().black(),
|
||||
details.name.green().bold(),
|
||||
details.id
|
||||
);
|
||||
anstream::println!(
|
||||
"{} {} [{}]",
|
||||
"Channel:".blue(),
|
||||
details.channel.name,
|
||||
details.channel.id
|
||||
);
|
||||
if let Some(subs) = details.channel.subscriber_count {
|
||||
anstream::println!("{} {}", "Subscribers:".blue(), subs);
|
||||
}
|
||||
if let Some(date) = details.publish_date {
|
||||
anstream::println!("{} {}", "Date:".blue(), date);
|
||||
}
|
||||
anstream::println!("{} {}", "Views:".blue(), details.view_count);
|
||||
if let Some(likes) = details.like_count {
|
||||
anstream::println!("{} {}", "Likes:".blue(), likes);
|
||||
}
|
||||
if let Some(comments) = details.top_comments.count {
|
||||
anstream::println!("{} {}", "Comments:".blue(), comments);
|
||||
}
|
||||
if details.is_ccommons {
|
||||
anstream::println!("{}", "Creative Commons".green());
|
||||
}
|
||||
if details.is_live {
|
||||
anstream::println!("{}", "Livestream".red());
|
||||
}
|
||||
print_description(Some(details.description.to_plaintext()));
|
||||
if !details.recommended.is_empty() {
|
||||
print_h2("Recommended");
|
||||
print_entities(&details.recommended.items);
|
||||
}
|
||||
let comment_list = comments.map(|c| match c {
|
||||
CommentsOrder::Top => &details.top_comments.items,
|
||||
CommentsOrder::Latest => &details.latest_comments.items,
|
||||
});
|
||||
if let Some(comment_list) = comment_list {
|
||||
print_h2("Comments");
|
||||
for c in comment_list {
|
||||
if let Some(author) = &c.author {
|
||||
anstream::print!("{} [{}]", author.name.cyan(), author.id);
|
||||
print_verification(author.verification);
|
||||
} else {
|
||||
anstream::print!("{}", "Unknown author".magenta());
|
||||
}
|
||||
if c.by_owner {
|
||||
print!(" (Owner)");
|
||||
}
|
||||
println!();
|
||||
println!("{}", c.text.to_plaintext());
|
||||
anstream::print!(
|
||||
"{} {}",
|
||||
"Likes:".blue(),
|
||||
c.like_count.unwrap_or_default()
|
||||
);
|
||||
if c.hearted {
|
||||
anstream::print!(" {}", "♥".red());
|
||||
}
|
||||
println!("\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print_data(&details, format, pretty);
|
||||
}
|
||||
}
|
||||
}
|
||||
UrlTarget::Channel { id } => {
|
||||
if music {
|
||||
let artist = rp.query().music_artist(&id, true).await.unwrap();
|
||||
print_data(&artist, format, pretty);
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n{} [{}]",
|
||||
"[Artist]".on_green().black(),
|
||||
artist.name.green().bold(),
|
||||
artist.id
|
||||
);
|
||||
if let Some(subs) = artist.subscriber_count {
|
||||
anstream::println!("{} {}", "Subscribers:".blue(), subs);
|
||||
}
|
||||
if let Some(url) = artist.wikipedia_url {
|
||||
anstream::println!("{} {}", "Wikipedia:".blue(), url);
|
||||
}
|
||||
if let Some(id) = artist.tracks_playlist_id {
|
||||
anstream::println!("{} {}", "All tracks:".blue(), id);
|
||||
}
|
||||
if let Some(id) = artist.videos_playlist_id {
|
||||
anstream::println!("{} {}", "All videos:".blue(), id);
|
||||
}
|
||||
if let Some(id) = artist.radio_id {
|
||||
anstream::println!("{} {}", "Radio:".blue(), id);
|
||||
}
|
||||
print_description(artist.description);
|
||||
if !artist.albums.is_empty() {
|
||||
print_h2("Albums");
|
||||
for b in artist.albums {
|
||||
anstream::print!(
|
||||
"[{}] {} ({:?}",
|
||||
b.id,
|
||||
b.name.bold(),
|
||||
b.album_type
|
||||
);
|
||||
if let Some(y) = b.year {
|
||||
print!(", {y}");
|
||||
}
|
||||
println!(")");
|
||||
}
|
||||
}
|
||||
if !artist.playlists.is_empty() {
|
||||
print_h2("Playlists");
|
||||
print_entities(&artist.playlists);
|
||||
}
|
||||
if !artist.similar_artists.is_empty() {
|
||||
print_h2("Similar artists");
|
||||
print_entities(&artist.similar_artists);
|
||||
}
|
||||
} else {
|
||||
print_data(&artist, format, pretty);
|
||||
}
|
||||
} else {
|
||||
match tab {
|
||||
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
|
||||
|
@ -636,15 +853,77 @@ async fn main() {
|
|||
.extend_limit(rp.query(), limit)
|
||||
.await
|
||||
.unwrap();
|
||||
print_data(&channel, format, pretty);
|
||||
|
||||
if txt {
|
||||
anstream::print!(
|
||||
"{}\n{} [{}]",
|
||||
format!("[Channel {tab:?}]").on_green().black(),
|
||||
channel.name.green().bold(),
|
||||
channel.id
|
||||
);
|
||||
print_verification(channel.verification);
|
||||
println!();
|
||||
if let Some(subs) = channel.subscriber_count {
|
||||
anstream::println!("{} {}", "Subscribers:".blue(), subs);
|
||||
}
|
||||
print_description(Some(channel.description));
|
||||
println!();
|
||||
print_entities(&channel.content.items);
|
||||
} else {
|
||||
print_data(&channel, format, pretty);
|
||||
}
|
||||
}
|
||||
ChannelTab::Playlists => {
|
||||
let channel = rp.query().channel_playlists(&id).await.unwrap();
|
||||
print_data(&channel, format, pretty);
|
||||
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n{} [{}]",
|
||||
format!("[Channel {tab:?}]").on_green().black(),
|
||||
channel.name.green().bold(),
|
||||
channel.id
|
||||
);
|
||||
print_description(Some(channel.description));
|
||||
if let Some(subs) = channel.subscriber_count {
|
||||
anstream::println!("{} {}", "Subscribers:".blue(), subs);
|
||||
}
|
||||
println!();
|
||||
print_entities(&channel.content.items);
|
||||
} else {
|
||||
print_data(&channel, format, pretty);
|
||||
}
|
||||
}
|
||||
ChannelTab::Info => {
|
||||
let channel = rp.query().channel_info(&id).await.unwrap();
|
||||
print_data(&channel, format, pretty);
|
||||
let info = rp.query().channel_info(&id).await.unwrap();
|
||||
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n<b>ID:</b>{}",
|
||||
"[Channel info]".on_green().black(),
|
||||
info.id
|
||||
);
|
||||
print_description(Some(info.description));
|
||||
if let Some(subs) = info.subscriber_count {
|
||||
anstream::println!("{} {}", "Subscribers:".blue(), subs);
|
||||
}
|
||||
if let Some(vids) = info.video_count {
|
||||
anstream::println!("{} {}", "Videos:".blue(), vids);
|
||||
}
|
||||
if let Some(views) = info.view_count {
|
||||
anstream::println!("{} {}", "Views:".blue(), views);
|
||||
}
|
||||
if let Some(created) = info.create_date {
|
||||
anstream::println!("{} {}", "Created on:".blue(), created);
|
||||
}
|
||||
if !info.links.is_empty() {
|
||||
print_h2("Links");
|
||||
for (name, url) in &info.links {
|
||||
anstream::println!("{} {}", name.blue(), url);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
print_data(&info, format, pretty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -657,7 +936,28 @@ async fn main() {
|
|||
.extend_limit(rp.query(), limit)
|
||||
.await
|
||||
.unwrap();
|
||||
print_data(&playlist, format, pretty);
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n{} [{}]\n{} {}",
|
||||
"[MusicPlaylist]".on_green().black(),
|
||||
playlist.name.green().bold(),
|
||||
playlist.id,
|
||||
"Tracks:".blue(),
|
||||
playlist.track_count.unwrap_or_default(),
|
||||
);
|
||||
if let Some(n) = playlist.channel_name() {
|
||||
anstream::print!("{} {}", "Author:".blue(), n.bold());
|
||||
if let Some(id) = playlist.channel_id() {
|
||||
print!(" [{id}]");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
print_description(playlist.description.map(|d| d.to_plaintext()));
|
||||
println!();
|
||||
print_tracks(&playlist.tracks.items);
|
||||
} else {
|
||||
print_data(&playlist, format, pretty);
|
||||
}
|
||||
} else {
|
||||
let mut playlist = rp.query().playlist(&id).await.unwrap();
|
||||
playlist
|
||||
|
@ -665,12 +965,59 @@ async fn main() {
|
|||
.extend_limit(rp.query(), limit)
|
||||
.await
|
||||
.unwrap();
|
||||
print_data(&playlist, format, pretty);
|
||||
if txt {
|
||||
anstream::println!(
|
||||
"{}\n{} [{}]\n{} {}",
|
||||
"[Playlist]".on_green().black(),
|
||||
playlist.name.green().bold(),
|
||||
playlist.id,
|
||||
"Videos:".blue(),
|
||||
playlist.video_count,
|
||||
);
|
||||
if let Some(n) = playlist.channel_name() {
|
||||
anstream::print!("{} {}", "Author:".blue(), n.bold());
|
||||
if let Some(id) = playlist.channel_id() {
|
||||
print!(" [{id}]");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
if let Some(last_update) = playlist.last_update {
|
||||
anstream::println!("{} {}", "Last update:".blue(), last_update);
|
||||
}
|
||||
print_description(playlist.description.map(|d| d.to_plaintext()));
|
||||
println!();
|
||||
print_entities(&playlist.videos.items);
|
||||
} else {
|
||||
print_data(&playlist, format, pretty);
|
||||
}
|
||||
}
|
||||
}
|
||||
UrlTarget::Album { id } => {
|
||||
let album = rp.query().music_album(&id).await.unwrap();
|
||||
print_data(&album, format, pretty);
|
||||
if txt {
|
||||
anstream::print!(
|
||||
"{}\n{} [{}] ({:?}",
|
||||
"[Album]".on_green().black(),
|
||||
album.name.green().bold(),
|
||||
album.id,
|
||||
album.album_type
|
||||
);
|
||||
if let Some(year) = album.year {
|
||||
print!(", {year}");
|
||||
}
|
||||
println!(")");
|
||||
if let Some(n) = album.channel_name() {
|
||||
anstream::print!("{} {}", "Artist:".blue(), n);
|
||||
if let Some(id) = album.channel_id() {
|
||||
print!(" [{id}]");
|
||||
}
|
||||
}
|
||||
print_description(album.description.map(|d| d.to_plaintext()));
|
||||
println!();
|
||||
print_tracks(&album.tracks);
|
||||
} else {
|
||||
print_data(&album, format, pretty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -678,6 +1025,7 @@ async fn main() {
|
|||
query,
|
||||
format,
|
||||
pretty,
|
||||
txt,
|
||||
limit,
|
||||
item_type,
|
||||
length,
|
||||
|
@ -704,32 +1052,40 @@ async fn main() {
|
|||
.await
|
||||
.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
|
||||
if txt {
|
||||
if let Some(corr) = res.corrected_query {
|
||||
anstream::println!("Did you mean `{}`?", corr.magenta());
|
||||
}
|
||||
print_entities(&res.items.items);
|
||||
} else {
|
||||
print_data(&res, format, pretty);
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(MusicSearchCategory::All) => {
|
||||
let res = rp.query().music_search_main(&query).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
Some(MusicSearchCategory::Tracks) => {
|
||||
let mut res = rp.query().music_search_tracks(&query).await.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
Some(MusicSearchCategory::Videos) => {
|
||||
let mut res = rp.query().music_search_videos(&query).await.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
Some(MusicSearchCategory::Artists) => {
|
||||
let mut res = rp.query().music_search_artists(&query).await.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
Some(MusicSearchCategory::Albums) => {
|
||||
let mut res = rp.query().music_search_albums(&query).await.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
|
||||
let mut res = rp
|
||||
|
@ -741,7 +1097,7 @@ async fn main() {
|
|||
.await
|
||||
.unwrap();
|
||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||
print_data(&res, format, pretty);
|
||||
print_music_search(&res, format, pretty, txt);
|
||||
}
|
||||
},
|
||||
Commands::Vdata => {
|
||||
|
|
|
@ -66,9 +66,10 @@ pub async fn download_testfiles() {
|
|||
music_genre().await;
|
||||
}
|
||||
|
||||
const CLIENT_TYPES: [ClientType; 5] = [
|
||||
const CLIENT_TYPES: [ClientType; 6] = [
|
||||
ClientType::Desktop,
|
||||
ClientType::DesktopMusic,
|
||||
ClientType::Tv,
|
||||
ClientType::TvHtml5Embed,
|
||||
ClientType::Android,
|
||||
ClientType::Ios,
|
||||
|
|
|
@ -30,6 +30,8 @@ rustls-tls-native-roots = [
|
|||
"rustypipe/rustls-tls-native-roots",
|
||||
]
|
||||
|
||||
audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"]
|
||||
|
||||
[dependencies]
|
||||
rustypipe.workspace = true
|
||||
once_cell.workspace = true
|
||||
|
@ -39,6 +41,10 @@ futures.workspace = true
|
|||
reqwest = { workspace = true, features = ["stream"] }
|
||||
rand.workspace = true
|
||||
tokio = { workspace = true, features = ["macros", "fs", "process"] }
|
||||
indicatif.workspace = true
|
||||
indicatif = { workspace = true, optional = true }
|
||||
filenamify.workspace = true
|
||||
tracing.workspace = true
|
||||
time.workspace = true
|
||||
lofty = { version = "0.21.0", optional = true }
|
||||
image = { version = "0.25.0", optional = true }
|
||||
smartcrop2 = { version = "0.3.0", optional = true }
|
||||
|
|
42
downloader/README.md
Normal file
42
downloader/README.md
Normal file
|
@ -0,0 +1,42 @@
|
|||
# RustyPipe downloader
|
||||
|
||||
The downloader is a companion crate for RustyPipe that allows for easy and fast
|
||||
downloading of video and audio files.
|
||||
|
||||
## Features
|
||||
|
||||
- Fast download of streams, bypassing YouTube's throttling
|
||||
- Join video and audio streams using ffmpeg
|
||||
- [Indicatif](https://crates.io/crates/indicatif) support to show download progress bars
|
||||
(enable `indicatif` feature to use)
|
||||
- Tag audio files with title, album, artist, date, description and album cover (enable
|
||||
`audiotag` feature to use)
|
||||
- Album covers are automatically cropped using smartcrop to ensure they are square
|
||||
|
||||
## How to use
|
||||
|
||||
For the downloader to work, you need to have ffmpeg installed on your system. If your
|
||||
ffmpeg binary is located at a non-standard path, you can configure the location using
|
||||
[`DownloaderBuilder::ffmpeg`].
|
||||
|
||||
At first you have to instantiate and configure the downloader using either
|
||||
[`Downloader::new`] or the [`DownloaderBuilder`].
|
||||
|
||||
Then you can build a new download query with a video ID, stream filter and destination
|
||||
path and finally download the video.
|
||||
|
||||
```rust ignore
|
||||
use rustypipe::param::StreamFilter;
|
||||
use rustypipe_downloader::DownloaderBuilder;
|
||||
|
||||
let dl = DownloaderBuilder::new()
|
||||
.audio_tag()
|
||||
.crop_cover()
|
||||
.build();
|
||||
|
||||
let filter_audio = StreamFilter::new().no_video();
|
||||
dl.id("ZeerrnuLi5E").stream_filter(filter_audio).to_file("audio.opus").download().await;
|
||||
|
||||
let filter_video = StreamFilter::new().video_max_res(720);
|
||||
dl.id("ZeerrnuLi5E").stream_filter(filter_video).to_file("video.mp4").download().await;
|
||||
```
|
File diff suppressed because it is too large
Load diff
|
@ -27,11 +27,29 @@ pub enum DownloadError {
|
|||
/// Download target already exists
|
||||
#[error("file {0} already exists")]
|
||||
Exists(PathBuf),
|
||||
#[cfg(feature = "audiotag")]
|
||||
/// Audio tagging error
|
||||
#[error("Audio tag error: {0}")]
|
||||
AudioTag(Cow<'static, str>),
|
||||
/// Other error
|
||||
#[error("error: {0}")]
|
||||
Other(Cow<'static, str>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "audiotag")]
|
||||
impl From<lofty::error::LoftyError> for DownloadError {
|
||||
fn from(value: lofty::error::LoftyError) -> Self {
|
||||
Self::AudioTag(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "audiotag")]
|
||||
impl From<image::ImageError> for DownloadError {
|
||||
fn from(value: image::ImageError) -> Self {
|
||||
Self::AudioTag(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Split an URL into its base string and parameter map
|
||||
///
|
||||
/// Example:
|
||||
|
|
|
@ -61,6 +61,8 @@ pub enum ClientType {
|
|||
///
|
||||
/// can access age-restricted videos, cannot access non-embeddable videos
|
||||
TvHtml5Embed,
|
||||
/// Client used by youtube.com/tv
|
||||
Tv,
|
||||
/// Client used by the Android app
|
||||
///
|
||||
/// no obfuscated stream URLs, includes lower resolution audio streams
|
||||
|
@ -74,7 +76,10 @@ pub enum ClientType {
|
|||
impl ClientType {
|
||||
fn is_web(self) -> bool {
|
||||
match self {
|
||||
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true,
|
||||
ClientType::Desktop
|
||||
| ClientType::DesktopMusic
|
||||
| ClientType::TvHtml5Embed
|
||||
| ClientType::Tv => true,
|
||||
ClientType::Android | ClientType::Ios => false,
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +188,7 @@ struct QContinuation<'a> {
|
|||
}
|
||||
|
||||
const DEFAULT_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0";
|
||||
const TV_UA: &str = "Mozilla/5.0 (SMART-TV; Linux; Tizen 5.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/5.0 NativeTVAds Safari/538.1";
|
||||
|
||||
const CONSENT_COOKIE: &str = "SOCS=CAISAiAD";
|
||||
|
||||
|
@ -191,12 +197,14 @@ 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 YOUTUBE_HOME_URL: &str = "https://www.youtube.com/";
|
||||
const YOUTUBE_MUSIC_HOME_URL: &str = "https://music.youtube.com/";
|
||||
const YOUTUBE_TV_URL: &str = "https://www.youtube.com/tv";
|
||||
|
||||
const DISABLE_PRETTY_PRINT_PARAMETER: &str = "prettyPrint=false";
|
||||
|
||||
// Desktop client
|
||||
const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00";
|
||||
const TVHTML5_CLIENT_VERSION: &str = "2.0";
|
||||
const TV_CLIENT_VERSION: &str = "7.20240724.13.00";
|
||||
const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01";
|
||||
|
||||
// Mobile client
|
||||
|
@ -454,7 +462,7 @@ impl RustyPipeBuilder {
|
|||
self.build_with_client(ClientBuilder::new())
|
||||
}
|
||||
|
||||
/// Create a new, configured RustyPipe instance using a Reqwest client builder.
|
||||
/// Create a new, configured RustyPipe instance using a Reqwest [`ClientBuilder`].
|
||||
pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
|
||||
let user_agent = self
|
||||
.user_agent
|
||||
|
@ -717,31 +725,33 @@ impl RustyPipe {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the internal HTTP client
|
||||
///
|
||||
/// Can be used for downloading videos or custom YT requests.
|
||||
#[must_use]
|
||||
pub fn http_client(&self) -> &Client {
|
||||
&self.inner.http
|
||||
}
|
||||
|
||||
/// Execute the given http request.
|
||||
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
|
||||
let mut last_resp = None;
|
||||
for n in 0..=self.inner.n_http_retries {
|
||||
let resp = self
|
||||
.inner
|
||||
.http
|
||||
.execute(request.try_clone().unwrap())
|
||||
.await?;
|
||||
let resp = self.inner.http.execute(request.try_clone().unwrap()).await;
|
||||
|
||||
let status = resp.status();
|
||||
// Immediately return in case of success or unrecoverable status code
|
||||
if status.is_success()
|
||||
|| (!status.is_server_error() && status != StatusCode::TOO_MANY_REQUESTS)
|
||||
{
|
||||
return Ok(resp);
|
||||
}
|
||||
let err = match resp {
|
||||
Ok(resp) => {
|
||||
let status = resp.status();
|
||||
// Immediately return in case of success or unrecoverable status code
|
||||
if status.is_success()
|
||||
|| (!status.is_server_error() && status != StatusCode::TOO_MANY_REQUESTS)
|
||||
{
|
||||
return Ok(resp);
|
||||
}
|
||||
last_resp = Some(Ok(resp));
|
||||
status.to_string()
|
||||
}
|
||||
Err(e) => {
|
||||
// Retry in case of a timeout error
|
||||
if !e.is_timeout() {
|
||||
return Err(e);
|
||||
}
|
||||
last_resp = Some(Err(e));
|
||||
"timeout".to_string()
|
||||
}
|
||||
};
|
||||
|
||||
// Retry in case of a recoverable status code (server err, too many requests)
|
||||
if n != self.inner.n_http_retries {
|
||||
|
@ -749,15 +759,13 @@ impl RustyPipe {
|
|||
tracing::warn!(
|
||||
"Retry attempt #{}. Error: {}. Waiting {} ms",
|
||||
n + 1,
|
||||
status,
|
||||
err,
|
||||
ms
|
||||
);
|
||||
tokio::time::sleep(Duration::from_millis(ms.into())).await;
|
||||
}
|
||||
|
||||
last_resp = Some(resp);
|
||||
}
|
||||
Ok(last_resp.unwrap())
|
||||
last_resp.unwrap()
|
||||
}
|
||||
|
||||
/// Execute the given http request, returning an error in case of a
|
||||
|
@ -1098,6 +1106,7 @@ impl RustyPipeQuery {
|
|||
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => {
|
||||
Cow::Borrowed(&self.client.inner.user_agent)
|
||||
}
|
||||
ClientType::Tv => TV_UA.into(),
|
||||
ClientType::Android => format!(
|
||||
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
|
||||
MOBILE_CLIENT_VERSION, self.opts.country
|
||||
|
@ -1178,6 +1187,24 @@ impl RustyPipeQuery {
|
|||
embed_url: YOUTUBE_HOME_URL,
|
||||
}),
|
||||
},
|
||||
ClientType::Tv => YTContext {
|
||||
client: ClientInfo {
|
||||
client_name: "TVHTML5",
|
||||
client_version: Cow::Borrowed(TV_CLIENT_VERSION),
|
||||
client_screen: Some("WATCH"),
|
||||
platform: "TV",
|
||||
device_model: Some("SmartTV"),
|
||||
visitor_data,
|
||||
hl,
|
||||
gl,
|
||||
..Default::default()
|
||||
},
|
||||
request: Some(RequestYT::default()),
|
||||
user: User::default(),
|
||||
third_party: Some(ThirdParty {
|
||||
embed_url: YOUTUBE_TV_URL,
|
||||
}),
|
||||
},
|
||||
ClientType::Android => YTContext {
|
||||
client: ClientInfo {
|
||||
client_name: "ANDROID",
|
||||
|
@ -1266,6 +1293,17 @@ impl RustyPipeQuery {
|
|||
.header(header::REFERER, YOUTUBE_HOME_URL)
|
||||
.header("X-YouTube-Client-Name", "1")
|
||||
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
|
||||
ClientType::Tv => self
|
||||
.client
|
||||
.inner
|
||||
.http
|
||||
.post(format!(
|
||||
"{YOUTUBEI_V1_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
|
||||
))
|
||||
.header(header::ORIGIN, YOUTUBE_HOME_URL)
|
||||
.header(header::REFERER, YOUTUBE_TV_URL)
|
||||
.header("X-YouTube-Client-Name", "7")
|
||||
.header("X-YouTube-Client-Version", TV_CLIENT_VERSION),
|
||||
ClientType::Android => self
|
||||
.client
|
||||
.inner
|
||||
|
|
|
@ -13,8 +13,8 @@ use crate::{
|
|||
deobfuscate::Deobfuscator,
|
||||
error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason},
|
||||
model::{
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset,
|
||||
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, Frameset, Subtitle,
|
||||
VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
|
||||
},
|
||||
util,
|
||||
};
|
||||
|
@ -265,10 +265,8 @@ impl MapResponse<VideoPlayer> for response::Player {
|
|||
description: video_details.short_description,
|
||||
duration: video_details.length_seconds,
|
||||
thumbnail: video_details.thumbnail.into(),
|
||||
channel: ChannelId {
|
||||
id: video_details.channel_id,
|
||||
name: video_details.author,
|
||||
},
|
||||
channel_id: video_details.channel_id,
|
||||
channel_name: video_details.author,
|
||||
view_count: video_details.view_count,
|
||||
keywords: video_details.keywords,
|
||||
is_live,
|
||||
|
@ -737,6 +735,7 @@ mod tests {
|
|||
#[rstest]
|
||||
#[case::desktop(ClientType::Desktop)]
|
||||
#[case::desktop_music(ClientType::DesktopMusic)]
|
||||
#[case::tv(ClientType::Tv)]
|
||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||
#[case::android(ClientType::Android)]
|
||||
#[case::ios(ClientType::Ios)]
|
||||
|
|
|
@ -236,7 +236,7 @@ pub(crate) struct CaptionTrack {
|
|||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct VideoDetails {
|
||||
pub video_id: String,
|
||||
pub title: String,
|
||||
pub title: Option<String>,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub length_seconds: u32,
|
||||
#[serde(default)]
|
||||
|
@ -245,9 +245,9 @@ pub(crate) struct VideoDetails {
|
|||
pub short_description: Option<String>,
|
||||
#[serde(default)]
|
||||
pub thumbnail: Thumbnails,
|
||||
#[serde_as(as = "DisplayFromStr")]
|
||||
pub view_count: u64,
|
||||
pub author: String,
|
||||
#[serde_as(as = "Option<DisplayFromStr>")]
|
||||
pub view_count: Option<u64>,
|
||||
pub author: Option<String>,
|
||||
pub is_live_content: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► 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"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -30,11 +30,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► 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"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -35,11 +35,9 @@ VideoPlayer(
|
|||
height: 1080,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting",
|
||||
name: Some("Inspiring Cinematic Uplifting"),
|
||||
description: None,
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -25,11 +25,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "Romansenykmusic",
|
||||
),
|
||||
view_count: 426583,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("Romansenykmusic"),
|
||||
view_count: Some(426583),
|
||||
keywords: [],
|
||||
is_live: false,
|
||||
is_live_content: false,
|
||||
|
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► 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"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -25,11 +25,9 @@ VideoPlayer(
|
|||
height: 480,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
|
|
@ -0,0 +1,518 @@
|
|||
---
|
||||
source: src/client/player.rs
|
||||
expression: map_res.c
|
||||
---
|
||||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: None,
|
||||
description: None,
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
Thumbnail(
|
||||
url: "https://i.ytimg.com/vi/pPvd8UxmSbQ/hqdefault.jpg",
|
||||
width: 480,
|
||||
height: 360,
|
||||
),
|
||||
],
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: None,
|
||||
view_count: None,
|
||||
keywords: [],
|
||||
is_live: false,
|
||||
is_live_content: false,
|
||||
),
|
||||
video_streams: [
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IT4iUCpJNJWUitTMgIi6njuKSsi3MNed1Szyf0qysTX0v1Nf6AyCvjIGbek5Fn50kuBrGtRJ5q&c=TVHTML5&clen=10262148&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=18&lmt=1700885551970466&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=BMzwItzIOB1HhmG&ns=YmgbZhlLp0C-9ilsQWGAyUAQ&pl=26&ratebypass=yes&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgUah4qH8RqPzmo75ExCWSiRYlUlsAk0v9gl638LitVNICICxFs5lK3CsmOAja0bsXavXkyykzpdhHZKGXOZQYT1f8&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cratebypass%2Cdur%2Clmt&svpuc=1&txp=1318224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 18,
|
||||
bitrate: 503574,
|
||||
average_bitrate: 503367,
|
||||
size: Some(10262148),
|
||||
index_range: None,
|
||||
init_range: None,
|
||||
duration_ms: Some(163096),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
],
|
||||
video_only_streams: [
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2273274&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=160&keepalive=yes&lmt=1705967288821438&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgb8eXnQ6MSJ3PuvFVBdYIWTnFobH8mTC9zbZpBNxLbBYCICkPLKEm3gNbW5HIFXs7bwF5rSqUKHHnXNK91qMslQog&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 160,
|
||||
bitrate: 114816,
|
||||
average_bitrate: 111551,
|
||||
size: Some(2273274),
|
||||
index_range: Some(Range(
|
||||
start: 738,
|
||||
end: 1165,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 737,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 256,
|
||||
height: 144,
|
||||
fps: 30,
|
||||
quality: "144p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d400c\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1151892&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=278&keepalive=yes&lmt=1705966620402771&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAP4IybR7cZRpx7IX1ke6UIu_hdFZN3LOuHBDywg_xv5WAiB8_XEx8VhT9OlFxmM-cY0fl6-7GT9uj3clMIPDk2w7cA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 278,
|
||||
bitrate: 70630,
|
||||
average_bitrate: 56524,
|
||||
size: Some(1151892),
|
||||
index_range: Some(Range(
|
||||
start: 218,
|
||||
end: 767,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 217,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 256,
|
||||
height: 144,
|
||||
fps: 30,
|
||||
quality: "144p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=5026513&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=133&keepalive=yes&lmt=1705967298859029&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgPF0ms4OEe15BTjOFVCkvf52UeTUf0b62_pavCfEyGjcCIH-0AoxzyT8iioWFFaX7iYjqzzaUTpo8rgAPQ0uX8DJa&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 133,
|
||||
bitrate: 257417,
|
||||
average_bitrate: 246656,
|
||||
size: Some(5026513),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 426,
|
||||
height: 240,
|
||||
fps: 30,
|
||||
quality: "240p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d4015\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2541351&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=242&keepalive=yes&lmt=1705966614837727&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgKj1JyMGwYtf16zLJsmbnizz5_v3jaZSa7-j-ls8-qzECIQDKUd50iIc52h7zOX50Hf1SkbV9h-hP4QHs-wkik1fk6Q%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 242,
|
||||
bitrate: 149589,
|
||||
average_bitrate: 124706,
|
||||
size: Some(2541351),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 768,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 426,
|
||||
height: 240,
|
||||
fps: 30,
|
||||
quality: "240p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7810925&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=134&keepalive=yes&lmt=1705967286812435&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAJ92IgZgdk3_WLsfzJV_ZyrSFSbzpsoJh3DkRKDHbNxzAiEA9UbnVlXQ2S3BUimLmWC5TZQfhIkc-PlLnZ81fL0S5yA%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 134,
|
||||
bitrate: 537902,
|
||||
average_bitrate: 383290,
|
||||
size: Some(7810925),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401e\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=4188954&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=243&keepalive=yes&lmt=1705966624121874&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgSCLGQvdZKNXym0zt7c3Yw_4e0J8-wNxtPagPRRn4dRoCIQCOj0IzalNG4EcowBIyK2LC6NLFDr8Zt6sNVkqPjw6lGg%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 243,
|
||||
bitrate: 248858,
|
||||
average_bitrate: 205556,
|
||||
size: Some(4188954),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 640,
|
||||
height: 360,
|
||||
fps: 30,
|
||||
quality: "360p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723538&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=135&keepalive=yes&lmt=1705967282545273&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM843wAa1e7Gc1S69gfXckm7hdgIKPXp0bUSh3hO6W5zAiEA-DDEPGsZBmF5N8VbPy75dhy3rLpE1F18KtWgmrUm2Pg%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 135,
|
||||
bitrate: 978945,
|
||||
average_bitrate: 722499,
|
||||
size: Some(14723538),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 854,
|
||||
height: 480,
|
||||
fps: 30,
|
||||
quality: "480p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401f\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=7788899&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=244&keepalive=yes&lmt=1705966622098793&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAKGyn799bfkVHYE195sPmD60dCMppqJrBM0O-sjgYTzzAiAoBjkNAtL90sXw2YP9UTW9JrMhPSvPiBI_KiCVMJAkFQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 244,
|
||||
bitrate: 467884,
|
||||
average_bitrate: 382209,
|
||||
size: Some(7788899),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 854,
|
||||
height: 480,
|
||||
fps: 30,
|
||||
quality: "480p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=24616305&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=136&keepalive=yes&lmt=1705967307531372&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAM57L2Utesn4xVyT0HSwR9Khv_S-efx4uFAbCPkZFoRXAiEAtIu63-jF2_FZkOMmZAqGU3SRU9QgxoajRjBhMFwcOuk%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 136,
|
||||
bitrate: 1560439,
|
||||
average_bitrate: 1207947,
|
||||
size: Some(24616305),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
quality: "720p",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d401f\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=34544823&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=298&keepalive=yes&lmt=1705967092637061&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAIIGU41JunuODw9qIlSoYQcwkCYO6k9XOVlDn1Nxqnu7AiEAoiMOgYU8s8lp01fW0L86hHrSrtlvOLSI9XA50iyIGBc%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 298,
|
||||
bitrate: 2188961,
|
||||
average_bitrate: 1694973,
|
||||
size: Some(34544823),
|
||||
index_range: Some(Range(
|
||||
start: 739,
|
||||
end: 1166,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 738,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
quality: "720p60",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.4d4020\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=14723992&dur=163.029&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=247&keepalive=yes&lmt=1705966613897741&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRgIhAL-upITxk7r9FQL5F4WL0A6SjPw673qyyzmXIC48eKfTAiEAlkdkx7IFYtehbhKakbffvIebpPXRtxSgBWLl7WEHCrE%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 247,
|
||||
bitrate: 929607,
|
||||
average_bitrate: 722521,
|
||||
size: Some(14723992),
|
||||
index_range: Some(Range(
|
||||
start: 220,
|
||||
end: 770,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 219,
|
||||
)),
|
||||
duration_ms: Some(163029),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 30,
|
||||
quality: "720p",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=30205331&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=302&keepalive=yes&lmt=1705966545733919&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAL428Az_BKxxff4FlH4WleHSy4Igq3mR71NuTMOc9xU3AiBN4lXfH9DklGaQUMnOT8wAhiMuzR73bW3cwr744TSoNA%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 302,
|
||||
bitrate: 2250391,
|
||||
average_bitrate: 1482051,
|
||||
size: Some(30205331),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 786,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
quality: "720p60",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=62057888&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=299&keepalive=yes&lmt=1705967093743693&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgBEemc0Cvd3KhNooNRblgX64_fjNSP30RmWDfFwDR7qYCIQCXpQ9FO0_X93ZHcyvRZCKX5gbJuusCReaRcJbRLFsM_g%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 299,
|
||||
bitrate: 3926810,
|
||||
average_bitrate: 3044926,
|
||||
size: Some(62057888),
|
||||
index_range: Some(Range(
|
||||
start: 740,
|
||||
end: 1167,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 739,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
quality: "1080p60",
|
||||
hdr: false,
|
||||
mime: "video/mp4; codecs=\"avc1.64002a\"",
|
||||
format: mp4,
|
||||
codec: avc1,
|
||||
),
|
||||
VideoStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?aitags=133%2C134%2C135%2C136%2C160%2C242%2C243%2C244%2C247%2C278%2C298%2C299%2C302%2C303&bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=55300085&dur=163.046&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=303&keepalive=yes&lmt=1705966651743358&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgTZlmOcsLYJ_a9SnVLehXnaoajtreQO97qawEIDPEi8sCIQDKFdtBWWMuQUb9X8H-x92B3q-y0g8TvAPanR95cfklXQ%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Caitags%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=130F224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 303,
|
||||
bitrate: 3473307,
|
||||
average_bitrate: 2713348,
|
||||
size: Some(55300085),
|
||||
index_range: Some(Range(
|
||||
start: 219,
|
||||
end: 792,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 218,
|
||||
)),
|
||||
duration_ms: Some(163046),
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
quality: "1080p60",
|
||||
hdr: false,
|
||||
mime: "video/webm; codecs=\"vp9\"",
|
||||
format: webm,
|
||||
codec: vp9,
|
||||
),
|
||||
],
|
||||
audio_streams: [
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=934750&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=249&keepalive=yes&lmt=1714877357172339&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIhAItfaWkRs94vqyae7GR4M1xHoQO2lduvNRFugRSf0h-IAiA9fdLOJMwPI8vAO2C13igyv2qGSpOlKQptS4sN6p5Ffw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 249,
|
||||
bitrate: 53073,
|
||||
average_bitrate: 45860,
|
||||
size: 934750,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=1245582&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=250&keepalive=yes&lmt=1714877466693058&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgdJ1SjWwaloQecEblSIMFp2qFmpG_kKYZP1vX_M55dE0CIQCDSfa_FsaiFRcNL-1LRTgCIRSO7dj5vrpKR1Ya-KbmMw%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 250,
|
||||
bitrate: 71197,
|
||||
average_bitrate: 61109,
|
||||
size: 1245582,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2640283&dur=163.096&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=140&keepalive=yes&lmt=1705966477945761&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRAIgSxdbLrbojMVJcyRzsI2TrzOf78LN28bWcsHpbs4QXDwCIHidfXoriWMHfuiktUCdzLuUmksU7r5vITdh6u0puNmx&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 140,
|
||||
bitrate: 130268,
|
||||
average_bitrate: 129508,
|
||||
size: 2640283,
|
||||
index_range: Some(Range(
|
||||
start: 632,
|
||||
end: 867,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 631,
|
||||
)),
|
||||
duration_ms: Some(163096),
|
||||
mime: "audio/mp4; codecs=\"mp4a.40.2\"",
|
||||
format: m4a,
|
||||
codec: mp4a,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.2200003),
|
||||
track: None,
|
||||
),
|
||||
AudioStream(
|
||||
url: "https://rr5---sn-h0jeenek.googlevideo.com/videoplayback?bui=AXc671IvQBUNCtxNiAkj0M-Bvcb-N5cUu1XFk68f4Cnj0sFLEy1sixyW5lThzLYJXioG8kVQ2xT9KNLS&c=TVHTML5&clen=2480393&dur=163.061&ei=viioZtTdKteHi9oPl42KsAg&expire=1722318110&fvip=4&gir=yes&id=o-AC7iotZ_nCvg7C6fK7ofX174GXVOdwW68lsyXLLmCs0h&initcwndbps=1957500&ip=93.235.183.158&itag=251&keepalive=yes&lmt=1714877359450110&lmw=1&lsig=AGtxev0wRgIhANyFV4Ji7jlkXvfkb_czMQDZCiu6AbJ3Kzyv_s9V9WyvAiEA0o8XuM9kyh98hG1yg7h44L3I5OAUXuTpQdjxUaZ1V4A%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=1722295996&mv=m&mvi=5&n=SWvqB0UTkUvifuM&ns=ZR8RwjQ3VJGDvQifdaM1IRMQ&pl=26&requiressl=yes&rqh=1&sefc=1&sig=AJfQdSswRQIgO0jG-x2l6AF7tjryIX_oM3np78WgNDiseezppLfbQrgCIQCVLdpDhclKc8vQgWGzKXcqsAxgNl5S3MlLT8u1Jeok2A%3D%3D&source=youtube&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cxpc%2Cbui%2Cvprv%2Csvpuc%2Cmime%2Cns%2Crqh%2Cgir%2Cclen%2Cdur%2Clmt&svpuc=1&txp=1308224&vprv=1&xpc=EgVo2aDSNQ%3D%3D",
|
||||
itag: 251,
|
||||
bitrate: 140833,
|
||||
average_bitrate: 121691,
|
||||
size: 2480393,
|
||||
index_range: Some(Range(
|
||||
start: 266,
|
||||
end: 551,
|
||||
)),
|
||||
init_range: Some(Range(
|
||||
start: 0,
|
||||
end: 265,
|
||||
)),
|
||||
duration_ms: Some(163061),
|
||||
mime: "audio/webm; codecs=\"opus\"",
|
||||
format: webm,
|
||||
codec: opus,
|
||||
channels: Some(2),
|
||||
loudness_db: Some(5.21),
|
||||
track: None,
|
||||
),
|
||||
],
|
||||
subtitles: [
|
||||
Subtitle(
|
||||
url: "https://www.youtube.com/api/timedtext?v=pPvd8UxmSbQ&ei=viioZtTdKteHi9oPl42KsAg&caps=asr&opi=112496729&exp=xbt&xoaf=5&hl=en&ip=0.0.0.0&ipbits=0&expire=1722321710&sparams=ip,ipbits,expire,v,ei,caps,opi,exp,xoaf&signature=7B002D0C2B79781E0E46F374D5BB53C6059A5252.E7B05ECC8D799DB96F3C21B727A0161E0032CDFA&key=yt8&lang=en",
|
||||
lang: "en",
|
||||
lang_name: "English",
|
||||
auto_generated: false,
|
||||
),
|
||||
],
|
||||
expires_in_seconds: 21540,
|
||||
hls_manifest_url: None,
|
||||
dash_manifest_url: None,
|
||||
preview_frames: [
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L0/default.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCsCT8Lprh2S0ptmCRsWH7VtDl3YQ",
|
||||
frame_width: 48,
|
||||
frame_height: 27,
|
||||
page_count: 1,
|
||||
total_count: 100,
|
||||
duration_per_frame: 0,
|
||||
frames_per_page_x: 10,
|
||||
frames_per_page_y: 10,
|
||||
),
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L1/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLBXrdgfuYV1WLnTGXqZtSAUm8oZCA",
|
||||
frame_width: 80,
|
||||
frame_height: 45,
|
||||
page_count: 1,
|
||||
total_count: 83,
|
||||
duration_per_frame: 2000,
|
||||
frames_per_page_x: 10,
|
||||
frames_per_page_y: 10,
|
||||
),
|
||||
Frameset(
|
||||
url_template: "https://i.ytimg.com/sb/pPvd8UxmSbQ/storyboard3_L2/M$M.jpg?sqp=-oaymwGbA0g48quKqQOSA4gBAZUBAAAEQpgBMqABPKgBBLABELABDbABDLABELABFbABH7ABJrABLbABDrABDrABD7ABErABF7ABK7ABLLABKbABD7ABDrABELABFbABH7ABKrABMrABKbABD7ABEbABFLABGLABJrABPbABOLABLbABEbABFLABHrABKrABMbABS7ABR7ABNrABFbABHLABKbABLrABObABR7ABTbABP7ABJbABLrABN7ABPbABR7ABUrABUbABRbABM7ABQLABQrABQ7ABTLABRLABRrABQ7gBEbgBEbgBFbgBI7gBRLgBQ7gBQ7gBQ7gBEbgBE7gBFrgBL7gBQ7gBQ7gBQ7gBQ7gBFbgBFrgBKbgBQ7gBQ7gBQ7gBQ7gBQ7gBI7gBL7gBQ7gBQ7gBQ7gBQ7gBQ7gBQ7gBRLgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQ7gBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQ7gBQ7gBQ7gBQrgBQrgBQrgBQrgBQqLzl_8DBgjf8LPxBQ==&sigh=rs$AOn4CLCRazj84zMuwJLaCCc_PiUakX_YdQ",
|
||||
frame_width: 160,
|
||||
frame_height: 90,
|
||||
page_count: 4,
|
||||
total_count: 83,
|
||||
duration_per_frame: 2000,
|
||||
frames_per_page_x: 5,
|
||||
frames_per_page_y: 5,
|
||||
),
|
||||
],
|
||||
client_type: tv,
|
||||
visitor_data: Some("CgtrbXRsWU4wUEtXbyi-0aC1BjIKCgJERRIEEgAgZg%3D%3D"),
|
||||
)
|
|
@ -5,7 +5,7 @@ expression: map_res.c
|
|||
VideoPlayer(
|
||||
details: VideoPlayerDetails(
|
||||
id: "pPvd8UxmSbQ",
|
||||
name: "Inspiring Cinematic Uplifting (Creative Commons)",
|
||||
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
|
||||
description: Some("► 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"),
|
||||
duration: 163,
|
||||
thumbnail: [
|
||||
|
@ -35,11 +35,9 @@ VideoPlayer(
|
|||
height: 1080,
|
||||
),
|
||||
],
|
||||
channel: ChannelId(
|
||||
id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
name: "RomanSenykMusic - Royalty Free Music",
|
||||
),
|
||||
view_count: 426567,
|
||||
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
|
||||
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
|
||||
view_count: Some(426567),
|
||||
keywords: [
|
||||
"no copyright music",
|
||||
"background music",
|
||||
|
|
|
@ -164,6 +164,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
|||
+ &deobfuscate_function
|
||||
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name);
|
||||
verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?;
|
||||
tracing::debug!("successfully extracted sig fn `{dfunc_name}`");
|
||||
|
||||
Ok(js_fn)
|
||||
}
|
||||
|
@ -171,7 +172,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
|
|||
fn get_nsig_fn_names(player_js: &str) -> impl Iterator<Item = String> + '_ {
|
||||
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
// x.get( .. y=functionName[array_num](z) .. x.set(
|
||||
Regex::new(r#"\w\.get\(.+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
|
||||
Regex::new(r#"(?:\w\.get\(|index\.m3u8).+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
|
||||
});
|
||||
|
||||
FUNCTION_NAME_REGEX
|
||||
|
@ -263,15 +264,15 @@ fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
|
|||
.ok_or(DeobfError::Extraction("could not find function base"))?;
|
||||
|
||||
let js_fn = extract_js_fn(&player_js[offset..], name)
|
||||
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, name))?;
|
||||
.map(|s| format!("var {};{}", s, caller_function(DEOBF_NSIG_FUNC_NAME, name)))?;
|
||||
verify_fn(&js_fn, DEOBF_NSIG_FUNC_NAME)?;
|
||||
tracing::info!("Successfully extracted nsig fn `{name}`");
|
||||
tracing::debug!("successfully extracted nsig fn `{name}`");
|
||||
Ok(js_fn)
|
||||
};
|
||||
|
||||
util::find_map_or_last_err(
|
||||
get_nsig_fn_names(player_js),
|
||||
DeobfError::Extraction("no nsig fn name found"),
|
||||
DeobfError::Extraction("nsig function name"),
|
||||
|name| {
|
||||
extract_fn(&name).map_err(|e| {
|
||||
tracing::warn!("Failed to extract nsig fn `{name}`: {e}");
|
||||
|
@ -330,7 +331,7 @@ mod tests {
|
|||
});
|
||||
|
||||
const SIG_DEOBF_FUNC: &str = r#"var qB={w8:function(a){a.reverse()},EC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},Np:function(a,b){a.splice(0,b)}};var Rva=function(a){a=a.split("");qB.Np(a,3);qB.w8(a,41);qB.EC(a,55);qB.Np(a,3);qB.w8(a,33);qB.Np(a,3);qB.EC(a,48);qB.EC(a,17);qB.EC(a,43);return a.join("")};var deobf_sig=Rva;"#;
|
||||
const NSIG_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[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(""))},
|
||||
const NSIG_DEOBF_FUNC: &str = r#"var Vo=function(a){var b=a.split(""),c=[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(""))},
|
||||
928409064,-595856984,1403221911,653089124,-168714481,-1883008765,158931990,1346921902,361518508,1403221911,-362174697,-233641452,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
|
||||
b,158931990,791141857,-907319795,-1776185924,1595027902,-829736173,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},
|
||||
-1274951142,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
|
||||
|
@ -436,6 +437,6 @@ 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
|
|||
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
|
||||
assert!(deobf_sig.len() >= 100);
|
||||
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
|
||||
assert!(deobf_nsig.len() >= 10);
|
||||
assert!(deobf_nsig.len() >= 6);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,7 +156,7 @@ pub struct VideoPlayerDetails {
|
|||
/// Unique YouTube video ID
|
||||
pub id: String,
|
||||
/// Video title
|
||||
pub name: String,
|
||||
pub name: Option<String>,
|
||||
/// Video description in plaintext format
|
||||
pub description: Option<String>,
|
||||
/// Video duration in seconds
|
||||
|
@ -165,10 +165,12 @@ pub struct VideoPlayerDetails {
|
|||
pub duration: u32,
|
||||
/// Video thumbnail
|
||||
pub thumbnail: Vec<Thumbnail>,
|
||||
/// Channel of the video
|
||||
pub channel: ChannelId,
|
||||
/// Channel ID of the video
|
||||
pub channel_id: String,
|
||||
/// Channel name of the video
|
||||
pub channel_name: Option<String>,
|
||||
/// Number of views / current viewers in case of a livestream.
|
||||
pub view_count: u64,
|
||||
pub view_count: Option<u64>,
|
||||
/// List of words that describe the topic of the video
|
||||
pub keywords: Vec<String>,
|
||||
/// True if the video is an active livestream
|
||||
|
@ -632,7 +634,6 @@ pub struct ChannelTag {
|
|||
#[derive(
|
||||
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
pub enum Verification {
|
||||
#[default]
|
||||
/// Unverified channel (default)
|
||||
|
|
|
@ -227,30 +227,16 @@ macro_rules! yt_entity_owner_music {
|
|||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
self.artists.first().map(|a| a.name.as_str())
|
||||
if self.by_va {
|
||||
Some(crate::util::VARIOUS_ARTISTS)
|
||||
} else {
|
||||
self.artists.first().map(|a| a.name.as_str())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl YtEntity for VideoPlayer {
|
||||
fn id(&self) -> &str {
|
||||
&self.details.id
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
Some(&self.details.channel.id)
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
Some(&self.details.channel.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> YtEntity for Channel<T> {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
|
@ -269,7 +255,78 @@ impl<T> YtEntity for Channel<T> {
|
|||
}
|
||||
}
|
||||
|
||||
yt_entity_owner! {VideoPlayerDetails}
|
||||
impl YtEntity for YouTubeItem {
|
||||
fn id(&self) -> &str {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => &v.id,
|
||||
YouTubeItem::Playlist(p) => &p.id,
|
||||
YouTubeItem::Channel(c) => &c.id,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => &v.name,
|
||||
YouTubeItem::Playlist(p) => &p.name,
|
||||
YouTubeItem::Channel(c) => &c.name,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => v.channel_id(),
|
||||
YouTubeItem::Playlist(p) => p.channel_id(),
|
||||
YouTubeItem::Channel(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
YouTubeItem::Video(v) => v.channel_name(),
|
||||
YouTubeItem::Playlist(p) => p.channel_name(),
|
||||
YouTubeItem::Channel(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl YtEntity for MusicItem {
|
||||
fn id(&self) -> &str {
|
||||
match self {
|
||||
MusicItem::Track(t) => &t.id,
|
||||
MusicItem::Album(b) => &b.id,
|
||||
MusicItem::Artist(a) => &a.id,
|
||||
MusicItem::Playlist(p) => &p.id,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
match self {
|
||||
MusicItem::Track(t) => &t.name,
|
||||
MusicItem::Album(b) => &b.name,
|
||||
MusicItem::Artist(a) => &a.name,
|
||||
MusicItem::Playlist(p) => &p.name,
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_id(&self) -> Option<&str> {
|
||||
match self {
|
||||
MusicItem::Track(t) => t.channel_id(),
|
||||
MusicItem::Album(b) => b.channel_id(),
|
||||
MusicItem::Artist(_) => None,
|
||||
MusicItem::Playlist(p) => p.channel_id(),
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_name(&self) -> Option<&str> {
|
||||
match self {
|
||||
MusicItem::Track(t) => t.channel_name(),
|
||||
MusicItem::Album(b) => b.channel_name(),
|
||||
MusicItem::Artist(_) => None,
|
||||
MusicItem::Playlist(p) => p.channel_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yt_entity_owner_opt! {Playlist}
|
||||
yt_entity! {ChannelId}
|
||||
yt_entity_owner! {VideoDetails}
|
||||
|
|
3150
testfiles/player/tv_video.json
Normal file
3150
testfiles/player/tv_video.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -31,7 +31,8 @@
|
|||
"height": 1080
|
||||
}
|
||||
],
|
||||
"channel": { "id": "UCYq-iAOSZBvoUxvfzwKIZWA", "name": "Jacob + Katie Schwarz" },
|
||||
"channel_id": "UCYq-iAOSZBvoUxvfzwKIZWA",
|
||||
"channel_name": "Jacob + Katie Schwarz",
|
||||
"view_count": 216221243,
|
||||
"keywords": [
|
||||
"4K",
|
||||
|
|
|
@ -31,10 +31,8 @@
|
|||
"height": 1080
|
||||
}
|
||||
],
|
||||
"channel": {
|
||||
"id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
|
||||
"name": "MrBeast"
|
||||
},
|
||||
"channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
|
||||
"channel_name": "MrBeast",
|
||||
"view_count": 136908834,
|
||||
"keywords": [],
|
||||
"is_live": false,
|
||||
|
|
|
@ -26,6 +26,7 @@ use rustypipe::validate;
|
|||
|
||||
#[rstest]
|
||||
#[case::desktop(ClientType::Desktop)]
|
||||
#[case::tv(ClientType::Tv)]
|
||||
#[case::tv_html5_embed(ClientType::TvHtml5Embed)]
|
||||
#[case::android(ClientType::Android)]
|
||||
#[case::ios(ClientType::Ios)]
|
||||
|
@ -40,13 +41,26 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
// dbg!(&player_data);
|
||||
|
||||
assert_eq!(player_data.details.id, "n4tK7LYFxI0");
|
||||
assert_eq!(
|
||||
player_data.details.name,
|
||||
"Spektrem - Shine | Progressive House | NCS - Copyright Free Music"
|
||||
);
|
||||
if client_type == ClientType::DesktopMusic {
|
||||
assert!(player_data.details.description.is_none());
|
||||
} else {
|
||||
assert_eq!(player_data.details.duration, 259);
|
||||
assert!(!player_data.details.thumbnail.is_empty());
|
||||
assert_eq!(player_data.details.channel_id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
||||
assert!(!player_data.details.is_live_content);
|
||||
|
||||
// The TV client dows not output most video metadata
|
||||
if client_type != ClientType::Tv {
|
||||
assert_eq!(
|
||||
player_data.details.name.expect("name"),
|
||||
"Spektrem - Shine | Progressive House | NCS - Copyright Free Music"
|
||||
);
|
||||
assert_eq!(
|
||||
player_data.details.channel_name.expect("channel name"),
|
||||
"NoCopyrightSounds"
|
||||
);
|
||||
assert_gte(
|
||||
player_data.details.view_count.expect("view count"),
|
||||
146_818_808,
|
||||
"view count",
|
||||
);
|
||||
assert!(player_data
|
||||
.details
|
||||
.description
|
||||
|
@ -54,15 +68,10 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
|
|||
.contains(
|
||||
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music"
|
||||
));
|
||||
assert_eq!(player_data.details.keywords[0], "spektrem");
|
||||
}
|
||||
assert_eq!(player_data.details.duration, 259);
|
||||
assert!(!player_data.details.thumbnail.is_empty());
|
||||
assert_eq!(player_data.details.channel.id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
|
||||
assert_eq!(player_data.details.channel.name, "NoCopyrightSounds");
|
||||
assert_gte(player_data.details.view_count, 146_818_808, "view count");
|
||||
assert_eq!(player_data.details.keywords[0], "spektrem");
|
||||
assert!(!player_data.details.is_live_content);
|
||||
|
||||
// Ios uses different A/V formats
|
||||
if client_type == ClientType::Ios {
|
||||
let video = player_data
|
||||
.video_only_streams
|
||||
|
@ -237,13 +246,13 @@ async fn get_player(
|
|||
let details = player_data.details;
|
||||
|
||||
assert_eq!(details.id, id);
|
||||
assert_eq!(details.name, name);
|
||||
assert_eq!(details.name.expect("name"), name);
|
||||
let desc = details.description.expect("description");
|
||||
assert!(desc.contains(description), "description: {desc}");
|
||||
assert_eq!(details.duration, duration);
|
||||
assert_eq!(details.channel.id, channel_id);
|
||||
assert_eq!(details.channel.name, channel_name);
|
||||
assert_gte(details.view_count, views, "views");
|
||||
assert_eq!(details.channel_id, channel_id);
|
||||
assert_eq!(details.channel_name.expect("channel name"), channel_name);
|
||||
assert_gte(details.view_count.expect("view count"), views, "views");
|
||||
assert_eq!(details.is_live, is_live);
|
||||
assert_eq!(details.is_live_content, is_live_content);
|
||||
|
||||
|
|
Loading…
Reference in a new issue