Compare commits

...

12 commits

Author SHA1 Message Date
Renovate Bot
d5133247aa chore(deps): update rust crate rstest to 0.22.0
Some checks failed
CI / Test (pull_request) Failing after 7m15s
2024-08-08 15:12:05 +02:00
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 4995 additions and 325 deletions

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ dirs = "5.0.0"
filenamify = "0.1.0"
# Testing
rstest = "0.21.0"
rstest = "0.22.0"
tokio-test = "0.4.2"
insta = { version = "1.17.1", features = ["ron", "redactions"] }
path_macro = "1.0.0"
@ -74,7 +74,10 @@ tracing-test = "0.2.5"
# Included crates
rustypipe = { path = ".", version = "0.2.0", default-features = false }
rustypipe-downloader = { path = "./downloader", version = "0.1.0", default-features = false }
rustypipe-downloader = { path = "./downloader", version = "0.1.0", default-features = false, features = [
"indicatif",
"audiotag",
] }
[features]
default = ["default-tls"]

View file

@ -1,5 +1,9 @@
# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg)
[![Current crates.io version](https://img.shields.io/crates/v/smartcrop2.svg)](https://crates.io/crates/smartcrop2)
[![License](https://img.shields.io/badge/License-GPL--3-blue.svg?style=flat)](http://opensource.org/licenses/MIT)
[![CI status](https://code.thetadev.de/ThetaDev/rustypipe/actions/workflows/ci.yaml/badge.svg?style=flat&label=CI)](https://code.thetadev.de/ThetaDev/rustypipe/actions/?workflow=ci.yaml)
Rust client for the public YouTube / YouTube Music API (Innertube), inspired by
[NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor).

View file

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

1
cli/README.md Normal file
View file

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

View file

@ -1,16 +1,23 @@
#![doc = include_str!("../README.md")]
#![warn(clippy::todo, clippy::dbg_macro)]
use std::{path::PathBuf, str::FromStr};
use std::{path::PathBuf, str::FromStr, time::Duration};
use clap::{Parser, Subcommand, ValueEnum};
use futures::stream::{self, StreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use owo_colors::OwoColorize;
use rustypipe::{
client::{ClientType, RustyPipe},
model::{UrlTarget, VideoId, YouTubeItem},
model::{
richtext::ToPlaintext, traits::YtEntity, ArtistId, MusicSearchResult, TrackItem, UrlTarget,
Verification, YouTubeItem,
},
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
};
use rustypipe_downloader::{DownloadQuery, DownloaderBuilder};
use rustypipe_downloader::{
DownloadError, DownloadQuery, DownloadVideo, Downloader, DownloaderBuilder,
};
use serde::Serialize;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
@ -81,6 +88,9 @@ enum Commands {
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
#[clap(short, long)]
resolution: Option<u32>,
/// Download only the audio track
#[clap(long)]
audio: bool,
/// Number of videos downloaded in parallel
#[clap(short, long, default_value_t = 8)]
parallel: usize,
@ -90,8 +100,9 @@ enum Commands {
/// Limit the number of videos to download
#[clap(long, default_value_t = 1000)]
limit: usize,
/// YT Client used to fetch player data
#[clap(long)]
player_type: Option<PlayerType>,
client_type: Option<PlayerType>,
},
/// Extract video, playlist, album or channel data
Get {
@ -103,6 +114,9 @@ enum Commands {
/// Pretty-print output
#[clap(long)]
pretty: bool,
/// Output as text
#[clap(long)]
txt: bool,
/// Limit the number of items to fetch
#[clap(long, default_value_t = 20)]
limit: usize,
@ -115,14 +129,15 @@ enum Commands {
/// Get comments
#[clap(long)]
comments: Option<CommentsOrder>,
/// Get lyrics
/// Get lyrics for YTM tracks
#[clap(long)]
lyrics: bool,
/// Get the player
/// Get the player data instead of the video details
#[clap(long)]
player: bool,
/// YT Client used to fetch player data
#[clap(long)]
player_type: Option<PlayerType>,
client_type: Option<PlayerType>,
},
/// Search YouTube
Search {
@ -134,6 +149,9 @@ enum Commands {
/// Pretty-print output
#[clap(long)]
pretty: bool,
/// Output as text
#[clap(long)]
txt: bool,
/// Limit the number of items to fetch
#[clap(long, default_value_t = 20)]
limit: usize,
@ -165,7 +183,7 @@ enum Format {
Yaml,
}
#[derive(Copy, Clone, ValueEnum)]
#[derive(Debug, Copy, Clone, ValueEnum)]
enum ChannelTab {
Videos,
Shorts,
@ -236,6 +254,7 @@ enum MusicSearchCategory {
enum PlayerType {
Desktop,
Tv,
TvEmbed,
Android,
Ios,
}
@ -286,7 +305,8 @@ impl From<PlayerType> for ClientType {
fn from(value: PlayerType) -> Self {
match value {
PlayerType::Desktop => Self::Desktop,
PlayerType::Tv => Self::TvHtml5Embed,
PlayerType::TvEmbed => Self::TvHtml5Embed,
PlayerType::Tv => Self::Tv,
PlayerType::Android => Self::Android,
PlayerType::Ios => Self::Ios,
}
@ -307,30 +327,99 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
};
}
async fn download_video(
rp: &RustyPipe,
id: &str,
target: &DownloadTarget,
resolution: Option<u32>,
player_type: Option<PlayerType>,
multi: MultiProgress,
) {
let mut filter = StreamFilter::new();
if let Some(res) = resolution {
if res == 0 {
filter = filter.no_video();
} else {
filter = filter.video_max_res(res);
fn print_entities(items: &[impl YtEntity]) {
for e in items {
anstream::print!("[{}] {}", e.id(), e.name().bold());
if let Some(n) = e.channel_name() {
anstream::print!(" - {}", n.cyan());
}
println!();
}
}
fn print_tracks(tracks: &[TrackItem]) {
for t in tracks {
if let Some(n) = t.track_nr {
anstream::print!("{} ", format!("{n:02}").yellow().bold());
}
anstream::print!("[{}] {} - ", t.id, t.name.bold());
print_artists(&t.artists);
print_duration(t.duration);
println!();
}
}
fn print_artists(artists: &[ArtistId]) {
for (i, a) in artists.iter().enumerate() {
if i > 0 {
print!(", ");
}
anstream::print!("{}", a.name.cyan());
if let Some(id) = &a.id {
print!(" [{id}]");
}
}
let dl = DownloaderBuilder::new()
.client(rp)
.stream_filter(filter)
.progress_bar(multi)
.build();
let mut q = target.apply(dl.download_id(id));
if let Some(player_type) = player_type {
q = q.player_type(player_type.into());
}
fn print_duration(duration: Option<u32>) {
if let Some(d) = duration {
print!(" ");
let hours = d / 3600;
let minutes = (d / 60) % 60;
let seconds = d % 60;
if hours > 0 {
anstream::print!("{}", format!("{hours:02}:").yellow());
}
anstream::print!("{}", format!("{minutes:02}:{seconds:02}").yellow());
}
}
fn print_music_search<T: Serialize + YtEntity>(
data: &MusicSearchResult<T>,
format: Format,
pretty: bool,
txt: bool,
) {
if txt {
if let Some(corr) = &data.corrected_query {
anstream::println!("Did you mean `{}`?", corr.magenta());
}
print_entities(&data.items.items);
} else {
print_data(data, format, pretty)
}
}
fn print_description(desc: Option<String>) {
if let Some(desc) = desc {
if !desc.is_empty() {
print_h2("Description");
println!("{}", desc);
}
}
}
fn print_h2(title: &str) {
anstream::println!("\n{}", format!("{title}:").green().underline());
}
fn print_verification(verification: Verification) {
match verification {
Verification::None => {}
Verification::Verified => print!(""),
Verification::Artist => print!(""),
}
}
async fn download_video(
dl: &Downloader,
id: &str,
target: &DownloadTarget,
client_type: Option<PlayerType>,
) {
let mut q = target.apply(dl.id(id));
if let Some(client_type) = client_type {
q = q.client_type(client_type.into());
}
let res = q.download().await;
if let Err(e) = res {
@ -339,29 +428,13 @@ async fn download_video(
}
async fn download_videos(
rp: &RustyPipe,
videos: &[VideoId],
dl: &Downloader,
videos: Vec<DownloadVideo>,
target: &DownloadTarget,
resolution: Option<u32>,
parallel: usize,
player_type: Option<PlayerType>,
client_type: Option<PlayerType>,
multi: MultiProgress,
) {
let mut filter = StreamFilter::new();
if let Some(res) = resolution {
if res == 0 {
filter = filter.no_video();
} else {
filter = filter.video_max_res(res);
}
}
let dl = DownloaderBuilder::new()
.client(rp)
.stream_filter(filter)
.progress_bar(multi.clone())
.path_precheck()
.build();
// Indicatif setup
let main = multi.add(ProgressBar::new(
videos.len().try_into().unwrap_or_default(),
@ -379,19 +452,20 @@ async fn download_videos(
.for_each_concurrent(parallel, |video| {
let dl = dl.clone();
let main = main.clone();
let id = &video.id;
let id = video.id().to_owned();
let mut q = target.apply(dl.download_entity(video));
if let Some(player_type) = player_type {
q = q.player_type(player_type.into());
let mut q = target.apply(dl.video(video));
if let Some(client_type) = client_type {
q = q.client_type(client_type.into());
}
async move {
if let Err(e) = q.download().await {
tracing::error!("[{id}]: {e}");
} else {
main.inc(1);
if !matches!(e, DownloadError::Exists(_)) {
tracing::error!("[{id}]: {e}");
}
}
main.inc(1);
}
})
.await;
@ -433,7 +507,9 @@ async fn main() {
.with_writer(ProgWriter(multi.clone()))
.init();
let mut rp = RustyPipe::builder().visitor_data_opt(cli.vdata);
let mut rp = RustyPipe::builder()
.visitor_data_opt(cli.vdata)
.timeout(Duration::from_secs(15));
if cli.report {
rp = rp.report();
} else {
@ -455,15 +531,35 @@ async fn main() {
id,
target,
resolution,
audio,
parallel,
music,
limit,
player_type,
client_type,
} => {
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
let mut filter = StreamFilter::new();
if let Some(res) = resolution {
if res == 0 {
filter = filter.no_video();
} else {
filter = filter.video_max_res(res);
}
}
let mut dl = DownloaderBuilder::new()
.rustypipe(&rp)
.multi_progress(multi.clone())
.path_precheck();
if audio {
dl = dl.audio_tag().crop_cover();
filter = filter.no_video();
}
let dl = dl.stream_filter(filter).build();
match url_target {
UrlTarget::Video { id, .. } => {
download_video(&rp, &id, &target, resolution, player_type, multi).await;
download_video(&dl, &id, &target, client_type).await;
}
UrlTarget::Channel { id } => {
target.assert_dir();
@ -473,27 +569,18 @@ async fn main() {
.extend_limit(&rp.query(), limit)
.await
.unwrap();
let videos: Vec<VideoId> = channel
let videos = channel
.content
.items
.into_iter()
.take(limit)
.map(VideoId::from)
.map(|v| DownloadVideo::from_entity(&v))
.collect();
download_videos(
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
}
UrlTarget::Playlist { id } => {
target.assert_dir();
let videos: Vec<VideoId> = if music {
let videos = if music {
let mut playlist = rp.query().music_playlist(id).await.unwrap();
playlist
.tracks
@ -505,7 +592,7 @@ async fn main() {
.items
.into_iter()
.take(limit)
.map(VideoId::from)
.map(|v| DownloadVideo::from_track(&v))
.collect()
} else {
let mut playlist = rp.query().playlist(id).await.unwrap();
@ -519,45 +606,28 @@ async fn main() {
.items
.into_iter()
.take(limit)
.map(VideoId::from)
.map(|v| DownloadVideo::from_entity(&v))
.collect()
};
download_videos(
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
}
UrlTarget::Album { id } => {
target.assert_dir();
let album = rp.query().music_album(id).await.unwrap();
let videos: Vec<VideoId> = album
let videos = album
.tracks
.into_iter()
.take(limit)
.map(VideoId::from)
.map(|v| DownloadVideo::from_track(&v))
.collect();
download_videos(
&rp,
&videos,
&target,
resolution,
parallel,
player_type,
multi,
)
.await;
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
}
}
}
Commands::Get {
id,
format,
txt,
pretty,
limit,
tab,
@ -565,7 +635,7 @@ async fn main() {
comments,
lyrics,
player,
player_type,
client_type,
} => {
let target = rp.query().resolve_string(&id, false).await.unwrap();
@ -576,16 +646,47 @@ async fn main() {
match details.lyrics_id {
Some(lyrics_id) => {
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap();
print_data(&lyrics, format, pretty);
if txt {
println!("{}\n\n{}", lyrics.body, lyrics.footer);
} else {
print_data(&lyrics, format, pretty);
}
}
None => eprintln!("no lyrics found"),
}
} else if music {
let details = rp.query().music_details(&id).await.unwrap();
print_data(&details, format, pretty);
if txt {
if details.track.is_video {
println!("[MV]");
} else {
println!("[Track]");
}
print!("{} [{}]", details.track.name, details.track.id);
print_duration(details.track.duration);
println!();
print_artists(&details.track.artists);
println!();
if !details.track.is_video {
println!(
"Album: {}",
details
.track
.album
.as_ref()
.map(|b| b.id.as_str())
.unwrap_or("None")
)
}
if let Some(view_count) = details.track.view_count {
println!("Views: {view_count}");
}
} else {
print_data(&details, format, pretty);
}
} else if player {
let player = if let Some(player_type) = player_type {
rp.query().player_from_client(&id, player_type.into()).await
let player = if let Some(client_type) = client_type {
rp.query().player_from_client(&id, client_type.into()).await
} else {
rp.query().player(&id).await
}
@ -612,13 +713,129 @@ async fn main() {
None => {}
}
print_data(&details, format, pretty);
if txt {
anstream::println!(
"{}\n{} [{}]",
"[Video]".on_green().black(),
details.name.green().bold(),
details.id
);
anstream::println!(
"{} {} [{}]",
"Channel:".blue(),
details.channel.name,
details.channel.id
);
if let Some(subs) = details.channel.subscriber_count {
anstream::println!("{} {}", "Subscribers:".blue(), subs);
}
if let Some(date) = details.publish_date {
anstream::println!("{} {}", "Date:".blue(), date);
}
anstream::println!("{} {}", "Views:".blue(), details.view_count);
if let Some(likes) = details.like_count {
anstream::println!("{} {}", "Likes:".blue(), likes);
}
if let Some(comments) = details.top_comments.count {
anstream::println!("{} {}", "Comments:".blue(), comments);
}
if details.is_ccommons {
anstream::println!("{}", "Creative Commons".green());
}
if details.is_live {
anstream::println!("{}", "Livestream".red());
}
print_description(Some(details.description.to_plaintext()));
if !details.recommended.is_empty() {
print_h2("Recommended");
print_entities(&details.recommended.items);
}
let comment_list = comments.map(|c| match c {
CommentsOrder::Top => &details.top_comments.items,
CommentsOrder::Latest => &details.latest_comments.items,
});
if let Some(comment_list) = comment_list {
print_h2("Comments");
for c in comment_list {
if let Some(author) = &c.author {
anstream::print!("{} [{}]", author.name.cyan(), author.id);
print_verification(author.verification);
} else {
anstream::print!("{}", "Unknown author".magenta());
}
if c.by_owner {
print!(" (Owner)");
}
println!();
println!("{}", c.text.to_plaintext());
anstream::print!(
"{} {}",
"Likes:".blue(),
c.like_count.unwrap_or_default()
);
if c.hearted {
anstream::print!(" {}", "".red());
}
println!("\n");
}
}
} else {
print_data(&details, format, pretty);
}
}
}
UrlTarget::Channel { id } => {
if music {
let artist = rp.query().music_artist(&id, true).await.unwrap();
print_data(&artist, format, pretty);
if txt {
anstream::println!(
"{}\n{} [{}]",
"[Artist]".on_green().black(),
artist.name.green().bold(),
artist.id
);
if let Some(subs) = artist.subscriber_count {
anstream::println!("{} {}", "Subscribers:".blue(), subs);
}
if let Some(url) = artist.wikipedia_url {
anstream::println!("{} {}", "Wikipedia:".blue(), url);
}
if let Some(id) = artist.tracks_playlist_id {
anstream::println!("{} {}", "All tracks:".blue(), id);
}
if let Some(id) = artist.videos_playlist_id {
anstream::println!("{} {}", "All videos:".blue(), id);
}
if let Some(id) = artist.radio_id {
anstream::println!("{} {}", "Radio:".blue(), id);
}
print_description(artist.description);
if !artist.albums.is_empty() {
print_h2("Albums");
for b in artist.albums {
anstream::print!(
"[{}] {} ({:?}",
b.id,
b.name.bold(),
b.album_type
);
if let Some(y) = b.year {
print!(", {y}");
}
println!(")");
}
}
if !artist.playlists.is_empty() {
print_h2("Playlists");
print_entities(&artist.playlists);
}
if !artist.similar_artists.is_empty() {
print_h2("Similar artists");
print_entities(&artist.similar_artists);
}
} else {
print_data(&artist, format, pretty);
}
} else {
match tab {
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
@ -636,15 +853,77 @@ async fn main() {
.extend_limit(rp.query(), limit)
.await
.unwrap();
print_data(&channel, format, pretty);
if txt {
anstream::print!(
"{}\n{} [{}]",
format!("[Channel {tab:?}]").on_green().black(),
channel.name.green().bold(),
channel.id
);
print_verification(channel.verification);
println!();
if let Some(subs) = channel.subscriber_count {
anstream::println!("{} {}", "Subscribers:".blue(), subs);
}
print_description(Some(channel.description));
println!();
print_entities(&channel.content.items);
} else {
print_data(&channel, format, pretty);
}
}
ChannelTab::Playlists => {
let channel = rp.query().channel_playlists(&id).await.unwrap();
print_data(&channel, format, pretty);
if txt {
anstream::println!(
"{}\n{} [{}]",
format!("[Channel {tab:?}]").on_green().black(),
channel.name.green().bold(),
channel.id
);
print_description(Some(channel.description));
if let Some(subs) = channel.subscriber_count {
anstream::println!("{} {}", "Subscribers:".blue(), subs);
}
println!();
print_entities(&channel.content.items);
} else {
print_data(&channel, format, pretty);
}
}
ChannelTab::Info => {
let channel = rp.query().channel_info(&id).await.unwrap();
print_data(&channel, format, pretty);
let info = rp.query().channel_info(&id).await.unwrap();
if txt {
anstream::println!(
"{}\n<b>ID:</b>{}",
"[Channel info]".on_green().black(),
info.id
);
print_description(Some(info.description));
if let Some(subs) = info.subscriber_count {
anstream::println!("{} {}", "Subscribers:".blue(), subs);
}
if let Some(vids) = info.video_count {
anstream::println!("{} {}", "Videos:".blue(), vids);
}
if let Some(views) = info.view_count {
anstream::println!("{} {}", "Views:".blue(), views);
}
if let Some(created) = info.create_date {
anstream::println!("{} {}", "Created on:".blue(), created);
}
if !info.links.is_empty() {
print_h2("Links");
for (name, url) in &info.links {
anstream::println!("{} {}", name.blue(), url);
}
}
} else {
print_data(&info, format, pretty);
}
}
}
}
@ -657,7 +936,28 @@ async fn main() {
.extend_limit(rp.query(), limit)
.await
.unwrap();
print_data(&playlist, format, pretty);
if txt {
anstream::println!(
"{}\n{} [{}]\n{} {}",
"[MusicPlaylist]".on_green().black(),
playlist.name.green().bold(),
playlist.id,
"Tracks:".blue(),
playlist.track_count.unwrap_or_default(),
);
if let Some(n) = playlist.channel_name() {
anstream::print!("{} {}", "Author:".blue(), n.bold());
if let Some(id) = playlist.channel_id() {
print!(" [{id}]");
}
println!();
}
print_description(playlist.description.map(|d| d.to_plaintext()));
println!();
print_tracks(&playlist.tracks.items);
} else {
print_data(&playlist, format, pretty);
}
} else {
let mut playlist = rp.query().playlist(&id).await.unwrap();
playlist
@ -665,12 +965,59 @@ async fn main() {
.extend_limit(rp.query(), limit)
.await
.unwrap();
print_data(&playlist, format, pretty);
if txt {
anstream::println!(
"{}\n{} [{}]\n{} {}",
"[Playlist]".on_green().black(),
playlist.name.green().bold(),
playlist.id,
"Videos:".blue(),
playlist.video_count,
);
if let Some(n) = playlist.channel_name() {
anstream::print!("{} {}", "Author:".blue(), n.bold());
if let Some(id) = playlist.channel_id() {
print!(" [{id}]");
}
println!();
}
if let Some(last_update) = playlist.last_update {
anstream::println!("{} {}", "Last update:".blue(), last_update);
}
print_description(playlist.description.map(|d| d.to_plaintext()));
println!();
print_entities(&playlist.videos.items);
} else {
print_data(&playlist, format, pretty);
}
}
}
UrlTarget::Album { id } => {
let album = rp.query().music_album(&id).await.unwrap();
print_data(&album, format, pretty);
if txt {
anstream::print!(
"{}\n{} [{}] ({:?}",
"[Album]".on_green().black(),
album.name.green().bold(),
album.id,
album.album_type
);
if let Some(year) = album.year {
print!(", {year}");
}
println!(")");
if let Some(n) = album.channel_name() {
anstream::print!("{} {}", "Artist:".blue(), n);
if let Some(id) = album.channel_id() {
print!(" [{id}]");
}
}
print_description(album.description.map(|d| d.to_plaintext()));
println!();
print_tracks(&album.tracks);
} else {
print_data(&album, format, pretty);
}
}
}
}
@ -678,6 +1025,7 @@ async fn main() {
query,
format,
pretty,
txt,
limit,
item_type,
length,
@ -704,32 +1052,40 @@ async fn main() {
.await
.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
if txt {
if let Some(corr) = res.corrected_query {
anstream::println!("Did you mean `{}`?", corr.magenta());
}
print_entities(&res.items.items);
} else {
print_data(&res, format, pretty);
}
}
},
Some(MusicSearchCategory::All) => {
let res = rp.query().music_search_main(&query).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
Some(MusicSearchCategory::Tracks) => {
let mut res = rp.query().music_search_tracks(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
Some(MusicSearchCategory::Videos) => {
let mut res = rp.query().music_search_videos(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
Some(MusicSearchCategory::Artists) => {
let mut res = rp.query().music_search_artists(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
Some(MusicSearchCategory::Albums) => {
let mut res = rp.query().music_search_albums(&query).await.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
let mut res = rp
@ -741,7 +1097,7 @@ async fn main() {
.await
.unwrap();
res.items.extend_limit(rp.query(), limit).await.unwrap();
print_data(&res, format, pretty);
print_music_search(&res, format, pretty, txt);
}
},
Commands::Vdata => {

View file

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

View file

@ -30,6 +30,8 @@ rustls-tls-native-roots = [
"rustypipe/rustls-tls-native-roots",
]
audiotag = ["dep:lofty", "dep:image", "dep:smartcrop2"]
[dependencies]
rustypipe.workspace = true
once_cell.workspace = true
@ -39,6 +41,10 @@ futures.workspace = true
reqwest = { workspace = true, features = ["stream"] }
rand.workspace = true
tokio = { workspace = true, features = ["macros", "fs", "process"] }
indicatif.workspace = true
indicatif = { workspace = true, optional = true }
filenamify.workspace = true
tracing.workspace = true
time.workspace = true
lofty = { version = "0.21.0", optional = true }
image = { version = "0.25.0", optional = true }
smartcrop2 = { version = "0.3.0", optional = true }

42
downloader/README.md Normal file
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
#[error("file {0} already exists")]
Exists(PathBuf),
#[cfg(feature = "audiotag")]
/// Audio tagging error
#[error("Audio tag error: {0}")]
AudioTag(Cow<'static, str>),
/// Other error
#[error("error: {0}")]
Other(Cow<'static, str>),
}
#[cfg(feature = "audiotag")]
impl From<lofty::error::LoftyError> for DownloadError {
fn from(value: lofty::error::LoftyError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
#[cfg(feature = "audiotag")]
impl From<image::ImageError> for DownloadError {
fn from(value: image::ImageError) -> Self {
Self::AudioTag(value.to_string().into())
}
}
/// Split an URL into its base string and parameter map
///
/// Example:

View file

@ -61,6 +61,8 @@ pub enum ClientType {
///
/// can access age-restricted videos, cannot access non-embeddable videos
TvHtml5Embed,
/// Client used by youtube.com/tv
Tv,
/// Client used by the Android app
///
/// no obfuscated stream URLs, includes lower resolution audio streams
@ -74,7 +76,10 @@ pub enum ClientType {
impl ClientType {
fn is_web(self) -> bool {
match self {
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => true,
ClientType::Desktop
| ClientType::DesktopMusic
| ClientType::TvHtml5Embed
| ClientType::Tv => true,
ClientType::Android | ClientType::Ios => false,
}
}
@ -183,6 +188,7 @@ struct QContinuation<'a> {
}
const DEFAULT_UA: &str = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0";
const TV_UA: &str = "Mozilla/5.0 (SMART-TV; Linux; Tizen 5.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/5.0 NativeTVAds Safari/538.1";
const CONSENT_COOKIE: &str = "SOCS=CAISAiAD";
@ -191,12 +197,14 @@ const YOUTUBEI_V1_GAPIS_URL: &str = "https://youtubei.googleapis.com/youtubei/v1
const YOUTUBE_MUSIC_V1_URL: &str = "https://music.youtube.com/youtubei/v1/";
const YOUTUBE_HOME_URL: &str = "https://www.youtube.com/";
const YOUTUBE_MUSIC_HOME_URL: &str = "https://music.youtube.com/";
const YOUTUBE_TV_URL: &str = "https://www.youtube.com/tv";
const DISABLE_PRETTY_PRINT_PARAMETER: &str = "prettyPrint=false";
// Desktop client
const DESKTOP_CLIENT_VERSION: &str = "2.20230126.00.00";
const TVHTML5_CLIENT_VERSION: &str = "2.0";
const TV_CLIENT_VERSION: &str = "7.20240724.13.00";
const DESKTOP_MUSIC_CLIENT_VERSION: &str = "1.20230123.01.01";
// Mobile client
@ -454,7 +462,7 @@ impl RustyPipeBuilder {
self.build_with_client(ClientBuilder::new())
}
/// Create a new, configured RustyPipe instance using a Reqwest client builder.
/// Create a new, configured RustyPipe instance using a Reqwest [`ClientBuilder`].
pub fn build_with_client(self, mut client_builder: ClientBuilder) -> Result<RustyPipe, Error> {
let user_agent = self
.user_agent
@ -717,31 +725,33 @@ impl RustyPipe {
}
}
/// Get the internal HTTP client
///
/// Can be used for downloading videos or custom YT requests.
#[must_use]
pub fn http_client(&self) -> &Client {
&self.inner.http
}
/// Execute the given http request.
async fn http_request(&self, request: &Request) -> Result<Response, reqwest::Error> {
let mut last_resp = None;
for n in 0..=self.inner.n_http_retries {
let resp = self
.inner
.http
.execute(request.try_clone().unwrap())
.await?;
let resp = self.inner.http.execute(request.try_clone().unwrap()).await;
let status = resp.status();
// Immediately return in case of success or unrecoverable status code
if status.is_success()
|| (!status.is_server_error() && status != StatusCode::TOO_MANY_REQUESTS)
{
return Ok(resp);
}
let err = match resp {
Ok(resp) => {
let status = resp.status();
// Immediately return in case of success or unrecoverable status code
if status.is_success()
|| (!status.is_server_error() && status != StatusCode::TOO_MANY_REQUESTS)
{
return Ok(resp);
}
last_resp = Some(Ok(resp));
status.to_string()
}
Err(e) => {
// Retry in case of a timeout error
if !e.is_timeout() {
return Err(e);
}
last_resp = Some(Err(e));
"timeout".to_string()
}
};
// Retry in case of a recoverable status code (server err, too many requests)
if n != self.inner.n_http_retries {
@ -749,15 +759,13 @@ impl RustyPipe {
tracing::warn!(
"Retry attempt #{}. Error: {}. Waiting {} ms",
n + 1,
status,
err,
ms
);
tokio::time::sleep(Duration::from_millis(ms.into())).await;
}
last_resp = Some(resp);
}
Ok(last_resp.unwrap())
last_resp.unwrap()
}
/// Execute the given http request, returning an error in case of a
@ -1098,6 +1106,7 @@ impl RustyPipeQuery {
ClientType::Desktop | ClientType::DesktopMusic | ClientType::TvHtml5Embed => {
Cow::Borrowed(&self.client.inner.user_agent)
}
ClientType::Tv => TV_UA.into(),
ClientType::Android => format!(
"com.google.android.youtube/{} (Linux; U; Android 12; {}) gzip",
MOBILE_CLIENT_VERSION, self.opts.country
@ -1178,6 +1187,24 @@ impl RustyPipeQuery {
embed_url: YOUTUBE_HOME_URL,
}),
},
ClientType::Tv => YTContext {
client: ClientInfo {
client_name: "TVHTML5",
client_version: Cow::Borrowed(TV_CLIENT_VERSION),
client_screen: Some("WATCH"),
platform: "TV",
device_model: Some("SmartTV"),
visitor_data,
hl,
gl,
..Default::default()
},
request: Some(RequestYT::default()),
user: User::default(),
third_party: Some(ThirdParty {
embed_url: YOUTUBE_TV_URL,
}),
},
ClientType::Android => YTContext {
client: ClientInfo {
client_name: "ANDROID",
@ -1266,6 +1293,17 @@ impl RustyPipeQuery {
.header(header::REFERER, YOUTUBE_HOME_URL)
.header("X-YouTube-Client-Name", "1")
.header("X-YouTube-Client-Version", TVHTML5_CLIENT_VERSION),
ClientType::Tv => self
.client
.inner
.http
.post(format!(
"{YOUTUBEI_V1_URL}{endpoint}?{DISABLE_PRETTY_PRINT_PARAMETER}"
))
.header(header::ORIGIN, YOUTUBE_HOME_URL)
.header(header::REFERER, YOUTUBE_TV_URL)
.header("X-YouTube-Client-Name", "7")
.header("X-YouTube-Client-Version", TV_CLIENT_VERSION),
ClientType::Android => self
.client
.inner

View file

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

View file

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

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer(
details: VideoPlayerDetails(
id: "pPvd8UxmSbQ",
name: "Inspiring Cinematic Uplifting (Creative Commons)",
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
duration: 163,
thumbnail: [
@ -30,11 +30,9 @@ VideoPlayer(
height: 480,
),
],
channel: ChannelId(
id: "UCbxxEi-ImPlbLx5F-fHetEg",
name: "RomanSenykMusic - Royalty Free Music",
),
view_count: 426567,
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
view_count: Some(426567),
keywords: [
"no copyright music",
"background music",

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer(
details: VideoPlayerDetails(
id: "pPvd8UxmSbQ",
name: "Inspiring Cinematic Uplifting (Creative Commons)",
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
duration: 163,
thumbnail: [
@ -35,11 +35,9 @@ VideoPlayer(
height: 1080,
),
],
channel: ChannelId(
id: "UCbxxEi-ImPlbLx5F-fHetEg",
name: "RomanSenykMusic - Royalty Free Music",
),
view_count: 426567,
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
view_count: Some(426567),
keywords: [
"no copyright music",
"background music",

View file

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

View file

@ -5,7 +5,7 @@ expression: map_res.c
VideoPlayer(
details: VideoPlayerDetails(
id: "pPvd8UxmSbQ",
name: "Inspiring Cinematic Uplifting (Creative Commons)",
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
duration: 163,
thumbnail: [
@ -25,11 +25,9 @@ VideoPlayer(
height: 480,
),
],
channel: ChannelId(
id: "UCbxxEi-ImPlbLx5F-fHetEg",
name: "RomanSenykMusic - Royalty Free Music",
),
view_count: 426567,
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
view_count: Some(426567),
keywords: [
"no copyright music",
"background music",

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(
details: VideoPlayerDetails(
id: "pPvd8UxmSbQ",
name: "Inspiring Cinematic Uplifting (Creative Commons)",
name: Some("Inspiring Cinematic Uplifting (Creative Commons)"),
description: Some("► Download Music: http://bit.ly/2QLufeh\nImportant to know! You can download this track for free through Patreon. You will pay only for new tracks! So join others and let\'s make next track together!\n\n► MORE MUSIC: Become my patron and get access to all our music from Patreon library. More Info here: http://bit.ly/2JJDFHb\n\n► Additional edit versions of this track you can download here: http://bit.ly/2WdRinT (5 versions)\n--------------------- \n\n►DESCRIPTION:\nInspiring Cinematic Uplifting Trailer Background - epic music for trailer video project with powerful drums, energetic orchestra and gentle piano melody. This motivational cinematic theme will work as perfect background for beautiful epic moments, landscapes, nature, drone video, motivational products and achievements.\n--------------------- \n\n► LICENSE:\n● If you need a license for your project, you can purchase it here: \nhttps://1.envato.market/ajicu (Audiojungle)\nhttps://bit.ly/3fWZZuI (Pond5)\n--------------------- \n\n► LISTEN ON:\n● Spotify - https://spoti.fi/2sHm3UH\n● Apple Music - https://apple.co/3qBjbUO\n--------------------- \n\n► SUBSCRIBE FOR MORE: \nPatreon: http://bit.ly/2JJDFHb\nYoutube: http://bit.ly/2AYBzfA\nFacebook: http://bit.ly/2T6dTx5\nInstagram: http://bit.ly/2BHJ8rB\nTwitter: http://bit.ly/2MwtOlT\nSoundCloud: http://bit.ly/2IwVVmt\nAudiojungle: https://1.envato.market/ajrsm\nPond5: https://bit.ly/2TLi1rW\n--------------------- \n►Photo by Vittorio Staffolani from Pexels\n--------------------- \n\nFAQ:\n\n► Can I use this music in my videos? \n● Sure! Just download this track and you are ready to use it! We only ask to credit us. \n-------------------- \n\n► What is \"Creative Commons\"? \nCreative Commons is a system that allows you to legally use “some rights reserved” music, movies, images, and other content — all for free. Licensees may copy, distribute, display and perform the work and make derivative works and remixes based on it only if they give the author or licensor the credits.\n-------------------- \n\n► Will I have any copyright issues with this track?\n● No, you should not have any copyright problems with this track!\n-------------------- \n\n► Is it necessary to become your patron?\n● No it\'s not necessary. But we recommend you to become our patron because you will get access to huge library of music. You will download only highest quality files. You will find additional edited versions of every track. You always be tuned with our news. You will find music not only from Roman Senyk but also from another talented authors.\n-------------------- \n\n► Why I received a copyright claim when I used this track?\n● Do not panic! This is very common situation. Content ID fingerprint system can mismatch our music. Just dispute the claim by showing our original track. Or send us the link to your video (romansenykmusic@gmail.com) and attach some screenshot with claim information. Claim will be released until 24 hours!\n\n► How to credit you in my video?\n● Just add to the description of your project information about Author, Name of Song and the link to our original track. Or copy and paste:\n\nMusic Info: Inspiring Cinematic Uplifting by RomanSenykMusic.\nMusic Link: https://youtu.be/pPvd8UxmSbQ\n--------------------- \n\n► If you have any questions, you can write in the comments for this video or by email: romansenykmusic@gmail.com\n--------------------- \n\nStay tuned! The best is yet to come! \nThanks For Listening!\nRoman Senyk"),
duration: 163,
thumbnail: [
@ -35,11 +35,9 @@ VideoPlayer(
height: 1080,
),
],
channel: ChannelId(
id: "UCbxxEi-ImPlbLx5F-fHetEg",
name: "RomanSenykMusic - Royalty Free Music",
),
view_count: 426567,
channel_id: "UCbxxEi-ImPlbLx5F-fHetEg",
channel_name: Some("RomanSenykMusic - Royalty Free Music"),
view_count: Some(426567),
keywords: [
"no copyright music",
"background music",

View file

@ -164,6 +164,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
+ &deobfuscate_function
+ &caller_function(DEOBF_SIG_FUNC_NAME, &dfunc_name);
verify_fn(&js_fn, DEOBF_SIG_FUNC_NAME)?;
tracing::debug!("successfully extracted sig fn `{dfunc_name}`");
Ok(js_fn)
}
@ -171,7 +172,7 @@ fn get_sig_fn(player_js: &str) -> Result<String, DeobfError> {
fn get_nsig_fn_names(player_js: &str) -> impl Iterator<Item = String> + '_ {
static FUNCTION_NAME_REGEX: Lazy<Regex> = Lazy::new(|| {
// x.get( .. y=functionName[array_num](z) .. x.set(
Regex::new(r#"\w\.get\(.+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
Regex::new(r#"(?:\w\.get\(|index\.m3u8).+\w=(\w{2,})\[(\d+)\]\(\w\).+\w\.set\("#).unwrap()
});
FUNCTION_NAME_REGEX
@ -263,15 +264,15 @@ fn get_nsig_fn(player_js: &str) -> Result<String, DeobfError> {
.ok_or(DeobfError::Extraction("could not find function base"))?;
let js_fn = extract_js_fn(&player_js[offset..], name)
.map(|s| s + ";" + &caller_function(DEOBF_NSIG_FUNC_NAME, name))?;
.map(|s| format!("var {};{}", s, caller_function(DEOBF_NSIG_FUNC_NAME, name)))?;
verify_fn(&js_fn, DEOBF_NSIG_FUNC_NAME)?;
tracing::info!("Successfully extracted nsig fn `{name}`");
tracing::debug!("successfully extracted nsig fn `{name}`");
Ok(js_fn)
};
util::find_map_or_last_err(
get_nsig_fn_names(player_js),
DeobfError::Extraction("no nsig fn name found"),
DeobfError::Extraction("nsig function name"),
|name| {
extract_fn(&name).map_err(|e| {
tracing::warn!("Failed to extract nsig fn `{name}`: {e}");
@ -330,7 +331,7 @@ mod tests {
});
const SIG_DEOBF_FUNC: &str = r#"var qB={w8:function(a){a.reverse()},EC:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},Np:function(a,b){a.splice(0,b)}};var Rva=function(a){a=a.split("");qB.Np(a,3);qB.w8(a,41);qB.EC(a,55);qB.Np(a,3);qB.w8(a,33);qB.Np(a,3);qB.EC(a,48);qB.EC(a,17);qB.EC(a,43);return a.join("")};var deobf_sig=Rva;"#;
const NSIG_DEOBF_FUNC: &str = r#"Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
const NSIG_DEOBF_FUNC: &str = r#"var Vo=function(a){var b=a.split(""),c=[function(d,e,f){var h=f.length;d.forEach(function(l,m,n){this.push(n[m]=f[(f.indexOf(l)-f.indexOf(this[m])+m+h--)%f.length])},e.split(""))},
928409064,-595856984,1403221911,653089124,-168714481,-1883008765,158931990,1346921902,361518508,1403221911,-362174697,-233641452,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
b,158931990,791141857,-907319795,-1776185924,1595027902,-829736173,function(d,e){e=(e%d.length+d.length)%d.length;d.splice(0,1,d.splice(e,1,d[0])[0])},
-1274951142,function(){for(var d=64,e=[];++d-e.length-32;){switch(d){case 91:d=44;continue;case 123:d=65;break;case 65:d-=18;continue;case 58:d=96;continue;case 46:d=95}e.push(String.fromCharCode(d))}return e},
@ -436,6 +437,6 @@ c[36](c[8],c[32]),c[20](c[25],c[10]),c[2](c[22],c[8]),c[32](c[20],c[16]),c[32](c
let deobf_sig = deobf.deobfuscate_sig("GOqGOqGOq0QJ8wRAIgaryQHfplJ9xJSKFywyaSMHuuwZYsoMTAvRvfm51qIGECIA5061zWeyfMPX9hEl_U6f9J0tr7GTJMKyPf5XNrJb5fb5i").unwrap();
assert!(deobf_sig.len() >= 100);
let deobf_nsig = deobf.deobfuscate_nsig("WHbZ-Nj2TSJxder").unwrap();
assert!(deobf_nsig.len() >= 10);
assert!(deobf_nsig.len() >= 6);
}
}

View file

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

View file

@ -227,30 +227,16 @@ macro_rules! yt_entity_owner_music {
}
fn channel_name(&self) -> Option<&str> {
self.artists.first().map(|a| a.name.as_str())
if self.by_va {
Some(crate::util::VARIOUS_ARTISTS)
} else {
self.artists.first().map(|a| a.name.as_str())
}
}
}
};
}
impl YtEntity for VideoPlayer {
fn id(&self) -> &str {
&self.details.id
}
fn name(&self) -> &str {
&self.details.name
}
fn channel_id(&self) -> Option<&str> {
Some(&self.details.channel.id)
}
fn channel_name(&self) -> Option<&str> {
Some(&self.details.channel.name)
}
}
impl<T> YtEntity for Channel<T> {
fn id(&self) -> &str {
&self.id
@ -269,7 +255,78 @@ impl<T> YtEntity for Channel<T> {
}
}
yt_entity_owner! {VideoPlayerDetails}
impl YtEntity for YouTubeItem {
fn id(&self) -> &str {
match self {
YouTubeItem::Video(v) => &v.id,
YouTubeItem::Playlist(p) => &p.id,
YouTubeItem::Channel(c) => &c.id,
}
}
fn name(&self) -> &str {
match self {
YouTubeItem::Video(v) => &v.name,
YouTubeItem::Playlist(p) => &p.name,
YouTubeItem::Channel(c) => &c.name,
}
}
fn channel_id(&self) -> Option<&str> {
match self {
YouTubeItem::Video(v) => v.channel_id(),
YouTubeItem::Playlist(p) => p.channel_id(),
YouTubeItem::Channel(_) => None,
}
}
fn channel_name(&self) -> Option<&str> {
match self {
YouTubeItem::Video(v) => v.channel_name(),
YouTubeItem::Playlist(p) => p.channel_name(),
YouTubeItem::Channel(_) => None,
}
}
}
impl YtEntity for MusicItem {
fn id(&self) -> &str {
match self {
MusicItem::Track(t) => &t.id,
MusicItem::Album(b) => &b.id,
MusicItem::Artist(a) => &a.id,
MusicItem::Playlist(p) => &p.id,
}
}
fn name(&self) -> &str {
match self {
MusicItem::Track(t) => &t.name,
MusicItem::Album(b) => &b.name,
MusicItem::Artist(a) => &a.name,
MusicItem::Playlist(p) => &p.name,
}
}
fn channel_id(&self) -> Option<&str> {
match self {
MusicItem::Track(t) => t.channel_id(),
MusicItem::Album(b) => b.channel_id(),
MusicItem::Artist(_) => None,
MusicItem::Playlist(p) => p.channel_id(),
}
}
fn channel_name(&self) -> Option<&str> {
match self {
MusicItem::Track(t) => t.channel_name(),
MusicItem::Album(b) => b.channel_name(),
MusicItem::Artist(_) => None,
MusicItem::Playlist(p) => p.channel_id(),
}
}
}
yt_entity_owner_opt! {Playlist}
yt_entity! {ChannelId}
yt_entity_owner! {VideoDetails}

File diff suppressed because one or more lines are too long

View file

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

View file

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

View file

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