diff --git a/README.md b/README.md index 15f6876..9b5646c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ # ![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) +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe.svg)](https://crates.io/crates/rustypipe) [![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). +RustyPipe is a fully featured Rust client for the public YouTube / YouTube Music API +(Innertube), inspired by [NewPipe](https://github.com/TeamNewPipe/NewPipeExtractor). ## Features diff --git a/cli/README.md b/cli/README.md index e88277e..4cb7d8a 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1 +1,94 @@ -# RustyPipe CLI +# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) CLI + +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-cli.svg)](https://crates.io/crates/rustypipe-cli) +[![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) + +The RustyPipe CLI is a powerful YouTube client for the command line. It allows you to +access most of the features of the RustyPipe crate: getting data from YouTube and +downloading videos. + +The following subcommands are included: + +## `get`: Fetch information + +You can call the get command with any YouTube entity ID or URL and RustyPipe will fetch +the associated metadata. It can fetch channels, playlists, albums and videos. + +**Usage:** `rustypipe get UC2TXq_t06Hjdr2g_KdKpHQg` + +- `-l`, `--limit` Limit the number of list items to fetch +- ``-t, --tab` Channel tab (options: **videos**, shorts, live, playlists, info) +- `-m, --music` Use the YouTube Music API +- `--rss`Fetch the RSS feed of a channel +- `--comments` Get comments (options: top, latest) +- `--lyrics` Get the lyrics for YTM tracks +- `--player` Get the player data instead of the video details when fetching videos +- `-c, --client-type` YT clients used to fetch player data (options: desktop, tv, + tv-embed, android, ios; if multiple clients are specified, they are attempted in + order) + +## `search`: Search YouTube + +With the search command you can search the entire YouTube platform or individual +channels. YouTube Music search is also supported. + +Note that search filters are only supported when searching YouTube. They have no effect +when searching YTM or individual channels. + +**Usage:** `rustypipe search "query"` + +### Options + +- `-l`, `--limit` Limit the number of list items to fetch + +- `--item-type` Filter results by item type +- `--length` Filter results by video length +- `--date` Filter results by upload date (options: hour, day, week, month, year) +- `--order` Sort search results (options: rating, date, views) +- `--channel` Channel ID for searching channel videos +- `-m, --music` Search YouTube Music in the given category (options: all, tracks, + videos, artists, albums, playlists-ytm, playlists-community) + +## `dl`: Download videos + +The downloader can download individual videos, playlists, albums and channels. Multiple +videos can be downloaded in parallel for improved performance. + +**Usage:** `rustypipe dl eRsGyueVLvQ` + +### Options + +- `-o`, `--output` Download to the given directory +- `--output-file` Download to the given file +- `--template` Download to a path determined by a template + +- `-r`, `--resolution` Video resolution (e.g. 720, 1080). Set to 0 for audio-only +- `-a`, `--audio` Download only the audio track and write track metadata + album cover +- `-p`, `--parallel` Number of videos downloaded in parallel (default: 8) +- `-m, --music` Use YouTube Music for downloading playlists +- `-l`, `--limit` Limit the number of videos to download (default: 1000) +- `-c`, `--client-type` YT clients used to fetch player data (options: desktop, tv, + tv-embed, android, ios; if multiple clients are specified, they are attempted in + order) +- `--pot` token to circumvent bot detection + +## `vdata`: Get visitor data + +You can use the vdata command to get a new visitor data cookie. This feature may come in +handy for testing and reproducing A/B tests. + +## Global options + +- **Proxy:** RustyPipe respects the environment variables `HTTP_PROXY`, `HTTPS_PROXY` + and `ALL_PROXY` +- **Logging:** You can change the log level with the `RUST_LOG` environment variable, it + is set to `info` by default +- **Visitor data:** A custom visitor data cookie can be used with the `--vdata` flag +- `--report` + +### Output format + +By default, the CLI outputs YouTube data in a human-readable text format. If you want to +store the data or process it with a script, you should choose a machine readable output +format. You can choose both JSON and YAML with the `-f, --format` flag. diff --git a/cli/src/main.rs b/cli/src/main.rs index 96e3925..5c4c249 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -49,10 +49,13 @@ struct Cli { #[derive(Parser)] #[group(multiple = false)] struct DownloadTarget { + /// Download to the given directory #[clap(short, long)] output: Option, + /// Download to the given file #[clap(long)] output_file: Option, + /// Download to a path determined by a template #[clap(long)] template: Option, } @@ -93,7 +96,7 @@ enum Commands { /// Video resolution (e.g. 720, 1080). Set to 0 for audio-only. #[clap(short, long)] resolution: Option, - /// Download only the audio track + /// Download only the audio track and write track information #[clap(short, long)] audio: bool, /// Number of videos downloaded in parallel @@ -117,24 +120,21 @@ enum Commands { /// ID or URL id: String, /// Output format - #[clap(long, value_parser, default_value = "json")] - format: Format, + #[clap(short, long, value_parser)] + format: Option, /// Pretty-print output #[clap(long)] pretty: bool, - /// Output as text - #[clap(short, long)] - txt: bool, /// Limit the number of items to fetch #[clap(short, long, default_value_t = 20)] limit: usize, /// Channel tab - #[clap(long, default_value = "videos")] + #[clap(short, long, default_value = "videos")] tab: ChannelTab, /// Use YouTube Music #[clap(short, long)] music: bool, - /// Use the RSS feed of a channel + /// Fetch the RSS feed of a channel #[clap(long)] rss: bool, /// Get comments @@ -148,21 +148,18 @@ enum Commands { player: bool, /// YT Client used to fetch player data #[clap(short, long)] - client_type: Option, + client_type: Option>, }, /// Search YouTube Search { /// Search query query: String, /// Output format - #[clap(long, value_parser, default_value = "json")] - format: Format, + #[clap(short, long, value_parser)] + format: Option, /// Pretty-print output #[clap(long)] pretty: bool, - /// Output as text - #[clap(short, long)] - txt: bool, /// Limit the number of items to fetch #[clap(short, long, default_value_t = 20)] limit: usize, @@ -181,7 +178,7 @@ enum Commands { /// Channel ID for searching channel videos #[clap(long)] channel: Option, - /// YouTube Music search filter + /// Search YouTube Music in the given category #[clap(short, long)] music: Option, }, @@ -189,8 +186,9 @@ enum Commands { Vdata, } -#[derive(Copy, Clone, ValueEnum)] +#[derive(Default, Copy, Clone, ValueEnum)] enum Format { + #[default] Json, Yaml, } @@ -388,17 +386,17 @@ fn print_duration(duration: Option) { fn print_music_search( data: &MusicSearchResult, - format: Format, + format: Option, pretty: bool, - txt: bool, ) { - if txt { - if let Some(corr) = &data.corrected_query { - anstream::println!("Did you mean `{}`?", corr.magenta()); + match format { + Some(format) => print_data(data, format, pretty), + None => { + if let Some(corr) = &data.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); + } + print_entities(&data.items.items); } - print_entities(&data.items.items); - } else { - print_data(data, format, pretty) } } @@ -406,7 +404,7 @@ fn print_description(desc: Option) { if let Some(desc) = desc { if !desc.is_empty() { print_h2("Description"); - println!("{}", desc); + println!("{}", desc.trim()); } } } @@ -651,7 +649,6 @@ async fn run() -> anyhow::Result<()> { Commands::Get { id, format, - txt, pretty, limit, tab, @@ -671,56 +668,60 @@ async fn run() -> anyhow::Result<()> { match details.lyrics_id { Some(lyrics_id) => { let lyrics = rp.query().music_lyrics(lyrics_id).await?; - if txt { - println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()); - } else { - print_data(&lyrics, format, pretty); + match format { + Some(format) => print_data(&lyrics, format, pretty), + None => println!("{}\n\n{}", lyrics.body, lyrics.footer.blue()), } } None => eprintln!("no lyrics found"), } } else if music { let details = rp.query().music_details(&id).await?; - if txt { - if details.track.is_video { - anstream::println!("{}", "[MV]".on_green().black()); - } else { - anstream::println!("{}", "[Track]".on_green().black()); + match format { + Some(format) => print_data(&details, format, pretty), + None => { + if details.track.is_video { + anstream::println!("{}", "[MV]".on_green().black()); + } else { + anstream::println!("{}", "[Track]".on_green().black()); + } + anstream::print!( + "{} [{}]", + details.track.name.green().bold(), + details.track.id + ); + print_duration(details.track.duration); + println!(); + print_artists(&details.track.artists); + println!(); + if !details.track.is_video { + anstream::println!( + "{} {}", + "Album:".blue(), + details + .track + .album + .as_ref() + .map(|b| b.id.as_str()) + .unwrap_or("None") + ) + } + if let Some(view_count) = details.track.view_count { + anstream::println!("{} {}", "Views:".blue(), view_count); + } } - anstream::print!( - "{} [{}]", - details.track.name.green().bold(), - details.track.id - ); - print_duration(details.track.duration); - println!(); - print_artists(&details.track.artists); - println!(); - if !details.track.is_video { - anstream::println!( - "{} {}", - "Album:".blue(), - details - .track - .album - .as_ref() - .map(|b| b.id.as_str()) - .unwrap_or("None") - ) - } - if let Some(view_count) = details.track.view_count { - anstream::println!("{} {}", "Views:".blue(), view_count); - } - } else { - print_data(&details, format, pretty); } } else if player { - let player = if let Some(client_type) = client_type { - rp.query().player_from_client(&id, client_type.into()).await + let player = if let Some(client_types) = client_type { + let cts = client_types + .into_iter() + .map(ClientType::from) + .collect::>(); + rp.query().player_from_clients(&id, &cts).await } else { rp.query().player(&id).await }?; - print_data(&player, format, pretty); + print_data(&player, format.unwrap_or_default(), pretty); } else { let mut details = rp.query().video_details(&id).await?; @@ -737,153 +738,160 @@ async fn run() -> anyhow::Result<()> { None => {} } - 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()); + match format { + Some(format) => print_data(&details, format, pretty), + None => { + 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"); } - 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?; - 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}"); + match format { + Some(format) => print_data(&artist, format, pretty), + None => { + 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!(")"); } - 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); } } - 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 if rss { let rss = rp.query().channel_rss(&id).await?; - if txt { - anstream::println!( - "{}\n{} [{}]\n{} {}", - "[Channel RSS]".on_green().black(), - rss.name.green().bold(), - rss.id, - "Created on:".blue(), - rss.create_date, - ); - if let Some(v) = rss.videos.first() { + match format { + Some(format) => print_data(&rss, format, pretty), + None => { anstream::println!( - "{} {} [{}]", - "Latest video:".blue(), - v.publish_date, - v.id + "{}\n{} [{}]\n{} {}", + "[Channel RSS]".on_green().black(), + rss.name.green().bold(), + rss.id, + "Created on:".blue(), + rss.create_date, ); + if let Some(v) = rss.videos.first() { + anstream::println!( + "{} {} [{}]", + "Latest video:".blue(), + v.publish_date, + v.id + ); + } + println!(); + print_entities(&rss.videos); } - println!(); - print_entities(&rss.videos); - } else { - print_data(&rss, format, pretty); } } else { match tab { @@ -899,75 +907,94 @@ async fn run() -> anyhow::Result<()> { channel.content.extend_limit(rp.query(), limit).await?; - 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); + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + 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); } - 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?; - 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); + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + 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); } - println!(); - print_entities(&channel.content.items); - } else { - print_data(&channel, format, pretty); } } ChannelTab::Info => { let info = rp.query().channel_info(&id).await?; - if txt { - anstream::println!( - "{}\nID:{}", - "[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); + match format { + Some(format) => print_data(&info, format, pretty), + None => { + anstream::println!( + "{}\nID:{}", + "[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); } } } @@ -977,83 +1004,86 @@ async fn run() -> anyhow::Result<()> { if music { let mut playlist = rp.query().music_playlist(&id).await?; playlist.tracks.extend_limit(rp.query(), limit).await?; - 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}]"); + match format { + Some(format) => print_data(&playlist, format, pretty), + None => { + 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); } - 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?; playlist.videos.extend_limit(rp.query(), limit).await?; - 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}]"); + match format { + Some(format) => print_data(&playlist, format, pretty), + None => { + 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); } - 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?; - 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}]"); + match format { + Some(format) => print_data(&album, format, pretty), + None => { + 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); } - print_description(album.description.map(|d| d.to_plaintext())); - println!(); - print_tracks(&album.tracks); - } else { - print_data(&album, format, pretty); } } } @@ -1062,7 +1092,6 @@ async fn run() -> anyhow::Result<()> { query, format, pretty, - txt, limit, item_type, length, @@ -1072,10 +1101,29 @@ async fn run() -> anyhow::Result<()> { music, } => match music { None => match channel { - Some(channel) => { - rustypipe::validate::channel_id(&channel)?; - let res = rp.query().channel_search(&channel, &query).await?; - print_data(&res, format, pretty); + Some(channel_id) => { + rustypipe::validate::channel_id(&channel_id)?; + let channel = rp.query().channel_search(&channel_id, &query).await?; + + match format { + Some(format) => print_data(&channel, format, pretty), + None => { + anstream::print!( + "{}\n{} [{}]", + "[Channel search]".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); + } + } } None => { let filter = search_filter::SearchFilter::new() @@ -1089,39 +1137,40 @@ async fn run() -> anyhow::Result<()> { .await?; res.items.extend_limit(rp.query(), limit).await?; - if txt { - if let Some(corr) = res.corrected_query { - anstream::println!("Did you mean `{}`?", corr.magenta()); + match format { + Some(format) => print_data(&res, format, pretty), + None => { + if let Some(corr) = res.corrected_query { + anstream::println!("Did you mean `{}`?", corr.magenta()); + } + print_entities(&res.items.items); } - print_entities(&res.items.items); - } else { - print_data(&res, format, pretty); } } }, Some(MusicSearchCategory::All) => { let res = rp.query().music_search_main(&query).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Tracks) => { let mut res = rp.query().music_search_tracks(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Videos) => { let mut res = rp.query().music_search_videos(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Artists) => { let mut res = rp.query().music_search_artists(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::Albums) => { let mut res = rp.query().music_search_albums(&query).await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } Some(MusicSearchCategory::PlaylistsYtm | MusicSearchCategory::PlaylistsCommunity) => { let mut res = rp @@ -1132,7 +1181,7 @@ async fn run() -> anyhow::Result<()> { ) .await?; res.items.extend_limit(rp.query(), limit).await?; - print_music_search(&res, format, pretty, txt); + print_music_search(&res, format, pretty); } }, Commands::Vdata => { diff --git a/downloader/README.md b/downloader/README.md index 24a554a..05ec1a8 100644 --- a/downloader/README.md +++ b/downloader/README.md @@ -1,4 +1,8 @@ -# RustyPipe downloader +# ![RustyPipe](https://code.thetadev.de/ThetaDev/rustypipe/raw/branch/main/notes/logo.svg) Downloader + +[![Current crates.io version](https://img.shields.io/crates/v/rustypipe-downloader.svg)](https://crates.io/crates/rustypipe-downloader) +[![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) The downloader is a companion crate for RustyPipe that allows for easy and fast downloading of video and audio files. @@ -35,8 +39,8 @@ let dl = DownloaderBuilder::new() .build(); let filter_audio = StreamFilter::new().no_video(); -dl.id("ZeerrnuLi5E").stream_filter(filter_audio).to_file("audio.opus").download().await; +dl.id("eRsGyueVLvQ").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; +dl.id("eRsGyueVLvQ").stream_filter(filter_video).to_file("video.mp4").download().await; ```