diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1ed00f6..02761bc 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -48,6 +48,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } futures-util.workspace = true serde.workspace = true serde_json.workspace = true +quick-xml.workspace = true indicatif.workspace = true anyhow.workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs index 5f216de..fee4670 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -223,7 +223,7 @@ enum Commands { Subscriptions { /// Output format #[clap(short, long, value_parser)] - format: Option, + format: Option, /// Pretty-print output #[clap(long)] pretty: bool, @@ -325,6 +325,15 @@ enum Format { Yaml, } +#[derive(Debug, Default, Copy, Clone, ValueEnum)] +enum SubscriptionFormat { + #[default] + Json, + Yaml, + Newpipe, + Opml, +} + #[derive(Debug, Copy, Clone, ValueEnum)] enum ChannelTab { Videos, @@ -402,6 +411,20 @@ enum ClientTypeArg { Ios, } +#[derive(Serialize)] +struct NewpipeSubscriptions { + app_version: &'static str, + app_version_int: u16, + subscriptions: Vec, +} + +#[derive(Serialize)] +struct NewpipeSubscription { + service_id: u16, + url: String, + name: String, +} + impl From for search_filter::ItemType { fn from(value: SearchItemType) -> Self { match value { @@ -456,6 +479,16 @@ impl From for ClientType { } } +impl From for Format { + fn from(value: SubscriptionFormat) -> Self { + match value { + SubscriptionFormat::Json => Self::Json, + SubscriptionFormat::Yaml => Self::Yaml, + _ => Self::default(), + } + } +} + fn print_data(data: &T, format: Format, pretty: bool) { let stdout = std::io::stdout().lock(); match format { @@ -514,6 +547,77 @@ fn fmt_print_tracks(tracks: &[TrackItem], format: Option, pretty: bool, } } +fn fmt_print_subscriptions( + items: &[T], + format: Option, + pretty: bool, + title: &str, +) { + match format { + Some(SubscriptionFormat::Newpipe) => { + let subscriptions = items + .iter() + .map(|itm| NewpipeSubscription { + service_id: 0, + url: format!("https://www.youtube.com/channel/{}", itm.id()), + name: itm.name().to_owned(), + }) + .collect(); + let data = NewpipeSubscriptions { + app_version: "0.24.1", + app_version_int: 991, + subscriptions, + }; + print_data(&data, Format::Json, pretty); + } + Some(SubscriptionFormat::Opml) => { + let mut writer = if pretty { + quick_xml::Writer::new_with_indent(std::io::stdout(), b' ', 2) + } else { + quick_xml::Writer::new(std::io::stdout()) + }; + writer + .create_element("opml") + .with_attribute(("version", "1.1")) + .write_inner_content(|writer| { + writer + .create_element("body") + .write_inner_content(|writer| { + writer + .create_element("outline") + .with_attributes([ + ("text", title), + ("title", title), + ]) + .write_inner_content(|writer| { + for itm in items { + writer + .create_element("outline") + .with_attributes([ + ("text", itm.name()), + ("title", itm.name()), + ("type", "rss"), + ("xmlUrl", &format!("https://www.youtube.com/feeds/videos.xml?channel_id={}", itm.id())), + ]) + .write_empty()?; + } + Ok(()) + })?; + Ok(()) + })?; + Ok(()) + }) + .unwrap(); + println!(); + } + Some(format) => print_data(&items, format.into(), pretty), + None => { + print_h1(title); + print_entities(items, false); + } + } +} + fn print_tracks(tracks: &[TrackItem]) { for t in tracks { if let Some(n) = t.track_nr { @@ -1507,15 +1611,25 @@ async fn run() -> anyhow::Result<()> { if music { let mut subscriptions = rp.query().music_saved_artists().await?; subscriptions.extend_limit(rp.query(), limit).await?; - fmt_print_entities(&subscriptions.items, format, pretty, "Music artists"); + fmt_print_subscriptions( + &subscriptions.items, + format, + pretty, + "YouTube Music artists", + ); } else if feed { let mut feed = rp.query().subscription_feed().await?; feed.extend_limit(rp.query(), limit).await?; - fmt_print_entities(&feed.items, format, pretty, "Feed"); + fmt_print_entities(&feed.items, format.map(Format::from), pretty, "Feed"); } else { let mut subscriptions = rp.query().subscriptions().await?; subscriptions.extend_limit(rp.query(), limit).await?; - fmt_print_entities(&subscriptions.items, format, pretty, "Subscriptions"); + fmt_print_subscriptions( + &subscriptions.items, + format, + pretty, + "YouTube subscriptions", + ); } } Commands::Playlists {