Compare commits

..

11 commits

Author SHA1 Message Date
d36ba595da
fix: extraction error message
Some checks failed
CI / Test (push) Failing after 7m7s
2024-08-08 15:10:55 +02:00
e8324cf3b0
fix: use anstream + owo-color for colorful CLI output
the color-print crate works very well, but it cannot disable styling if the terminal does not support it,
when saving the output to a file, etc
2024-08-08 15:04:15 +02:00
d053ac3eba
fix: make Verification enum exhaustive 2024-08-08 14:56:39 +02:00
91b020efd4
feat: add plaintext output to CLI 2024-08-08 03:22:51 +02:00
114a86a382
feat: add YtEntity trait to YouTubeItem and MusicItem 2024-08-08 03:22:04 +02:00
97fb0578b5
feat: add audiotag+indicatif features to downloader 2024-08-06 14:04:03 +02:00
c6bd03fb70
fix: add var to deobf fn assignment 2024-08-06 14:01:38 +02:00
e1e4fb29c1
feat: downloader: add download_track fn, improve path templates 2024-08-01 03:11:54 +02:00
3c83e11e75
fix: nsig fn extraction 2024-07-31 21:46:32 +02:00
1e1315a837
feat: downloader: add audio tagging 2024-07-31 03:27:27 +02:00
e608811e5f
feat!: add TV client 2024-07-30 01:55:24 +02:00
29 changed files with 4994 additions and 324 deletions

View file

@ -17,7 +17,7 @@ jobs:
cache-on-failure: "true" cache-on-failure: "true"
- name: 📎 Clippy - name: 📎 Clippy
run: cargo clippy --all --features=rss -- -D warnings run: cargo clippy --all --tests --features=rss,indicatif,audiotag -- -D warnings
- name: 🧪 Test - name: 🧪 Test
run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace run: cargo nextest run --config-file ~/.config/nextest.toml --profile ci --retries 2 --features rss --workspace

View file

@ -10,4 +10,4 @@ repos:
hooks: hooks:
- id: cargo-fmt - id: cargo-fmt
- id: cargo-clippy - id: cargo-clippy
args: ["--all", "--tests", "--features=rss", "--", "-D", "warnings"] args: ["--all", "--tests", "--features=rss,indicatif,audiotag", "--", "-D", "warnings"]

View file

@ -1,3 +1,3 @@
{ {
"rust-analyzer.cargo.features": ["rss"] "rust-analyzer.cargo.features": ["rss", "indicatif", "audiotag"]
} }

View file

@ -74,7 +74,10 @@ tracing-test = "0.2.5"
# Included crates # Included crates
rustypipe = { path = ".", version = "0.2.0", default-features = false } rustypipe = { path = ".", version = "0.2.0", default-features = false }
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] [features]
default = ["default-tls"] default = ["default-tls"]

View file

@ -1,5 +1,9 @@
# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) # ![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 Rust client for the public YouTube / YouTube Music API (Innertube), inspired by
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).

View file

@ -56,3 +56,6 @@ tracing.workspace = true
tracing-subscriber.workspace = true tracing-subscriber.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
dirs.workspace = true dirs.workspace = true
anstream = "0.6.15"
owo-colors = "4.0.0"

1
cli/README.md Normal file
View file

@ -0,0 +1 @@
# RustyPipe CLI

View file

