feat: export subscriptions as OPML / NewPipe JSON
This commit is contained in:
parent
a1b43ad70a
commit
c90d966b17
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
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
quick-xml.workspace = true
|
||||
|
||||
indicatif.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
|
|
|||
122
cli/src/main.rs
122
cli/src/main.rs
|
|
@ -223,7 +223,7 @@ enum Commands {
|
|||
Subscriptions {
|
||||
/// Output format
|
||||
#[clap(short, long, value_parser)]
|
||||
format: Option<Format>,
|
||||
format: Option<SubscriptionFormat>,
|
||||
/// 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<NewpipeSubscription>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct NewpipeSubscription {
|
||||
service_id: u16,
|
||||
url: String,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl From<SearchItemType> for search_filter::ItemType {
|
||||
fn from(value: SearchItemType) -> Self {
|
||||
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) {
|
||||
let stdout = std::io::stdout().lock();
|
||||
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]) {
|
||||
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 {
|
||||
|
|
|
|||
Reference in a new issue