feat: export subscriptions as OPML / NewPipe JSON
This commit is contained in:
parent
50f5c4b84a
commit
638a22d012
2 changed files with 119 additions and 4 deletions
|
|
@ -48,6 +48,7 @@ tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
|
||||||
futures-util.workspace = true
|
futures-util.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
quick-xml.workspace = true
|
||||||
|
|
||||||
indicatif.workspace = true
|
indicatif.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
|
||||||
122
cli/src/main.rs
122
cli/src/main.rs
|
|
@ -223,7 +223,7 @@ enum Commands {
|
||||||
Subscriptions {
|
Subscriptions {
|
||||||
/// Output format
|
/// Output format
|
||||||
#[clap(short, long, value_parser)]
|
#[clap(short, long, value_parser)]
|
||||||
format: Option<Format>,
|
format: Option<SubscriptionFormat>,
|
||||||
/// Pretty-print output
|
/// Pretty-print output
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
pretty: bool,
|
pretty: bool,
|
||||||
|
|
@ -325,6 +325,15 @@ enum Format {
|
||||||
Yaml,
|
Yaml,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Copy, Clone, ValueEnum)]
|
||||||
|
enum SubscriptionFormat {
|
||||||
|
#[default]
|
||||||
|
Json,
|
||||||
|
Yaml,
|
||||||
|
Newpipe,
|
||||||
|
Opml,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, ValueEnum)]
|
#[derive(Debug, Copy, Clone, ValueEnum)]
|
||||||
enum ChannelTab {
|
enum ChannelTab {
|
||||||
Videos,
|
Videos,
|
||||||
|
|
@ -402,6 +411,20 @@ enum ClientTypeArg {
|
||||||
Ios,
|
Ios,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct NewpipeSubscriptions {
|
||||||
|
app_version: &'static str,
|
||||||
|
app_version_int: u16,
|
||||||
|
subscriptions: Vec<NewpipeSubscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct NewpipeSubscription {
|
||||||
|
service_id: u16,
|
||||||
|
url: String,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
impl From<SearchItemType> for search_filter::ItemType {
|
impl From<SearchItemType> for search_filter::ItemType {
|
||||||
fn from(value: SearchItemType) -> Self {
|
fn from(value: SearchItemType) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
|
@ -456,6 +479,16 @@ impl From<ClientTypeArg> for ClientType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<SubscriptionFormat> for Format {
|
||||||
|
fn from(value: SubscriptionFormat) -> Self {
|
||||||
|
match value {
|
||||||
|
SubscriptionFormat::Json => Self::Json,
|
||||||
|
SubscriptionFormat::Yaml => Self::Yaml,
|
||||||
|
_ => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
fn print_data<T: Serialize>(data: &T, format: Format, pretty: bool) {
|
||||||
let stdout = std::io::stdout().lock();
|
let stdout = std::io::stdout().lock();
|
||||||
match format {
|
match format {
|
||||||
|
|
@ -514,6 +547,77 @@ fn fmt_print_tracks(tracks: &[TrackItem], format: Option<Format>, pretty: bool,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fmt_print_subscriptions<T: YtEntity + Serialize>(
|
||||||
|
items: &[T],
|
||||||
|
format: Option<SubscriptionFormat>,
|
||||||
|
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]) {
|
fn print_tracks(tracks: &[TrackItem]) {
|
||||||
for t in tracks {
|
for t in tracks {
|
||||||
if let Some(n) = t.track_nr {
|
if let Some(n) = t.track_nr {
|
||||||
|
|
@ -1507,15 +1611,25 @@ async fn run() -> anyhow::Result<()> {
|
||||||
if music {
|
if music {
|
||||||
let mut subscriptions = rp.query().music_saved_artists().await?;
|
let mut subscriptions = rp.query().music_saved_artists().await?;
|
||||||
subscriptions.extend_limit(rp.query(), limit).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 {
|
} else if feed {
|
||||||
let mut feed = rp.query().subscription_feed().await?;
|
let mut feed = rp.query().subscription_feed().await?;
|
||||||
feed.extend_limit(rp.query(), limit).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 {
|
} else {
|
||||||
let mut subscriptions = rp.query().subscriptions().await?;
|
let mut subscriptions = rp.query().subscriptions().await?;
|
||||||
subscriptions.extend_limit(rp.query(), limit).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 {
|
Commands::Playlists {
|
||||||
|
|
|
||||||
Reference in a new issue