Compare commits
6 Commits
1e9aa887f7
...
f56ff3e06f
Author | SHA1 | Date |
---|---|---|
Bauke | f56ff3e06f | |
Bauke | a9d68ea859 | |
Bauke | e979671047 | |
Bauke | 252fb17d56 | |
Bauke | e543f0aac0 | |
Bauke | f49e12db37 |
|
@ -160,20 +160,6 @@ dependencies = [
|
|||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"terminal_size",
|
||||
"unicode-width",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
|
@ -183,12 +169,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.8"
|
||||
|
@ -296,15 +276,10 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.17.1"
|
||||
name = "itoa"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b"
|
||||
dependencies = [
|
||||
"console",
|
||||
"number_prefix",
|
||||
"unicode-width",
|
||||
]
|
||||
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
||||
|
||||
[[package]]
|
||||
name = "jetscii"
|
||||
|
@ -357,12 +332,6 @@ dependencies = [
|
|||
"adler",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "number_prefix"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.29.0"
|
||||
|
@ -505,6 +474,12 @@ dependencies = [
|
|||
"webpki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.0"
|
||||
|
@ -535,6 +510,17 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.85"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.4"
|
||||
|
@ -556,9 +542,10 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"indicatif",
|
||||
"opml",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
|
@ -588,16 +575,6 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.1"
|
||||
|
@ -711,12 +688,6 @@ dependencies = [
|
|||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
|
|
|
@ -13,9 +13,10 @@ path = "source/main.rs"
|
|||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.2"
|
||||
indicatif = "0.17.1"
|
||||
opml = "1.1.4"
|
||||
regex = "1.6.0"
|
||||
serde = "1.0.144"
|
||||
serde_json = "1.0.85"
|
||||
ureq = "2.5.0"
|
||||
|
||||
[dependencies.clap]
|
||||
|
|
150
source/main.rs
150
source/main.rs
|
@ -12,8 +12,9 @@ use std::{thread::sleep, time::Duration};
|
|||
use {
|
||||
clap::Parser,
|
||||
color_eyre::{install, Result},
|
||||
indicatif::{ProgressBar, ProgressStyle},
|
||||
regex::Regex,
|
||||
serde::Deserialize,
|
||||
serde_json::Value,
|
||||
};
|
||||
|
||||
/// CLI arguments struct using [`clap`]'s Derive API.
|
||||
|
@ -39,13 +40,18 @@ pub struct Args {
|
|||
/// A game's store URL, can be used multiple times.
|
||||
#[clap(long)]
|
||||
pub url: Vec<String>,
|
||||
|
||||
/// A person's steamcommunity.com ID or full URL, can be used multiple times.
|
||||
#[clap(long)]
|
||||
pub user: Vec<String>,
|
||||
}
|
||||
|
||||
/// A simple feed struct.
|
||||
#[derive(Debug)]
|
||||
pub struct Feed {
|
||||
/// The CLI option that was used for this feed.
|
||||
pub option: FeedOption,
|
||||
/// A potential alternate friendly URL, see [`SteamApp::friendly_url`] for an
|
||||
/// explanation.
|
||||
pub friendly_url: Option<String>,
|
||||
|
||||
/// The text to use for the feed in the OPML output.
|
||||
pub text: Option<String>,
|
||||
|
@ -54,14 +60,27 @@ pub struct Feed {
|
|||
pub url: String,
|
||||
}
|
||||
|
||||
/// An enum for [`Feed`]s for which option was used in the CLI.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum FeedOption {
|
||||
/// `-a, --appid <APPID>` was used.
|
||||
AppID,
|
||||
/// A small representation of a Steam game that is parsed from JSON.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SteamApp {
|
||||
/// The AppID of the game.
|
||||
pub appid: usize,
|
||||
|
||||
/// `--url <URL>` was used.
|
||||
Url,
|
||||
/// The name of the game.
|
||||
pub name: String,
|
||||
|
||||
/// A friendly URL name of the game, some feeds will use this instead of their
|
||||
/// AppID for their RSS feed.
|
||||
///
|
||||
/// For example, [Portal's feed](https://steamcommunity.com/games/Portal/rss)
|
||||
/// uses `Portal`, instead of
|
||||
/// [its AppID 400](https://steamcommunity.com/games/400/rss).
|
||||
///
|
||||
/// Some games may also have a friendly URL different from their AppID but
|
||||
/// don't use it for their feed. Steam is weird.
|
||||
#[serde(rename = "friendlyURL")]
|
||||
pub friendly_url: Value,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
|
@ -78,10 +97,14 @@ fn main() -> Result<()> {
|
|||
|
||||
let store_url_regex =
|
||||
Regex::new(r"(?i)^https?://store.steampowered.com/app/(?P<appid>\d+)")?;
|
||||
let user_json_regex = Regex::new(r"var rgGames = (?P<json>\[.+\]);\s+var")?;
|
||||
let user_id_regex = Regex::new(r"(i?)^\w+$")?;
|
||||
let user_url_regex =
|
||||
Regex::new(r"(?i)https?://steamcommunity.com/id/(?P<userid>\w+)")?;
|
||||
|
||||
for appid in args.appid {
|
||||
potential_feeds.push(Feed {
|
||||
option: FeedOption::AppID,
|
||||
friendly_url: None,
|
||||
text: Some(format!("Steam AppID {appid}")),
|
||||
url: appid_to_rss_url(appid),
|
||||
});
|
||||
|
@ -94,43 +117,94 @@ fn main() -> Result<()> {
|
|||
.and_then(|appid_match| appid_match.as_str().parse::<usize>().ok());
|
||||
if let Some(appid) = appid {
|
||||
potential_feeds.push(Feed {
|
||||
option: FeedOption::Url,
|
||||
friendly_url: None,
|
||||
text: Some(format!("Steam AppID {appid}")),
|
||||
url: appid_to_rss_url(appid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if args.verify {
|
||||
let progress = ProgressBar::new(potential_feeds.len().try_into()?)
|
||||
.with_style(ProgressStyle::with_template("Verifying {pos}/{len} {bar}")?);
|
||||
for user in args.user {
|
||||
let user_url = if user_id_regex.is_match(&user) {
|
||||
userid_to_games_url(user)
|
||||
} else if let Some(user) = user_url_regex
|
||||
.captures(&user)
|
||||
.and_then(|captures| captures.name("userid"))
|
||||
{
|
||||
userid_to_games_url(user.as_str())
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for potential_feed in potential_feeds {
|
||||
let potential_feed = if [FeedOption::AppID, FeedOption::Url]
|
||||
.contains(&potential_feed.option)
|
||||
{
|
||||
let response = ureq_agent.get(&potential_feed.url).call()?;
|
||||
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
|
||||
}
|
||||
let body = ureq_agent.get(&user_url).call()?.into_string()?;
|
||||
sleep(timeout);
|
||||
|
||||
let games_json = user_json_regex
|
||||
.captures(&body)
|
||||
.and_then(|captures| captures.name("json"))
|
||||
.map(|json| json.as_str());
|
||||
if let Some(games_json) = games_json {
|
||||
let games = serde_json::from_str::<Vec<SteamApp>>(games_json)?;
|
||||
for game in games {
|
||||
let friendly_url = if game.friendly_url.is_string() {
|
||||
Some(appid_to_rss_url(game.friendly_url.as_str().unwrap()))
|
||||
} else {
|
||||
continue;
|
||||
None
|
||||
};
|
||||
|
||||
potential_feeds.push(Feed {
|
||||
friendly_url,
|
||||
text: Some(game.name),
|
||||
url: appid_to_rss_url(game.appid),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
eprintln!("Couldn't scan games from: {user_url}");
|
||||
eprintln!(
|
||||
"Make sure \"Game Details\" in Privacy Settings is set to Public."
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if args.verify {
|
||||
let verify_feed = |url: &str| -> Result<_> {
|
||||
let response = ureq_agent.get(&url).call()?;
|
||||
sleep(timeout);
|
||||
Ok((
|
||||
response.content_type() == "text/xml",
|
||||
response.into_string()?,
|
||||
))
|
||||
};
|
||||
|
||||
for mut potential_feed in potential_feeds {
|
||||
let (mut is_valid_feed, mut body) = verify_feed(&potential_feed.url)?;
|
||||
|
||||
// If the potential URL doesn't return `text/xml`, try the friendly URL
|
||||
// if one exists.
|
||||
if !is_valid_feed && potential_feed.friendly_url.is_some() {
|
||||
let friendly_url = potential_feed.friendly_url.as_deref().unwrap();
|
||||
(is_valid_feed, body) = verify_feed(friendly_url)?;
|
||||
if is_valid_feed {
|
||||
potential_feed.url = friendly_url.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
let verified_feed = if is_valid_feed {
|
||||
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;
|
||||
};
|
||||
|
||||
feeds_to_output.push(potential_feed);
|
||||
sleep(timeout);
|
||||
progress.inc(1);
|
||||
feeds_to_output.push(verified_feed);
|
||||
}
|
||||
} else {
|
||||
feeds_to_output = potential_feeds;
|
||||
feeds_to_output.append(&mut potential_feeds);
|
||||
}
|
||||
|
||||
let mut opml_document = opml::OPML {
|
||||
|
@ -138,6 +212,11 @@ fn main() -> Result<()> {
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
if feeds_to_output.is_empty() {
|
||||
eprintln!("No feeds found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for feed in feeds_to_output {
|
||||
if args.opml {
|
||||
opml_document
|
||||
|
@ -158,3 +237,8 @@ fn main() -> Result<()> {
|
|||
fn appid_to_rss_url<D: std::fmt::Display>(appid: D) -> String {
|
||||
format!("https://steamcommunity.com/games/{appid}/rss/")
|
||||
}
|
||||
|
||||
/// Creates a user's Steam Games URL from a given User ID.
|
||||
fn userid_to_games_url<D: std::fmt::Display>(userid: D) -> String {
|
||||
format!("https://steamcommunity.com/id/{userid}/games/?tab=all")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue