todo: update metadata

This commit is contained in:
ThetaDev 2024-08-17 00:10:31 +02:00
parent abb783219a
commit 8692ca81d9
No known key found for this signature in database
GPG key ID: E319D3C5148D65B6
4 changed files with 477 additions and 331 deletions

View file

@ -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

View file

@ -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.

View file

@ -49,10 +49,13 @@ struct Cli {
#[derive(Parser)]
#[group(multiple = false)]
struct DownloadTarget {
/// Download to the given directory
#[clap(short, long)]
output: Option<PathBuf>,
/// Download to the given file
#[clap(long)]
output_file: Option<PathBuf>,
/// Download to a path determined by a template
#[clap(long)]
template: Option<String>,
}
@ -93,7 +96,7 @@ 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
/// 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<Format>,
/// 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<ClientTypeArg>,
client_type: Option<Vec<ClientTypeArg>>,
},
/// 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<Format>,
/// 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<String>,
/// YouTube Music search filter
/// Search YouTube Music in the given category
#[clap(short, long)]
music: Option<MusicSearchCategory>,
},
@ -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<u32>) {
fn print_music_search<T: Serialize + YtEntity>(
data: &MusicSearchResult<T>,
format: Format,
format: Option<Format>,
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<String>) {
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::<Vec<_>>();
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!(
"{}\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);
match format {
Some(format) => print_data(&info, format, pretty),
None => {
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);
}
}
}
@ -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 => {

View file

@ -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;
```