@ -1,16 +1,23 @@
#![doc = include_str!("../README.md")]
#![warn(clippy::todo, clippy::dbg_macro)] #![warn(clippy::todo, clippy::dbg_macro)]
use std::{path::PathBuf, str::FromStr}; use std::{path::PathBuf, str::FromStr, time::Duration};
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use futures::stream::{self, StreamExt}; use futures::stream::{self, StreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
use rustypipe::{ use rustypipe::{
client::{ClientType, 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}, param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
}; };
use rustypipe_downloader::{DownloadQuery, DownloaderBuilder}; use rustypipe_downloader::{
DownloadError, DownloadQuery, DownloadVideo, Downloader, DownloaderBuilder,
};
use serde::Serialize; use serde::Serialize;
use tracing::level_filters::LevelFilter; use tracing::level_filters::LevelFilter;
use tracing_subscriber::{fmt::MakeWriter, EnvFilter}; use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
@ -81,6 +88,9 @@ enum Commands {
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only. /// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
#[clap(short, long)] #[clap(short, long)]
resolution: Option<u32>, resolution: Option<u32>,
/// Download only the audio track
#[clap(long)]
audio: bool,
/// Number of videos downloaded in parallel /// Number of videos downloaded in parallel
#[clap(short, long, default_value_t = 8)] #[clap(short, long, default_value_t = 8)]
parallel: usize, parallel: usize,
@ -90,8 +100,9 @@ enum Commands {
/// Limit the number of videos to download /// Limit the number of videos to download
#[clap(long, default_value_t = 1000)] #[clap(long, default_value_t = 1000)]
limit: usize, limit: usize,
/// YT Client used to fetch player data
#[clap(long)] #[clap(long)]
player_type: Option<PlayerType>, client_type: Option<PlayerType>,
}, },
/// Extract video, playlist, album or channel data /// Extract video, playlist, album or channel data
Get { Get {
@ -103,6 +114,9 @@ enum Commands {
/// Pretty-print output /// Pretty-print output
#[clap(long)] #[clap(long)]
pretty: bool, pretty: bool,
/// Output as text
#[clap(long)]
txt: bool,
/// Limit the number of items to fetch /// Limit the number of items to fetch
#[clap(long, default_value_t = 20)] #[clap(long, default_value_t = 20)]
limit: usize, limit: usize,
@ -115,14 +129,15 @@ enum Commands {
/// Get comments /// Get comments
#[clap(long)] #[clap(long)]
comments: Option<CommentsOrder>, comments: Option<CommentsOrder>,
/// Get lyrics /// Get lyrics for YTM tracks
#[clap(long)] #[clap(long)]
lyrics: bool, lyrics: bool,
/// Get the player /// Get the player data instead of the video details
#[clap(long)] #[clap(long)]
player: bool, player: bool,
/// YT Client used to fetch player data
#[clap(long)] #[clap(long)]
player_type: Option<PlayerType>, client_type: Option<PlayerType>,
}, },
/// Search YouTube /// Search YouTube
Search { Search {
@ -134,6 +149,9 @@ enum Commands {
/// Pretty-print output /// Pretty-print output
#[clap(long)] #[clap(long)]
pretty: bool, pretty: bool,
/// Output as text
#[clap(long)]
txt: bool,
/// Limit the number of items to fetch /// Limit the number of items to fetch
#[clap(long, default_value_t = 20)] #[clap(long, default_value_t = 20)]
limit: usize, limit: usize,
@ -165,7 +183,7 @@ enum Format {
Yaml, Yaml,
} }
#[derive(Copy, Clone, ValueEnum)] #[derive(Debug, Copy, Clone, ValueEnum)]
enum ChannelTab { enum ChannelTab {
Videos, Videos,
Shorts, Shorts,
@ -236,6 +254,7 @@ enum MusicSearchCategory {
enum PlayerType { enum PlayerType {
Desktop, Desktop,
Tv, Tv,
TvEmbed,
Android, Android,
Ios, Ios,
} }
@ -286,7 +305,8 @@ impl From<PlayerType> for ClientType {
fn from(value: PlayerType) -> Self { fn from(value: PlayerType) -> Self {
match value { match value {
PlayerType::Desktop => Self::Desktop, PlayerType::Desktop => Self::Desktop,
PlayerType::Tv => Self::TvHtml5Embed, PlayerType::TvEmbed => Self::TvHtml5Embed,
PlayerType::Tv => Self::Tv,
PlayerType::Android => Self::Android, PlayerType::Android => Self::Android,
PlayerType::Ios => Self::Ios, PlayerType::Ios => Self::Ios,
} }
@ -307,30 +327,99 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
}; };
} }
async fn download_video( fn print_entities(items: &[impl YtEntity]) {
rp: &RustyPipe, for e in items {
id: &str, anstream::print!("[{}] {}", e.id(), e.name().bold());
target: &DownloadTarget, if let Some(n) = e.channel_name() {
resolution: Option<u32>, anstream::print!(" - {}", n.cyan());
player_type: Option<PlayerType>, }
multi: MultiProgress, println!();
) { }
let mut filter = StreamFilter::new(); }
if let Some(res) = resolution {
if res == 0 { fn print_tracks(tracks: &[TrackItem]) {
filter = filter.no_video(); for t in tracks {
} else { if let Some(n) = t.track_nr {
filter = filter.video_max_res(res); 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) fn print_duration(duration: Option<u32>) {
.progress_bar(multi) if let Some(d) = duration {
.build(); print!(" ");
let mut q = target.apply(dl.download_id(id)); let hours = d / 3600;
if let Some(player_type) = player_type { let minutes = (d / 60) % 60;
q = q.player_type(player_type.into()); 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; let res = q.download().await;
if let Err(e) = res { if let Err(e) = res {
@ -339,29 +428,13 @@ async fn download_video(
} }
async fn download_videos( async fn download_videos(
rp: &RustyPipe, dl: &Downloader,
videos: &[VideoId], videos: Vec<DownloadVideo>,
target: &DownloadTarget, target: &DownloadTarget,
resolution: Option<u32>,
parallel: usize, parallel: usize,
player_type: Option<PlayerType>, client_type: Option<PlayerType>,
multi: MultiProgress, 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 // Indicatif setup
let main = multi.add(ProgressBar::new( let main = multi.add(ProgressBar::new(
videos.len().try_into().unwrap_or_default(), videos.len().try_into().unwrap_or_default(),
@ -379,19 +452,20 @@ async fn download_videos(
.for_each_concurrent(parallel, |video| { .for_each_concurrent(parallel, |video| {
let dl = dl.clone(); let dl = dl.clone();
let main = main.clone(); let main = main.clone();
let id = &video.id; let id = video.id().to_owned();
let mut q = target.apply(dl.download_entity(video)); let mut q = target.apply(dl.video(video));
if let Some(player_type) = player_type { if let Some(client_type) = client_type {
q = q.player_type(player_type.into()); q = q.client_type(client_type.into());
} }
async move { async move {
if let Err(e) = q.download().await { if let Err(e) = q.download().await {
tracing::error!("[{id}]: {e}"); if !matches!(e, DownloadError::Exists(_)) {
} else { tracing::error!("[{id}]: {e}");
main.inc(1); }
} }
main.inc(1);
} }
}) })
.await; .await;
@ -433,7 +507,9 @@ async fn main() {
.with_writer(ProgWriter(multi.clone())) .with_writer(ProgWriter(multi.clone()))
.init(); .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 { if cli.report {
rp = rp.report(); rp = rp.report();
} else { } else {
@ -455,15 +531,35 @@ async fn main() {
id, id,
target, target,
resolution, resolution,
audio,
parallel, parallel,
music, music,
limit, limit,
player_type, client_type,
} => { } => {
let url_target = rp.query().resolve_string(&id, false).await.unwrap(); 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 { match url_target {
UrlTarget::Video { id, .. } => { UrlTarget::Video { id, .. } => {
download_video(&rp, &id, &target, resolution, player_type, multi).await; download_video(&dl, &id, &target, client_type).await;
} }
UrlTarget::Channel { id } => { UrlTarget::Channel { id } => {
target.assert_dir(); target.assert_dir();
@ -473,27 +569,18 @@ async fn main() {
.extend_limit(&rp.query(), limit) .extend_limit(&rp.query(), limit)
.await .await
.unwrap(); .unwrap();
let videos: Vec<VideoId> = channel let videos = channel
.content .content
.items .items
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(VideoId::from) .map(|v| DownloadVideo::from_entity(&v))
.collect(); .collect();
download_videos( download_videos(&dl, videos, &target, parallel, client_type, multi).await;
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
} }
UrlTarget::Playlist { id } => { UrlTarget::Playlist { id } => {
target.assert_dir(); target.assert_dir();
let videos: Vec<VideoId> = if music { let videos = if music {
let mut playlist = rp.query().music_playlist(id).await.unwrap(); let mut playlist = rp.query().music_playlist(id).await.unwrap();
playlist playlist
.tracks .tracks
@ -505,7 +592,7 @@ async fn main() {
.items .items
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(VideoId::from) .map(|v| DownloadVideo::from_track(&v))
.collect() .collect()
} else { } else {
let mut playlist = rp.query().playlist(id).await.unwrap(); let mut playlist = rp.query().playlist(id).await.unwrap();
@ -519,45 +606,28 @@ async fn main() {
.items .items
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(VideoId::from) .map(|v| DownloadVideo::from_entity(&v))
.collect() .collect()
}; };
download_videos( download_videos(&dl, videos, &target, parallel, client_type, multi).await;
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
} }
UrlTarget::Album { id } => { UrlTarget::Album { id } => {
target.assert_dir(); target.assert_dir();
let album = rp.query().music_album(id).await.unwrap(); let album = rp.query().music_album(id).await.unwrap();
let videos: Vec<VideoId> = album let videos = album
.tracks .tracks
.into_iter() .into_iter()
.take(limit) .take(limit)
.map(VideoId::from) .map(|v| DownloadVideo::from_track(&v))
.collect(); .collect();
download_videos( download_videos(&dl, videos, &target, parallel, client_type, multi).await;
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
} }
} }
} }
Commands::Get { Commands::Get {
id, id,
format, format,
txt,
pretty, pretty,
limit, limit,
tab, tab,
@ -565,7 +635,7 @@ async fn main() {
comments, comments,
lyrics, lyrics,
player, player,
player_type, client_type,
} => { } => {
let target = rp.query().resolve_string(&id, false).await.unwrap(); let target = rp.query().resolve_string(&id, false).await.unwrap();
@ -576,16 +646,47 @@ async fn main() {
match details.lyrics_id { match details.lyrics_id {
Some(lyrics_id) => { Some(lyrics_id) => {
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap(); 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"), None => eprintln!("no lyrics found"),
} }
} else if music { } else if music {
let details = rp.query().music_details(&id).await.unwrap(); 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 { } else if player {
let player = if let Some(player_type) = player_type { let player = if let Some(client_type) = client_type {
rp.query().player_from_client(&id, player_type.into()).await rp.query().player_from_client(&id, client_type.into()).await
} else { } else {
rp.query().player(&id).await rp.query().player(&id).await
} }
@ -612,13 +713,129 @@ async fn main() {
None => {} 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 } => { UrlTarget::Channel { id } => {
if music { if music {
let artist = rp.query().music_artist(&id, true).await.unwrap(); 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 { } else {
match tab { match tab {
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => { ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
@ -636,15 +853,77 @@ async fn main() {
.extend_limit(rp.query(), limit) .extend_limit(rp.query(), limit)
.await .await
.unwrap(); .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 => { ChannelTab::Playlists => {
let channel = rp.query().channel_playlists(&id).await.unwrap(); 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 => { ChannelTab::Info => {
let channel = rp.query().channel_info(&id).await.unwrap(); let info = rp.query().channel_info(&id).await.unwrap();
print_data(&channel, format, pretty);
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) .extend_limit(rp.query(), limit)
.await .await
.unwrap(); .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 { } else {
let mut playlist = rp.query().playlist(&id).await.unwrap(); let mut playlist = rp.query().playlist(&id).await.unwrap();
playlist playlist
@ -665,12 +965,59 @@ async fn main() {
.extend_limit(rp.query(), limit) .extend_limit(rp.query(), limit)
.await .await
.unwrap(); .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 } => { UrlTarget::Album { id } => {
let album = rp.query().music_album(&id).await.unwrap(); 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, query,
format, format,
pretty, pretty,
txt,
limit, limit,
item_type, item_type,
length, length,
@ -704,32 +1052,40 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
res.items.extend_limit(rp.query(), limit).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) => { Some(MusicSearchCategory::All) => {
let res = rp.query().music_search_main(&query).await.unwrap(); 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) => { Some(MusicSearchCategory::Tracks) => {
let mut res = rp.query().music_search_tracks(&query).await.unwrap(); let mut res = rp.query().music_search_tracks(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).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) => { Some(MusicSearchCategory::Videos) => {
let mut res = rp.query().music_search_videos(&query).await.unwrap(); let mut res = rp.query().music_search_videos(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).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) => { Some(MusicSearchCategory::Artists) => {
let mut res = rp.query().music_search_artists(&query).await.unwrap(); let mut res = rp.query().music_search_artists(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).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) => { Some(MusicSearchCategory::Albums) => {
let mut res = rp.query().music_search_albums(&query).await.unwrap(); let mut res = rp.query().music_search_albums(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).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) => { Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
let mut res = rp let mut res = rp
@ -741,7 +1097,7 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
res.items.extend_limit(rp.query(), limit).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 => { Commands::Vdata => {

View file

@ -66,9 +66,10 @@ pub async fn download_testfiles() {
music_genre().await; music_genre().await;
} }
const CLIENT_TYPES: [ClientType; 5] = [ const CLIENT_TYPES: [ClientType; 6] = [
ClientType::Desktop, ClientType::Desktop,
ClientType::DesktopMusic, ClientType::DesktopMusic,
ClientType::Tv,
ClientType::TvHtml5Embed, ClientType::TvHtml5Embed,
ClientType::Android, ClientType::Android,
ClientType::Ios, ClientType::Ios,

View file

@ -30,6 +30,8 @@ rustls-tls-native-roots = [
"rustypipe/rustls-tls-native-roots", "rustypipe/rustls-tls-native-roots",
] ]
audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"]
[dependencies] [dependencies]
rustypipe.workspace = true rustypipe.workspace = true
once_cell.workspace = true once_cell.workspace = true
@ -39,6 +41,10 @@ futures.workspace = true
reqwest = { workspace = true, features = ["stream"] } reqwest = { workspace = true, features = ["stream"] }
rand.workspace = true rand.workspace = true
tokio = { workspace = true, features = ["macros", "fs", "process"] } tokio = { workspace = true, features = ["macros", "fs", "process"] }
indicatif.workspace = true indicatif = { workspace = true, optional = true }
filenamify.workspace = true filenamify.workspace = true
tracing.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
View 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

View file

@ -27,11 +27,29 @@ pub enum DownloadError {
/// Download target already exists /// Download target already exists
#[error("file {0} already exists")] #[error("file {0} already exists")]
Exists(PathBuf), Exists(PathBuf),
#[cfg(feature = "audiotag")]
/// Audio tagging error
#[error("Audio tag error: {0}")]
AudioTag(Cow<'static, str>),
/// Other error /// Other error
#[error("error: {0}")] #[error("error: {0}")]
Other(Cow<'static, str>), 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 /// Split an URL into its base string and parameter map
/// ///
/// Example: /// Example:

View file

@ -61,6 +61,8 @@ pub enum ClientType {
/// ///
/// can access age-restricted videos, cannot access non-embeddable videos /// can access age-restricted videos, cannot access non-embeddable videos
TvHtml5Embed, TvHtml5Embed,
/// Client used by youtube.com/tv
Tv,
/// Client used by the Android app /// Client used by the Android app
/// ///
/// no obfuscated stream URLs, includes lower resolution audio streams /// no obfuscated stream URLs, includes lower resolution audio streams
@ -74,7 +76,10 @@ pub enum ClientType {
impl ClientType { impl ClientType {
fn is_web(self) -> bool { fn is_web(self) -> bool {
match self { match self {
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true, ClientType::Desktop
| ClientType::DesktopMusic
| ClientType::TvHtml5Embed
| ClientType::Tv => true,
ClientType::Android | ClientType::Ios => false, 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 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"; 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_MUSIC_V1_URL: &str = "https://music.youtube.com/youtubei/v1/";
const YOUTUBE_HOME_URL: &str = "https://www.youtube.com/"; const YOUTUBE_HOME_URL: &str = "https://www.youtube.com/";
const YOUTUBE_MUSIC_HOME_URL: &str = "https://music.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"; const DISABLE_PRETTY_PRINT_PARAMETER: &str = "prettyPrint=false";
// Desktop client // Desktop client
const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00"; const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00";
const TVHTML5_CLIENT_VERSION: &str = "2.0"; 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"; const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01";
// Mobile client // Mobile client
@ -454,7 +462,7 @@ impl RustyPipeBuilder {
self.build_with_client(ClientBuilder::new()) 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> { pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
let user_agent = self let user_agent = self
.user_agent .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. /// Execute the given http request.
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> { async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
let mut last_resp = None; let mut last_resp = None;
for n in 0..=self.inner.n_http_retries { for n in 0..=self.inner.n_http_retries {
let resp = self let resp = self.inner.http.execute(request.try_clone().unwrap()).await;
.inner
.http
.execute(request.try_clone().unwrap())
.await?;
let status = resp.status(); let err = match resp {
// Immediately return in case of success or unrecoverable status code Ok(resp) => {
if status.is_success() let status = resp.status();
|| (!status.is_server_error() && status != StatusCode::TOO_MANY_REQUESTS) // Immediately return in case of success or unrecoverable status code
{ if status.is_success()
return Ok(resp); || (!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) // Retry in case of a recoverable status code (server err, too many requests)
if n != self.inner.n_http_retries { if n != self.inner.n_http_retries {
@ -749,15 +759,13 @@ impl RustyPipe {
tracing::warn!( tracing::warn!(
"Retry attempt #{}. Error: {}. Waiting {} ms", "Retry attempt #{}. Error: {}. Waiting {} ms",
n + 1, n + 1,
status, err,
ms ms
); );
tokio::time::sleep(Duration::from_millis(ms.into())).await; 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 /// Execute the given http request, returning an error in case of a
@ -1098,6 +1106,7 @@ impl RustyPipeQuery {
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => { ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => {
Cow::Borrowed(&self.client.inner.user_agent) Cow::Borrowed(&self.client.inner.user_agent)
} }
ClientType::Tv => TV_UA.into(),
ClientType::Android => format!( ClientType::Android => format!(
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip", "com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
MOBILE_CLIENT_VERSION, self.opts.country MOBILE_CLIENT_VERSION, self.opts.country
@ -1178,6 +1187,24 @@ impl RustyPipeQuery {
embed_url: YOUTUBE_HOME_URL, 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 { ClientType::Android => YTContext {
client: ClientInfo { client: ClientInfo {
client_name: "ANDROID", client_name: "ANDROID",
@ -1266,6 +1293,17 @@ impl RustyPipeQuery {
.header(header::REFERER, YOUTUBE_HOME_URL) .header(header::REFERER, YOUTUBE_HOME_URL)
.header("X-YouTube-Client-Name", "1") .header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION), .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 ClientType::Android => self
.client .client
.inner .inner

View file

@ -13,8 +13,8 @@ use crate::{
deobfuscate::Deobfuscator, deobfuscate::Deobfuscator,
error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason}, error::{internal::DeobfError, Error, ExtractionError, UnavailabilityReason},
model::{ model::{
traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, ChannelId, Frameset, traits::QualityOrd, AudioCodec, AudioFormat, AudioStream, AudioTrack, Frameset, Subtitle,
Subtitle, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream, VideoCodec, VideoFormat, VideoPlayer, VideoPlayerDetails, VideoStream,
}, },
util, util,
}; };
@ -265,10 +265,8 @@ impl MapResponse<VideoPlayer> for response::Player {
description: video_details.short_description, description: video_details.short_description,
duration: video_details.length_seconds, duration: video_details.length_seconds,
thumbnail: video_details.thumbnail.into(), thumbnail: video_details.thumbnail.into(),
channel: ChannelId { channel_id: video_details.channel_id,
id: video_details.channel_id, channel_name: video_details.author,
name: video_details.author,
},
view_count: video_details.view_count, view_count: video_details.view_count,
keywords: video_details.keywords, keywords: video_details.keywords,
is_live, is_live,
@ -737,6 +735,7 @@ mod tests {
#[rstest] #[rstest]
#[case::desktop(ClientType::Desktop)] #[case::desktop(ClientType::Desktop)]
#[case::desktop_music(ClientType::DesktopMusic)] #[case::desktop_music(ClientType::DesktopMusic)]
#[case::tv(ClientType::Tv)]
#[case::tv_html5_embed(ClientType::TvHtml5Embed)] #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
#[case::android(ClientType::Android)] #[case::android(ClientType::Android)]
#[case::ios(ClientType::Ios)] #[case::ios(ClientType::Ios)]

View file

@ -236,7 +236,7 @@ pub(crate) struct CaptionTrack {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct VideoDetails { pub(crate) struct VideoDetails {
pub video_id: String, pub video_id: String,
pub title: String, pub title: Option<String>,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "DisplayFromStr")]
pub length_seconds: u32, pub length_seconds: u32,
#[serde(default)] #[serde(default)]
@ -245,9 +245,9 @@ pub(crate) struct VideoDetails {
pub short_description: Option<String>, pub short_description: Option<String>,
#[serde(default)] #[serde(default)]
pub thumbnail: Thumbnails, pub thumbnail: Thumbnails,
#[serde_as(as = "DisplayFromStr")] #[serde_as(as = "Option<DisplayFromStr>")]
pub view_count: u64, pub view_count: Option<u64>,
pub author: String, pub author: Option<String>,
pub is_live_content: bool, pub is_live_content: bool,
} }

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer( VideoPlayer(
details: VideoPlayerDetails( details: VideoPlayerDetails(
id: "pPvd8UxmSbQ", 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"), 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, duration: 163,
thumbnail: [ thumbnail: [
@ -30,11 +30,9 @@ VideoPlayer(
height: 480, height: 480,
), ),
], ],
channel: ChannelId( channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
id: "UCbxxEi-ImPlbLx5F-fHetEg", channel_name: Some("RomanSenykMusic - Royalty Free Music"),
name: "RomanSenykMusic - Royalty Free Music", view_count: Some(426567),
),
view_count: 426567,
keywords: [ keywords: [
"no copyright music", "no copyright music",
"background music", "background music",

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer( VideoPlayer(
details: VideoPlayerDetails( details: VideoPlayerDetails(
id: "pPvd8UxmSbQ", 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"), 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, duration: 163,
thumbnail: [ thumbnail: [
@ -35,11 +35,9 @@ VideoPlayer(
height: 1080, height: 1080,
), ),
], ],
channel: ChannelId( channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
id: "UCbxxEi-ImPlbLx5F-fHetEg", channel_name: Some("RomanSenykMusic - Royalty Free Music"),
name: "RomanSenykMusic - Royalty Free Music", view_count: Some(426567),
),
view_count: 426567,
keywords: [ keywords: [
"no copyright music", "no copyright music",
"background music", "background music",

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer( VideoPlayer(
details: VideoPlayerDetails( details: VideoPlayerDetails(
id: "pPvd8UxmSbQ", id: "pPvd8UxmSbQ",
name: "Inspiring Cinematic Uplifting", name: Some("Inspiring Cinematic Uplifting"),
description: None, description: None,
duration: 163, duration: 163,
thumbnail: [ thumbnail: [
@ -25,11 +25,9 @@ VideoPlayer(
height: 480, height: 480,
), ),
], ],
channel: ChannelId( channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
id: "UCbxxEi-ImPlbLx5F-fHetEg", channel_name: Some("Romansenykmusic"),
name: "Romansenykmusic", view_count: Some(426583),
),
view_count: 426583,
keywords: [], keywords: [],
is_live: false, is_live: false,
is_live_content: false, is_live_content: false,

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer( VideoPlayer(
details: VideoPlayerDetails( details: VideoPlayerDetails(
id: "pPvd8UxmSbQ", 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"), 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, duration: 163,
thumbnail: [ thumbnail: [
@ -25,11 +25,9 @@ VideoPlayer(
height: 480, height: 480,
), ),
], ],
channel: ChannelId( channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
id: "UCbxxEi-ImPlbLx5F-fHetEg", channel_name: Some("RomanSenykMusic - Royalty Free Music"),
name: "RomanSenykMusic - Royalty Free Music", view_count: Some(426567),
),
view_count: 426567,
keywords: [ keywords: [
"no copyright music", "no copyright music",
"background music", "background music",

View file

@ -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"),
)

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer( VideoPlayer(
details: VideoPlayerDetails( details: VideoPlayerDetails(
id: "pPvd8UxmSbQ", 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"), 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, duration: 163,
thumbnail: [ thumbnail: [
@ -35,11 +35,9 @@ VideoPlayer(
height: 1080, height: 1080,
), ),
], ],
channel: ChannelId( channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
id: "UCbxxEi-ImPlbLx5F-fHetEg", channel_name: Some("RomanSenykMusic - Royalty Free Music"),
name: "RomanSenykMusic - Royalty Free Music", view_count: Some(426567),
),
view_count: 426567,
keywords: [ keywords: [
"no copyright music", "no copyright music",
"background music", "background music",

View file

@ -164,6 +164,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
+ &deobfuscate_function + &deobfuscate_function
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name); + &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name);
verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?; verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?;
tracing::debug!("successfully extracted sig fn `{dfunc_name}`");
Ok(js_fn) 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> + '_ { fn get_nsig_fn_names(player_js: &str) -> impl Iterator<Item = String> + '_ {
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| { static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
// x.get( .. y=functionName[array_num](z) .. x.set( // 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 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"))?; .ok_or(DeobfError::Extraction("could not find function base"))?;
let js_fn = extract_js_fn(&player_js[offset..], name) 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)?; 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) Ok(js_fn)
}; };
util::find_map_or_last_err( util::find_map_or_last_err(
get_nsig_fn_names(player_js), get_nsig_fn_names(player_js),
DeobfError::Extraction("no nsig fn name found"), DeobfError::Extraction("nsig function name"),
|name| { |name| {
extract_fn(&name).map_err(|e| { extract_fn(&name).map_err(|e| {
tracing::warn!("Failed to extract nsig fn `{name}`: {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 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}, 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])}, 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}, -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(); let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
assert!(deobf_sig.len() >= 100); assert!(deobf_sig.len() >= 100);
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap(); let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
assert!(deobf_nsig.len() >= 10); assert!(deobf_nsig.len() >= 6);
} }
} }

View file

@ -156,7 +156,7 @@ pub struct VideoPlayerDetails {
/// Unique YouTube video ID /// Unique YouTube video ID
pub id: String, pub id: String,
/// Video title /// Video title
pub name: String, pub name: Option<String>,
/// Video description in plaintext format /// Video description in plaintext format
pub description: Option<String>, pub description: Option<String>,
/// Video duration in seconds /// Video duration in seconds
@ -165,10 +165,12 @@ pub struct VideoPlayerDetails {
pub duration: u32, pub duration: u32,
/// Video thumbnail /// Video thumbnail
pub thumbnail: Vec<Thumbnail>, pub thumbnail: Vec<Thumbnail>,
/// Channel of the video /// Channel ID of the video
pub channel: ChannelId, pub channel_id: String,
/// Channel name of the video
pub channel_name: Option<String>,
/// Number of views / current viewers in case of a livestream. /// 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 /// List of words that describe the topic of the video
pub keywords: Vec<String>, pub keywords: Vec<String>,
/// True if the video is an active livestream /// True if the video is an active livestream
@ -632,7 +634,6 @@ pub struct ChannelTag {
#[derive( #[derive(
Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
)] )]
#[non_exhaustive]
pub enum Verification { pub enum Verification {
#[default] #[default]
/// Unverified channel (default) /// Unverified channel (default)

View file

@ -227,30 +227,16 @@ macro_rules! yt_entity_owner_music {
} }
fn channel_name(&self) -> Option<&str> { 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> { impl<T> YtEntity for Channel<T> {
fn id(&self) -> &str { fn id(&self) -> &str {
&self.id &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_owner_opt! {Playlist}
yt_entity! {ChannelId} yt_entity! {ChannelId}
yt_entity_owner! {VideoDetails} yt_entity_owner! {VideoDetails}

File diff suppressed because one or more lines are too long

View file

@ -31,7 +31,8 @@
"height": 1080 "height": 1080
} }
], ],
"channel": { "id": "UCYq-iAOSZBvoUxvfzwKIZWA", "name": "Jacob + Katie Schwarz" }, "channel_id": "UCYq-iAOSZBvoUxvfzwKIZWA",
"channel_name": "Jacob + Katie Schwarz",
"view_count": 216221243, "view_count": 216221243,
"keywords": [ "keywords": [
"4K", "4K",

View file

@ -31,10 +31,8 @@
"height": 1080 "height": 1080
} }
], ],
"channel": { "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
"id": "UCX6OQ3DkcsbYNE6H8uQQuVA", "channel_name": "MrBeast",
"name": "MrBeast"
},
"view_count": 136908834, "view_count": 136908834,
"keywords": [], "keywords": [],
"is_live": false, "is_live": false,

View file

@ -26,6 +26,7 @@ use rustypipe::validate;
#[rstest] #[rstest]
#[case::desktop(ClientType::Desktop)] #[case::desktop(ClientType::Desktop)]
#[case::tv(ClientType::Tv)]
#[case::tv_html5_embed(ClientType::TvHtml5Embed)] #[case::tv_html5_embed(ClientType::TvHtml5Embed)]
#[case::android(ClientType::Android)] #[case::android(ClientType::Android)]
#[case::ios(ClientType::Ios)] #[case::ios(ClientType::Ios)]
@ -40,13 +41,26 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
// dbg!(&player_data); // dbg!(&player_data);
assert_eq!(player_data.details.id, "n4tK7LYFxI0"); assert_eq!(player_data.details.id, "n4tK7LYFxI0");
assert_eq!( assert_eq!(player_data.details.duration, 259);
player_data.details.name, assert!(!player_data.details.thumbnail.is_empty());
"Spektrem - Shine | Progressive House | NCS - Copyright Free Music" assert_eq!(player_data.details.channel_id, "UC_aEa8K-EOJ3D6gOs7HcyNg");
); assert!(!player_data.details.is_live_content);
if client_type == ClientType::DesktopMusic {
assert!(player_data.details.description.is_none()); // The TV client dows not output most video metadata
} else { 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 assert!(player_data
.details .details
.description .description
@ -54,15 +68,10 @@ async fn get_player_from_client(#[case] client_type: ClientType, rp: RustyPipe)
.contains( .contains(
"NCS (NoCopyrightSounds): Empowering Creators through Copyright / Royalty Free Music" "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 { if client_type == ClientType::Ios {
let video = player_data let video = player_data
.video_only_streams .video_only_streams
@ -237,13 +246,13 @@ async fn get_player(
let details = player_data.details; let details = player_data.details;
assert_eq!(details.id, id); assert_eq!(details.id, id);
assert_eq!(details.name, name); assert_eq!(details.name.expect("name"), name);
let desc = details.description.expect("description"); let desc = details.description.expect("description");
assert!(desc.contains(description), "description: {desc}"); assert!(desc.contains(description), "description: {desc}");
assert_eq!(details.duration, duration); assert_eq!(details.duration, duration);
assert_eq!(details.channel.id, channel_id); assert_eq!(details.channel_id, channel_id);
assert_eq!(details.channel.name, channel_name); assert_eq!(details.channel_name.expect("channel name"), channel_name);
assert_gte(details.view_count, views, "views"); assert_gte(details.view_count.expect("view count"), views, "views");
assert_eq!(details.is_live, is_live); assert_eq!(details.is_live, is_live);
assert_eq!(details.is_live_content, is_live_content); assert_eq!(details.is_live_content, is_live_content);