feat: add plaintext output to CLI
This commit is contained in:
parent
171b3cb8d1
commit
8618da4746
4 changed files with 411 additions and 55 deletions
|
|
@ -56,3 +56,5 @@ tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
serde_yaml.workspace = true
|
serde_yaml.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
|
||||||
|
color-print = "0.3.6"
|
||||||
|
|
|
||||||
1
cli/README.md
Normal file
1
cli/README.md
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# RustyPipe CLI
|
||||||
445
cli/src/main.rs
445
cli/src/main.rs
|
|
@ -4,11 +4,15 @@
|
||||||
use std::{path::PathBuf, str::FromStr, time::Duration};
|
use std::{path::PathBuf, str::FromStr, time::Duration};
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use color_print::{cprint, cprintln};
|
||||||
use futures::stream::{self, StreamExt};
|
use futures::stream::{self, StreamExt};
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||||
use rustypipe::{
|
use rustypipe::{
|
||||||
client::{ClientType, RustyPipe},
|
client::{ClientType, RustyPipe},
|
||||||
model::{UrlTarget, YouTubeItem},
|
model::{
|
||||||
|
richtext::ToPlaintext, traits::YtEntity, ArtistId, MusicSearchResult, TrackItem, UrlTarget,
|
||||||
|
YouTubeItem,
|
||||||
|
},
|
||||||
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
param::{search_filter, ChannelVideoTab, Country, Language, StreamFilter},
|
||||||
};
|
};
|
||||||
use rustypipe_downloader::{
|
use rustypipe_downloader::{
|
||||||
|
|
@ -84,6 +88,9 @@ enum Commands {
|
||||||
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
|
/// Video resolution (e.g. 720, 1080). Set to 0 for audio-only.
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
resolution: Option<u32>,
|
resolution: Option<u32>,
|
||||||
|
/// Download only the audio track
|
||||||
|
#[clap(long)]
|
||||||
|
audio: bool,
|
||||||
/// Number of videos downloaded in parallel
|
/// Number of videos downloaded in parallel
|
||||||
#[clap(short, long, default_value_t = 8)]
|
#[clap(short, long, default_value_t = 8)]
|
||||||
parallel: usize,
|
parallel: usize,
|
||||||
|
|
@ -93,8 +100,9 @@ enum Commands {
|
||||||
/// Limit the number of videos to download
|
/// Limit the number of videos to download
|
||||||
#[clap(long, default_value_t = 1000)]
|
#[clap(long, default_value_t = 1000)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
/// YT Client used to fetch player data
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
player_type: Option<PlayerType>,
|
client_type: Option<PlayerType>,
|
||||||
},
|
},
|
||||||
/// Extract video, playlist, album or channel data
|
/// Extract video, playlist, album or channel data
|
||||||
Get {
|
Get {
|
||||||
|
|
@ -106,6 +114,9 @@ enum Commands {
|
||||||
/// Pretty-print output
|
/// Pretty-print output
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
|
/// Output as text
|
||||||
|
#[clap(long)]
|
||||||
|
txt: bool,
|
||||||
/// Limit the number of items to fetch
|
/// Limit the number of items to fetch
|
||||||
#[clap(long, default_value_t = 20)]
|
#[clap(long, default_value_t = 20)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
|
@ -118,14 +129,15 @@ enum Commands {
|
||||||
/// Get comments
|
/// Get comments
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
comments: Option<CommentsOrder>,
|
comments: Option<CommentsOrder>,
|
||||||
/// Get lyrics
|
/// Get lyrics for YTM tracks
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
lyrics: bool,
|
lyrics: bool,
|
||||||
/// Get the player
|
/// Get the player data instead of the video details
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
player: bool,
|
player: bool,
|
||||||
|
/// YT Client used to fetch player data
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
player_type: Option<PlayerType>,
|
client_type: Option<PlayerType>,
|
||||||
},
|
},
|
||||||
/// Search YouTube
|
/// Search YouTube
|
||||||
Search {
|
Search {
|
||||||
|
|
@ -137,6 +149,9 @@ enum Commands {
|
||||||
/// Pretty-print output
|
/// Pretty-print output
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
|
/// Output as text
|
||||||
|
#[clap(long)]
|
||||||
|
txt: bool,
|
||||||
/// Limit the number of items to fetch
|
/// Limit the number of items to fetch
|
||||||
#[clap(long, default_value_t = 20)]
|
#[clap(long, default_value_t = 20)]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
|
|
@ -168,7 +183,7 @@ enum Format {
|
||||||
Yaml,
|
Yaml,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, ValueEnum)]
|
#[derive(Debug, Copy, Clone, ValueEnum)]
|
||||||
enum ChannelTab {
|
enum ChannelTab {
|
||||||
Videos,
|
Videos,
|
||||||
Shorts,
|
Shorts,
|
||||||
|
|
@ -312,15 +327,91 @@ fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_entities(items: &[impl YtEntity]) {
|
||||||
|
for e in items {
|
||||||
|
cprint!("[{}] <b>{}</b>", e.id(), e.name());
|
||||||
|
if let Some(n) = e.channel_name() {
|
||||||
|
cprint!(" - <c>{}</c>", n);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_tracks(tracks: &[TrackItem]) {
|
||||||
|
for t in tracks {
|
||||||
|
if let Some(n) = t.track_nr {
|
||||||
|
cprint!("<y><bold>{n:02}</bold></y> ");
|
||||||
|
}
|
||||||
|
cprint!("[{}] <b>{}</b> - ", t.id, t.name);
|
||||||
|
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!(", ");
|
||||||
|
}
|
||||||
|
cprint!("<c>{}</c>", a.name);
|
||||||
|
if let Some(id) = &a.id {
|
||||||
|
print!(" [{id}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
cprint!("<y>{hours:02}:");
|
||||||
|
}
|
||||||
|
cprint!("<y>{minutes:02}:{seconds:02}</y>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
cprintln!("Did you mean <m>`{}`</m>?", corr);
|
||||||
|
}
|
||||||
|
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");
|
||||||
|
cprintln!("{}", desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_h2(title: &str) {
|
||||||
|
cprintln!("\n<g><u>{}:</u></g>", title);
|
||||||
|
}
|
||||||
|
|
||||||
async fn download_video(
|
async fn download_video(
|
||||||
dl: &Downloader,
|
dl: &Downloader,
|
||||||
id: &str,
|
id: &str,
|
||||||
target: &DownloadTarget,
|
target: &DownloadTarget,
|
||||||
player_type: Option<PlayerType>,
|
client_type: Option<PlayerType>,
|
||||||
) {
|
) {
|
||||||
let mut q = target.apply(dl.id(id));
|
let mut q = target.apply(dl.id(id));
|
||||||
if let Some(player_type) = player_type {
|
if let Some(client_type) = client_type {
|
||||||
q = q.player_type(player_type.into());
|
q = q.client_type(client_type.into());
|
||||||
}
|
}
|
||||||
let res = q.download().await;
|
let res = q.download().await;
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
|
|
@ -333,7 +424,7 @@ async fn download_videos(
|
||||||
videos: Vec<DownloadVideo>,
|
videos: Vec<DownloadVideo>,
|
||||||
target: &DownloadTarget,
|
target: &DownloadTarget,
|
||||||
parallel: usize,
|
parallel: usize,
|
||||||
player_type: Option<PlayerType>,
|
client_type: Option<PlayerType>,
|
||||||
multi: MultiProgress,
|
multi: MultiProgress,
|
||||||
) {
|
) {
|
||||||
// Indicatif setup
|
// Indicatif setup
|
||||||
|
|
@ -356,8 +447,8 @@ async fn download_videos(
|
||||||
let id = video.id().to_owned();
|
let id = video.id().to_owned();
|
||||||
|
|
||||||
let mut q = target.apply(dl.video(video));
|
let mut q = target.apply(dl.video(video));
|
||||||
if let Some(player_type) = player_type {
|
if let Some(client_type) = client_type {
|
||||||
q = q.player_type(player_type.into());
|
q = q.client_type(client_type.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
|
|
@ -432,10 +523,11 @@ async fn main() {
|
||||||
id,
|
id,
|
||||||
target,
|
target,
|
||||||
resolution,
|
resolution,
|
||||||
|
audio,
|
||||||
parallel,
|
parallel,
|
||||||
music,
|
music,
|
||||||
limit,
|
limit,
|
||||||
player_type,
|
client_type,
|
||||||
} => {
|
} => {
|
||||||
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
|
let url_target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||||
|
|
||||||
|
|
@ -447,18 +539,19 @@ async fn main() {
|
||||||
filter = filter.video_max_res(res);
|
filter = filter.video_max_res(res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dl = DownloaderBuilder::new()
|
let mut dl = DownloaderBuilder::new()
|
||||||
.rustypipe(&rp)
|
.rustypipe(&rp)
|
||||||
.stream_filter(filter)
|
|
||||||
.multi_progress(multi.clone())
|
.multi_progress(multi.clone())
|
||||||
.audio_tag()
|
.path_precheck();
|
||||||
.crop_cover()
|
if audio {
|
||||||
.path_precheck()
|
dl = dl.audio_tag().crop_cover();
|
||||||
.build();
|
filter = filter.no_video();
|
||||||
|
}
|
||||||
|
let dl = dl.stream_filter(filter).build();
|
||||||
|
|
||||||
match url_target {
|
match url_target {
|
||||||
UrlTarget::Video { id, .. } => {
|
UrlTarget::Video { id, .. } => {
|
||||||
download_video(&dl, &id, &target, player_type).await;
|
download_video(&dl, &id, &target, client_type).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Channel { id } => {
|
UrlTarget::Channel { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
|
|
@ -475,7 +568,7 @@ async fn main() {
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map(|v| DownloadVideo::from_entity(&v))
|
.map(|v| DownloadVideo::from_entity(&v))
|
||||||
.collect();
|
.collect();
|
||||||
download_videos(&dl, videos, &target, parallel, player_type, multi).await;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Playlist { id } => {
|
UrlTarget::Playlist { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
|
|
@ -508,7 +601,7 @@ async fn main() {
|
||||||
.map(|v| DownloadVideo::from_entity(&v))
|
.map(|v| DownloadVideo::from_entity(&v))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
download_videos(&dl, videos, &target, parallel, player_type, multi).await;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
target.assert_dir();
|
target.assert_dir();
|
||||||
|
|
@ -519,13 +612,14 @@ async fn main() {
|
||||||
.take(limit)
|
.take(limit)
|
||||||
.map(|v| DownloadVideo::from_track(&v))
|
.map(|v| DownloadVideo::from_track(&v))
|
||||||
.collect();
|
.collect();
|
||||||
download_videos(&dl, videos, &target, parallel, player_type, multi).await;
|
download_videos(&dl, videos, &target, parallel, client_type, multi).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Commands::Get {
|
Commands::Get {
|
||||||
id,
|
id,
|
||||||
format,
|
format,
|
||||||
|
txt,
|
||||||
pretty,
|
pretty,
|
||||||
limit,
|
limit,
|
||||||
tab,
|
tab,
|
||||||
|
|
@ -533,7 +627,7 @@ async fn main() {
|
||||||
comments,
|
comments,
|
||||||
lyrics,
|
lyrics,
|
||||||
player,
|
player,
|
||||||
player_type,
|
client_type,
|
||||||
} => {
|
} => {
|
||||||
let target = rp.query().resolve_string(&id, false).await.unwrap();
|
let target = rp.query().resolve_string(&id, false).await.unwrap();
|
||||||
|
|
||||||
|
|
@ -544,16 +638,47 @@ async fn main() {
|
||||||
match details.lyrics_id {
|
match details.lyrics_id {
|
||||||
Some(lyrics_id) => {
|
Some(lyrics_id) => {
|
||||||
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap();
|
let lyrics = rp.query().music_lyrics(lyrics_id).await.unwrap();
|
||||||
print_data(&lyrics, format, pretty);
|
if txt {
|
||||||
|
println!("{}\n\n{}", lyrics.body, lyrics.footer);
|
||||||
|
} else {
|
||||||
|
print_data(&lyrics, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => eprintln!("no lyrics found"),
|
None => eprintln!("no lyrics found"),
|
||||||
}
|
}
|
||||||
} else if music {
|
} else if music {
|
||||||
let details = rp.query().music_details(&id).await.unwrap();
|
let details = rp.query().music_details(&id).await.unwrap();
|
||||||
print_data(&details, format, pretty);
|
if txt {
|
||||||
|
if details.track.is_video {
|
||||||
|
println!("[MV]");
|
||||||
|
} else {
|
||||||
|
println!("[Track]");
|
||||||
|
}
|
||||||
|
print!("{} [{}]", details.track.name, details.track.id);
|
||||||
|
print_duration(details.track.duration);
|
||||||
|
println!();
|
||||||
|
print_artists(&details.track.artists);
|
||||||
|
println!();
|
||||||
|
if !details.track.is_video {
|
||||||
|
println!(
|
||||||
|
"Album: {}",
|
||||||
|
details
|
||||||
|
.track
|
||||||
|
.album
|
||||||
|
.as_ref()
|
||||||
|
.map(|b| b.id.as_str())
|
||||||
|
.unwrap_or("None")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if let Some(view_count) = details.track.view_count {
|
||||||
|
println!("Views: {view_count}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_data(&details, format, pretty);
|
||||||
|
}
|
||||||
} else if player {
|
} else if player {
|
||||||
let player = if let Some(player_type) = player_type {
|
let player = if let Some(client_type) = client_type {
|
||||||
rp.query().player_from_client(&id, player_type.into()).await
|
rp.query().player_from_client(&id, client_type.into()).await
|
||||||
} else {
|
} else {
|
||||||
rp.query().player(&id).await
|
rp.query().player(&id).await
|
||||||
}
|
}
|
||||||
|
|
@ -580,13 +705,116 @@ async fn main() {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
print_data(&details, format, pretty);
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Video]</k></G>\n<g><bold>{}</bold></g> [{}]",
|
||||||
|
details.name,
|
||||||
|
details.id
|
||||||
|
);
|
||||||
|
cprintln!(
|
||||||
|
"<b>Channel:</b> {} [{}]",
|
||||||
|
details.channel.name,
|
||||||
|
details.channel.id
|
||||||
|
);
|
||||||
|
if let Some(subs) = details.channel.subscriber_count {
|
||||||
|
cprintln!("<b>Subscribers:</b> {}", subs);
|
||||||
|
}
|
||||||
|
if let Some(date) = details.publish_date {
|
||||||
|
cprintln!("<b>Date:</b> {}", date);
|
||||||
|
}
|
||||||
|
cprintln!("<b>Views</b>: {}", details.view_count);
|
||||||
|
if let Some(likes) = details.like_count {
|
||||||
|
cprintln!("<b>Likes:</b> {}", likes);
|
||||||
|
}
|
||||||
|
if let Some(comments) = details.top_comments.count {
|
||||||
|
cprintln!("<b>Comments:</b> {}", comments);
|
||||||
|
}
|
||||||
|
if details.is_ccommons {
|
||||||
|
cprintln!("<g>Creative Commons</g>");
|
||||||
|
}
|
||||||
|
if details.is_live {
|
||||||
|
cprintln!("<r>Livestream</r>");
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
cprint!("<c>{}</c> [{}]", author.name, author.id);
|
||||||
|
} else {
|
||||||
|
cprint!("<m>Unknown author</m>");
|
||||||
|
}
|
||||||
|
if c.by_owner {
|
||||||
|
print!(" (Owner)");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!("{}", c.text.to_plaintext());
|
||||||
|
cprint!("<b>Likes:</b> {}", c.like_count.unwrap_or_default());
|
||||||
|
if c.hearted {
|
||||||
|
cprint!(" <r>♥</r>");
|
||||||
|
}
|
||||||
|
println!("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_data(&details, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UrlTarget::Channel { id } => {
|
UrlTarget::Channel { id } => {
|
||||||
if music {
|
if music {
|
||||||
let artist = rp.query().music_artist(&id, true).await.unwrap();
|
let artist = rp.query().music_artist(&id, true).await.unwrap();
|
||||||
print_data(&artist, format, pretty);
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Artist]</k></G>\n<g><bold>{}</bold></g> [{}]",
|
||||||
|
artist.name,
|
||||||
|
artist.id
|
||||||
|
);
|
||||||
|
if let Some(subs) = artist.subscriber_count {
|
||||||
|
cprintln!("<b>Subscribers:</b> {subs}");
|
||||||
|
}
|
||||||
|
if let Some(url) = artist.wikipedia_url {
|
||||||
|
cprintln!("<b>Wikipedia:</b> {url}");
|
||||||
|
}
|
||||||
|
if let Some(id) = artist.tracks_playlist_id {
|
||||||
|
cprintln!("<b>All tracks:</b> {id}");
|
||||||
|
}
|
||||||
|
if let Some(id) = artist.videos_playlist_id {
|
||||||
|
cprintln!("<b>All videos:</b> {id}");
|
||||||
|
}
|
||||||
|
if let Some(id) = artist.radio_id {
|
||||||
|
cprintln!("<b>Radio:</b> {id}");
|
||||||
|
}
|
||||||
|
print_description(artist.description);
|
||||||
|
if !artist.albums.is_empty() {
|
||||||
|
print_h2("Albums");
|
||||||
|
for b in artist.albums {
|
||||||
|
cprint!("[{}] <b>{}</b> ({:?}", b.id, b.name, b.album_type);
|
||||||
|
if let Some(y) = b.year {
|
||||||
|
print!(", {y}");
|
||||||
|
}
|
||||||
|
println!(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !artist.playlists.is_empty() {
|
||||||
|
print_h2("Playlists");
|
||||||
|
print_entities(&artist.playlists);
|
||||||
|
}
|
||||||
|
if !artist.similar_artists.is_empty() {
|
||||||
|
print_h2("Similar artists");
|
||||||
|
print_entities(&artist.similar_artists);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_data(&artist, format, pretty);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
match tab {
|
match tab {
|
||||||
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
|
ChannelTab::Videos | ChannelTab::Shorts | ChannelTab::Live => {
|
||||||
|
|
@ -604,15 +832,74 @@ async fn main() {
|
||||||
.extend_limit(rp.query(), limit)
|
.extend_limit(rp.query(), limit)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
print_data(&channel, format, pretty);
|
|
||||||
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Channel {:?}]</k></G>\n<g><bold>{}</bold></g> [{}]",
|
||||||
|
tab,
|
||||||
|
channel.name,
|
||||||
|
channel.id
|
||||||
|
);
|
||||||
|
print_description(Some(channel.description));
|
||||||
|
if let Some(subs) = channel.subscriber_count {
|
||||||
|
cprintln!("<b>Subscribers:</b> {subs}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
print_entities(&channel.content.items);
|
||||||
|
} else {
|
||||||
|
print_data(&channel, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ChannelTab::Playlists => {
|
ChannelTab::Playlists => {
|
||||||
let channel = rp.query().channel_playlists(&id).await.unwrap();
|
let channel = rp.query().channel_playlists(&id).await.unwrap();
|
||||||
print_data(&channel, format, pretty);
|
|
||||||
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Channel {:?}]</k></G>\n<g><bold>{}</bold></g> [{}]",
|
||||||
|
tab,
|
||||||
|
channel.name,
|
||||||
|
channel.id
|
||||||
|
);
|
||||||
|
print_description(Some(channel.description));
|
||||||
|
if let Some(subs) = channel.subscriber_count {
|
||||||
|
cprintln!("<b>Subscribers:</b> {subs}");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
print_entities(&channel.content.items);
|
||||||
|
} else {
|
||||||
|
print_data(&channel, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ChannelTab::Info => {
|
ChannelTab::Info => {
|
||||||
let channel = rp.query().channel_info(&id).await.unwrap();
|
let info = rp.query().channel_info(&id).await.unwrap();
|
||||||
print_data(&channel, format, pretty);
|
|
||||||
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Channel info]</k></G>\n<b>ID:</b>{}",
|
||||||
|
info.id
|
||||||
|
);
|
||||||
|
print_description(Some(info.description));
|
||||||
|
if let Some(subs) = info.subscriber_count {
|
||||||
|
cprintln!("<b>Subscribers:</b> {subs}");
|
||||||
|
}
|
||||||
|
if let Some(vids) = info.video_count {
|
||||||
|
cprintln!("<b>Videos:</b> {vids}");
|
||||||
|
}
|
||||||
|
if let Some(views) = info.view_count {
|
||||||
|
cprintln!("<b>Views:</b> {views}");
|
||||||
|
}
|
||||||
|
if let Some(created) = info.create_date {
|
||||||
|
cprintln!("<b>Created on:</b> {created}");
|
||||||
|
}
|
||||||
|
if !info.links.is_empty() {
|
||||||
|
print_h2("Links");
|
||||||
|
for (name, url) in &info.links {
|
||||||
|
cprintln!("<b>{name}:</b> {url}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_data(&info, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -625,7 +912,26 @@ async fn main() {
|
||||||
.extend_limit(rp.query(), limit)
|
.extend_limit(rp.query(), limit)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
print_data(&playlist, format, pretty);
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[MusicPlaylist]</k></G>\n<g><bold>{}</bold></g> [{}]\n<b>Tracks:</b> {}",
|
||||||
|
playlist.name,
|
||||||
|
playlist.id,
|
||||||
|
playlist.track_count.unwrap_or_default(),
|
||||||
|
);
|
||||||
|
if let Some(n) = playlist.channel_name() {
|
||||||
|
cprint!("<b>Author:</b> {n}");
|
||||||
|
if let Some(id) = playlist.channel_id() {
|
||||||
|
print!(" [{id}]");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
print_description(playlist.description.map(|d| d.to_plaintext()));
|
||||||
|
println!();
|
||||||
|
print_tracks(&playlist.tracks.items);
|
||||||
|
} else {
|
||||||
|
print_data(&playlist, format, pretty);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let mut playlist = rp.query().playlist(&id).await.unwrap();
|
let mut playlist = rp.query().playlist(&id).await.unwrap();
|
||||||
playlist
|
playlist
|
||||||
|
|
@ -633,12 +939,54 @@ async fn main() {
|
||||||
.extend_limit(rp.query(), limit)
|
.extend_limit(rp.query(), limit)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
print_data(&playlist, format, pretty);
|
if txt {
|
||||||
|
cprintln!(
|
||||||
|
"<G><k>[Playlist]</k></G>\n<g><bold>{}</bold></g> [{}]\n<b>Videos:</b> {}",
|
||||||
|
playlist.name, playlist.id, playlist.video_count,
|
||||||
|
);
|
||||||
|
if let Some(n) = playlist.channel_name() {
|
||||||
|
cprint!("<b>Author:</b> {n}");
|
||||||
|
if let Some(id) = playlist.channel_id() {
|
||||||
|
print!(" [{id}]");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
if let Some(last_update) = playlist.last_update {
|
||||||
|
cprintln!("<b>Last update:</b> {last_update}");
|
||||||
|
}
|
||||||
|
print_description(playlist.description.map(|d| d.to_plaintext()));
|
||||||
|
println!();
|
||||||
|
print_entities(&playlist.videos.items);
|
||||||
|
} else {
|
||||||
|
print_data(&playlist, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UrlTarget::Album { id } => {
|
UrlTarget::Album { id } => {
|
||||||
let album = rp.query().music_album(&id).await.unwrap();
|
let album = rp.query().music_album(&id).await.unwrap();
|
||||||
print_data(&album, format, pretty);
|
if txt {
|
||||||
|
cprint!(
|
||||||
|
"<G><k>[Album]</k></G>\n<g><bold>{}</bold></g> [{}] ({:?}",
|
||||||
|
album.name,
|
||||||
|
album.id,
|
||||||
|
album.album_type
|
||||||
|
);
|
||||||
|
if let Some(year) = album.year {
|
||||||
|
print!(", {year}");
|
||||||
|
}
|
||||||
|
println!(")");
|
||||||
|
if let Some(n) = album.channel_name() {
|
||||||
|
cprint!("<b>Artist:</b> {}", 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -646,6 +994,7 @@ async fn main() {
|
||||||
query,
|
query,
|
||||||
format,
|
format,
|
||||||
pretty,
|
pretty,
|
||||||
|
txt,
|
||||||
limit,
|
limit,
|
||||||
item_type,
|
item_type,
|
||||||
length,
|
length,
|
||||||
|
|
@ -672,32 +1021,40 @@ async fn main() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
|
||||||
|
if txt {
|
||||||
|
if let Some(corr) = res.corrected_query {
|
||||||
|
cprintln!("Did you mean <m>`{}`</m>?", corr);
|
||||||
|
}
|
||||||
|
print_entities(&res.items.items);
|
||||||
|
} else {
|
||||||
|
print_data(&res, format, pretty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Some(MusicSearchCategory::All) => {
|
Some(MusicSearchCategory::All) => {
|
||||||
let res = rp.query().music_search_main(&query).await.unwrap();
|
let res = rp.query().music_search_main(&query).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Tracks) => {
|
Some(MusicSearchCategory::Tracks) => {
|
||||||
let mut res = rp.query().music_search_tracks(&query).await.unwrap();
|
let mut res = rp.query().music_search_tracks(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Videos) => {
|
Some(MusicSearchCategory::Videos) => {
|
||||||
let mut res = rp.query().music_search_videos(&query).await.unwrap();
|
let mut res = rp.query().music_search_videos(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Artists) => {
|
Some(MusicSearchCategory::Artists) => {
|
||||||
let mut res = rp.query().music_search_artists(&query).await.unwrap();
|
let mut res = rp.query().music_search_artists(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::Albums) => {
|
Some(MusicSearchCategory::Albums) => {
|
||||||
let mut res = rp.query().music_search_albums(&query).await.unwrap();
|
let mut res = rp.query().music_search_albums(&query).await.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
|
Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => {
|
||||||
let mut res = rp
|
let mut res = rp
|
||||||
|
|
@ -709,7 +1066,7 @@ async fn main() {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
res.items.extend_limit(rp.query(), limit).await.unwrap();
|
||||||
print_data(&res, format, pretty);
|
print_music_search(&res, format, pretty, txt);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Commands::Vdata => {
|
Commands::Vdata => {
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ pub struct DownloadQuery {
|
||||||
/// Target video format
|
/// Target video format
|
||||||
video_format: Option<DownloadVideoFormat>,
|
video_format: Option<DownloadVideoFormat>,
|
||||||
/// ClientType type for fetching videos
|
/// ClientType type for fetching videos
|
||||||
player_type: Option<ClientType>,
|
client_type: Option<ClientType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Video to be downloaded
|
/// Video to be downloaded
|
||||||
|
|
@ -163,11 +163,7 @@ impl DownloadVideo {
|
||||||
id: track.id.to_owned(),
|
id: track.id.to_owned(),
|
||||||
name: Some(track.name.to_owned()),
|
name: Some(track.name.to_owned()),
|
||||||
channel_id: track.channel_id().map(str::to_owned),
|
channel_id: track.channel_id().map(str::to_owned),
|
||||||
channel_name: if track.by_va {
|
channel_name: track.channel_name().map(str::to_owned),
|
||||||
Some("Various Artists".to_owned())
|
|
||||||
} else {
|
|
||||||
track.channel_name().map(str::to_owned)
|
|
||||||
},
|
|
||||||
album_id: track.album.as_ref().map(|b| b.id.to_owned()),
|
album_id: track.album.as_ref().map(|b| b.id.to_owned()),
|
||||||
album_name: track.album.as_ref().map(|b| b.name.to_owned()),
|
album_name: track.album.as_ref().map(|b| b.name.to_owned()),
|
||||||
track_nr: track.track_nr,
|
track_nr: track.track_nr,
|
||||||
|
|
@ -454,7 +450,7 @@ impl Downloader {
|
||||||
progress: None,
|
progress: None,
|
||||||
filter: None,
|
filter: None,
|
||||||
video_format: None,
|
video_format: None,
|
||||||
player_type: None,
|
client_type: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -589,8 +585,8 @@ impl DownloadQuery {
|
||||||
|
|
||||||
/// Set the [`ClientType`] used to fetch the YT player
|
/// Set the [`ClientType`] used to fetch the YT player
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn player_type(mut self, player_type: ClientType) -> Self {
|
pub fn client_type(mut self, client_type: ClientType) -> Self {
|
||||||
self.player_type = Some(player_type);
|
self.client_type = Some(client_type);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -684,8 +680,8 @@ impl DownloadQuery {
|
||||||
}
|
}
|
||||||
|
|
||||||
let q = self.dl.i.rp.query();
|
let q = self.dl.i.rp.query();
|
||||||
let player_data = match self.player_type {
|
let player_data = match self.client_type {
|
||||||
Some(player_type) => q.player_from_client(&self.video.id, player_type).await?,
|
Some(client_type) => q.player_from_client(&self.video.id, client_type).await?,
|
||||||
None => q.player(&self.video.id).await?,
|
None => q.player(&self.video.id).await?,
|
||||||
};
|
};
|
||||||
let user_agent = q.user_agent(player_data.client_type);
|
let user_agent = q.user_agent(player_data.client_type);
|
||||||
|
|
|
||||||
Reference in a new issue