1
Fork 0

Compare commits

..

No commits in common. "f56ff3e06fcf5c8b89a5a50057a10ff6d1e15044" and "1e9aa887f71778ffbb3eaec6a6b8bc85ba51ed21" have entirely different histories.

3 changed files with 85 additions and 141 deletions

73
Cargo.lock generated
View File

@ -160,6 +160,20 @@ dependencies = [
"tracing-error", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.3.2" version = "1.3.2"
@ -169,6 +183,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "eyre" name = "eyre"
version = "0.6.8" version = "0.6.8"
@ -276,10 +296,15 @@ dependencies = [
] ]
[[package]] [[package]]
name = "itoa" name = "indicatif"
version = "1.0.3" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b"
dependencies = [
"console",
"number_prefix",
"unicode-width",
]
[[package]] [[package]]
name = "jetscii" name = "jetscii"
@ -332,6 +357,12 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "object" name = "object"
version = "0.29.0" version = "0.29.0"
@ -474,12 +505,6 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]] [[package]]
name = "sct" name = "sct"
version = "0.7.0" version = "0.7.0"
@ -510,17 +535,6 @@ dependencies = [
"syn", "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]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.4" version = "0.1.4"
@ -542,10 +556,9 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"color-eyre", "color-eyre",
"indicatif",
"opml", "opml",
"regex", "regex",
"serde",
"serde_json",
"ureq", "ureq",
] ]
@ -575,6 +588,16 @@ dependencies = [
"winapi-util", "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]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.15.1" version = "0.15.1"
@ -688,6 +711,12 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-width"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"

View File

@ -13,10 +13,9 @@ path = "source/main.rs"
[dependencies] [dependencies]
color-eyre = "0.6.2" color-eyre = "0.6.2"
indicatif = "0.17.1"
opml = "1.1.4" opml = "1.1.4"
regex = "1.6.0" regex = "1.6.0"
serde = "1.0.144"
serde_json = "1.0.85"
ureq = "2.5.0" ureq = "2.5.0"
[dependencies.clap] [dependencies.clap]

View File

@ -12,9 +12,8 @@ use std::{thread::sleep, time::Duration};
use { use {
clap::Parser, clap::Parser,
color_eyre::{install, Result}, color_eyre::{install, Result},
indicatif::{ProgressBar, ProgressStyle},
regex::Regex, regex::Regex,
serde::Deserialize,
serde_json::Value,
}; };
/// CLI arguments struct using [`clap`]'s Derive API. /// CLI arguments struct using [`clap`]'s Derive API.
@ -40,18 +39,13 @@ pub struct Args {
/// A game's store URL, can be used multiple times. /// A game's store URL, can be used multiple times.
#[clap(long)] #[clap(long)]
pub url: Vec<String>, 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. /// A simple feed struct.
#[derive(Debug)] #[derive(Debug)]
pub struct Feed { pub struct Feed {
/// A potential alternate friendly URL, see [`SteamApp::friendly_url`] for an /// The CLI option that was used for this feed.
/// explanation. pub option: FeedOption,
pub friendly_url: Option<String>,
/// The text to use for the feed in the OPML output. /// The text to use for the feed in the OPML output.
pub text: Option<String>, pub text: Option<String>,
@ -60,27 +54,14 @@ pub struct Feed {
pub url: String, pub url: String,
} }
/// A small representation of a Steam game that is parsed from JSON. /// An enum for [`Feed`]s for which option was used in the CLI.
#[derive(Debug, Deserialize)] #[derive(Debug, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] pub enum FeedOption {
pub struct SteamApp { /// `-a, --appid <APPID>` was used.
/// The AppID of the game. AppID,
pub appid: usize,
/// The name of the game. /// `--url <URL>` was used.
pub name: String, Url,
/// 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<()> { fn main() -> Result<()> {
@ -97,14 +78,10 @@ fn main() -> Result<()> {
let store_url_regex = let store_url_regex =
Regex::new(r"(?i)^https?://store.steampowered.com/app/(?P<appid>\d+)")?; 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 { for appid in args.appid {
potential_feeds.push(Feed { potential_feeds.push(Feed {
friendly_url: None, option: FeedOption::AppID,
text: Some(format!("Steam AppID {appid}")), text: Some(format!("Steam AppID {appid}")),
url: appid_to_rss_url(appid), url: appid_to_rss_url(appid),
}); });
@ -117,80 +94,24 @@ fn main() -> Result<()> {
.and_then(|appid_match| appid_match.as_str().parse::<usize>().ok()); .and_then(|appid_match| appid_match.as_str().parse::<usize>().ok());
if let Some(appid) = appid { if let Some(appid) = appid {
potential_feeds.push(Feed { potential_feeds.push(Feed {
friendly_url: None, option: FeedOption::Url,
text: Some(format!("Steam AppID {appid}")), text: Some(format!("Steam AppID {appid}")),
url: appid_to_rss_url(appid), url: appid_to_rss_url(appid),
}); });
} }
} }
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;
};
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 {
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 { if args.verify {
let verify_feed = |url: &str| -> Result<_> { let progress = ProgressBar::new(potential_feeds.len().try_into()?)
let response = ureq_agent.get(&url).call()?; .with_style(ProgressStyle::with_template("Verifying {pos}/{len} {bar}")?);
sleep(timeout);
Ok((
response.content_type() == "text/xml",
response.into_string()?,
))
};
for mut potential_feed in potential_feeds { for potential_feed in potential_feeds {
let (mut is_valid_feed, mut body) = verify_feed(&potential_feed.url)?; let potential_feed = if [FeedOption::AppID, FeedOption::Url]
.contains(&potential_feed.option)
// If the potential URL doesn't return `text/xml`, try the friendly URL {
// if one exists. let response = ureq_agent.get(&potential_feed.url).call()?;
if !is_valid_feed && potential_feed.friendly_url.is_some() { if response.content_type() == "text/xml" {
let friendly_url = potential_feed.friendly_url.as_deref().unwrap(); let body = response.into_string()?;
(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_start = body.find("<title>").unwrap() + 7;
let title_end = body.find("</title>").unwrap(); let title_end = body.find("</title>").unwrap();
Feed { Feed {
@ -199,12 +120,17 @@ fn main() -> Result<()> {
} }
} else { } else {
continue; continue;
};
feeds_to_output.push(verified_feed);
} }
} else { } else {
feeds_to_output.append(&mut potential_feeds); continue;
};
feeds_to_output.push(potential_feed);
sleep(timeout);
progress.inc(1);
}
} else {
feeds_to_output = potential_feeds;
} }
let mut opml_document = opml::OPML { let mut opml_document = opml::OPML {
@ -212,11 +138,6 @@ fn main() -> Result<()> {
..Default::default() ..Default::default()
}; };
if feeds_to_output.is_empty() {
eprintln!("No feeds found.");
return Ok(());
}
for feed in feeds_to_output { for feed in feeds_to_output {
if args.opml { if args.opml {
opml_document opml_document
@ -237,8 +158,3 @@ fn main() -> Result<()> {
fn appid_to_rss_url<D: std::fmt::Display>(appid: D) -> String { fn appid_to_rss_url<D: std::fmt::Display>(appid: D) -> String {
format!("https://steamcommunity.com/games/{appid}/rss/") 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")
}