2022-09-21 13:48:49 +00:00
|
|
|
//! # Steam RSS
|
|
|
|
//!
|
|
|
|
//! > **Get RSS feeds for Steam games.**
|
|
|
|
//!
|
|
|
|
//! *AGPL-3.0-or-later*
|
|
|
|
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
#![warn(missing_docs, clippy::missing_docs_in_private_items)]
|
|
|
|
|
|
|
|
use std::{thread::sleep, time::Duration};
|
|
|
|
|
|
|
|
use {
|
|
|
|
clap::Parser,
|
|
|
|
color_eyre::{install, Result},
|
|
|
|
indicatif::{ProgressBar, ProgressStyle},
|
2022-09-21 21:37:17 +00:00
|
|
|
regex::Regex,
|
2022-09-21 13:48:49 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/// CLI arguments struct using [`clap`]'s Derive API.
|
|
|
|
#[derive(Debug, Parser)]
|
|
|
|
#[clap(about, author, version)]
|
|
|
|
pub struct Args {
|
|
|
|
/// A game's AppID, can be used multiple times.
|
|
|
|
#[clap(short, long)]
|
|
|
|
pub appid: Vec<usize>,
|
|
|
|
|
2022-09-21 14:18:53 +00:00
|
|
|
/// Output the feeds as OPML.
|
|
|
|
#[clap(long)]
|
|
|
|
pub opml: bool,
|
|
|
|
|
2022-09-21 13:48:49 +00:00
|
|
|
/// The time in milliseconds to sleep between HTTP requests.
|
|
|
|
#[clap(short, long, default_value = "250")]
|
|
|
|
pub timeout: u64,
|
|
|
|
|
|
|
|
/// Verify potential feeds by downloading them and checking if they return XML.
|
|
|
|
#[clap(short, long)]
|
|
|
|
pub verify: bool,
|
2022-09-21 21:37:17 +00:00
|
|
|
|
|
|
|
/// A game's store URL, can be used multiple times.
|
|
|
|
#[clap(long)]
|
|
|
|
pub url: Vec<String>,
|
2022-09-21 13:48:49 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 19:56:34 +00:00
|
|
|
/// A simple feed struct.
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Feed {
|
2022-09-21 20:52:16 +00:00
|
|
|
/// The CLI option that was used for this feed.
|
|
|
|
pub option: FeedOption,
|
|
|
|
|
2022-09-21 19:56:34 +00:00
|
|
|
/// The text to use for the feed in the OPML output.
|
|
|
|
pub text: Option<String>,
|
|
|
|
|
|
|
|
/// The URL of the feed.
|
|
|
|
pub url: String,
|
|
|
|
}
|
|
|
|
|
2022-09-21 20:52:16 +00:00
|
|
|
/// An enum for [`Feed`]s for which option was used in the CLI.
|
2022-09-21 21:50:43 +00:00
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
2022-09-21 20:52:16 +00:00
|
|
|
pub enum FeedOption {
|
|
|
|
/// `-a, --appid <APPID>` was used.
|
|
|
|
AppID,
|
2022-09-21 21:37:17 +00:00
|
|
|
|
|
|
|
/// `--url <URL>` was used.
|
|
|
|
Url,
|
2022-09-21 20:52:16 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 13:48:49 +00:00
|
|
|
fn main() -> Result<()> {
|
|
|
|
install()?;
|
|
|
|
|
|
|
|
let args = Args::parse();
|
|
|
|
let timeout = Duration::from_millis(args.timeout);
|
|
|
|
|
|
|
|
let ureq_agent = ureq::AgentBuilder::new()
|
|
|
|
.user_agent("Steam Feeds (https://git.bauke.xyz/Bauke/steam-rss)")
|
|
|
|
.build();
|
|
|
|
let mut potential_feeds = vec![];
|
|
|
|
let mut feeds_to_output = vec![];
|
|
|
|
|
2022-09-21 21:37:17 +00:00
|
|
|
let store_url_regex =
|
|
|
|
Regex::new(r"(?i)^https?://store.steampowered.com/app/(?P<appid>\d+)")?;
|
|
|
|
|
2022-09-21 13:48:49 +00:00
|
|
|
for appid in args.appid {
|
2022-09-21 19:56:34 +00:00
|
|
|
potential_feeds.push(Feed {
|
2022-09-21 20:52:16 +00:00
|
|
|
option: FeedOption::AppID,
|
2022-09-21 19:56:34 +00:00
|
|
|
text: Some(format!("Steam AppID {appid}")),
|
2022-09-21 21:37:17 +00:00
|
|
|
url: appid_to_rss_url(appid),
|
2022-09-21 19:56:34 +00:00
|
|
|
});
|
2022-09-21 13:48:49 +00:00
|
|
|
}
|
|
|
|
|
2022-09-21 21:37:17 +00:00
|
|
|
for url in args.url {
|
|
|
|
let appid = store_url_regex
|
|
|
|
.captures(&url)
|
|
|
|
.and_then(|captures| captures.name("appid"))
|
|
|
|
.and_then(|appid_match| appid_match.as_str().parse::<usize>().ok());
|
|
|
|
if let Some(appid) = appid {
|
|
|
|
potential_feeds.push(Feed {
|
|
|
|
option: FeedOption::Url,
|
|
|
|
text: Some(format!("Steam AppID {appid}")),
|
|
|
|
url: appid_to_rss_url(appid),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-21 13:48:49 +00:00
|
|
|
if args.verify {
|
|
|
|
let progress = ProgressBar::new(potential_feeds.len().try_into()?)
|
|
|
|
.with_style(ProgressStyle::with_template("Verifying {pos}/{len} {bar}")?);
|
|
|
|
|
|
|
|
for potential_feed in potential_feeds {
|
2022-09-21 21:37:17 +00:00
|
|
|
let potential_feed = if [FeedOption::AppID, FeedOption::Url]
|
|
|
|
.contains(&potential_feed.option)
|
|
|
|
{
|
2022-09-21 20:57:31 +00:00
|
|
|
let response = ureq_agent.get(&potential_feed.url).call()?;
|
2022-09-22 10:36:51 +00:00
|
|
|
sleep(timeout);
|
2022-09-21 20:57:31 +00:00
|
|
|
if response.content_type() == "text/xml" {
|
|
|
|
let body = response.into_string()?;
|
|
|
|
let title_start = body.find("<title>").unwrap() + 7;
|
|
|
|
let title_end = body.find("</title>").unwrap();
|
|
|
|
Feed {
|
|
|
|
text: Some(body[title_start..title_end].to_string()),
|
|
|
|
..potential_feed
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
|
|
|
|
feeds_to_output.push(potential_feed);
|
2022-09-21 13:48:49 +00:00
|
|
|
progress.inc(1);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
feeds_to_output = potential_feeds;
|
|
|
|
}
|
|
|
|
|
2022-09-21 21:50:43 +00:00
|
|
|
let mut opml_document = opml::OPML {
|
|
|
|
head: None,
|
|
|
|
..Default::default()
|
|
|
|
};
|
2022-09-21 20:07:01 +00:00
|
|
|
|
2022-09-21 13:48:49 +00:00
|
|
|
for feed in feeds_to_output {
|
2022-09-21 14:18:53 +00:00
|
|
|
if args.opml {
|
2022-09-21 19:56:34 +00:00
|
|
|
opml_document
|
|
|
|
.add_feed(&feed.text.unwrap_or_else(|| feed.url.clone()), &feed.url);
|
2022-09-21 14:18:53 +00:00
|
|
|
} else {
|
2022-09-21 19:56:34 +00:00
|
|
|
println!("{}", feed.url);
|
2022-09-21 14:18:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if args.opml {
|
|
|
|
println!("{}", opml_document.to_string()?);
|
2022-09-21 13:48:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2022-09-21 21:26:46 +00:00
|
|
|
|
|
|
|
/// Creates a Steam RSS URL from a given AppID.
|
2022-09-21 21:47:27 +00:00
|
|
|
fn appid_to_rss_url<D: std::fmt::Display>(appid: D) -> String {
|
2022-09-21 21:26:46 +00:00
|
|
|
format!("https://steamcommunity.com/games/{appid}/rss/")
|
|
|
|
}
|