Steam collections (#60)

This commit is contained in:
Philip Kristoffersen
2022-03-26 21:51:56 +01:00
committed by GitHub
parent 8ce3b42ae9
commit a7524400f3
15 changed files with 962 additions and 64 deletions
Generated
+139 -7
View File
@@ -87,6 +87,7 @@ dependencies = [
name = "boilr"
version = "0.2.7"
dependencies = [
"base64",
"config",
"dashmap",
"failure",
@@ -99,6 +100,7 @@ dependencies = [
"nom 7.0.0",
"nom_locate",
"reqwest",
"rusty-leveldb",
"serde 1.0.130",
"serde_json",
"steam_shortcuts_util",
@@ -108,6 +110,12 @@ dependencies = [
"winreg 0.10.1",
]
[[package]]
name = "build_const"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ae4235e6dac0694637c763029ecea1a2ec9e4e06ec2729bd21ba4d9c863eb7"
[[package]]
name = "bumpalo"
version = "3.7.1"
@@ -197,6 +205,15 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b"
[[package]]
name = "crc"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
dependencies = [
"build_const",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
@@ -246,6 +263,27 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "failure"
version = "0.1.8"
@@ -351,6 +389,16 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "funty"
version = "1.1.0"
@@ -451,6 +499,17 @@ dependencies = [
"slab",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.3"
@@ -594,6 +653,12 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "integer-encoding"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48dc51180a9b377fd75814d0cc02199c20f8e99433d6762f650d39cdbbd3b56f"
[[package]]
name = "ipnet"
version = "2.3.1"
@@ -970,6 +1035,19 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc 0.2.0",
]
[[package]]
name = "rand"
version = "0.8.4"
@@ -977,9 +1055,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
"rand_chacha 0.3.1",
"rand_core 0.6.3",
"rand_hc 0.3.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
@@ -989,7 +1077,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
@@ -998,7 +1095,16 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
"getrandom 0.2.3",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@@ -1007,7 +1113,7 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [
"rand_core",
"rand_core 0.6.3",
]
[[package]]
@@ -1092,6 +1198,20 @@ version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342"
[[package]]
name = "rusty-leveldb"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19365c4619fa0892aef573e6ee1b569b1161ac1ac3da765f550a17645192615e"
dependencies = [
"crc",
"errno",
"fs2",
"integer-encoding",
"rand 0.7.3",
"snap",
]
[[package]]
name = "ryu"
version = "1.0.5"
@@ -1219,6 +1339,12 @@ version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "snap"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45456094d1983e2ee2a18fdfebce3189fa451699d0502cb8e3b49dba5ba41451"
[[package]]
name = "socket2"
version = "0.4.4"
@@ -1296,7 +1422,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if",
"libc",
"rand",
"rand 0.8.4",
"redox_syscall",
"remove_dir_all",
"winapi",
@@ -1480,6 +1606,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
+2
View File
@@ -23,6 +23,8 @@ futures = { version = "^0.3.17" }
dashmap = { version = "^4.0.2", features = ["serde"] }
fltk-theme = {version ="0.4" , optional = true }
is_executable = "1.0.1"
rusty-leveldb = "0.3.6"
base64="0.13.0"
[build-dependencies]
fl2rust = { version = "0.4", optional = true }
+8 -3
View File
@@ -12,11 +12,16 @@ class UserInterface {open
} {
# ----- Steam -----
Fl_Input steam_location_input {
label {Steam Location}
xywh {25 25 450 25} align 5
Fl_Check_Button steam_create_collections {
label {Create Collections}
tooltip {Create Collections for each platform}
xywh {25 25 155 25} down_box DOWN_BOX
}
Fl_Input steam_location_input {
label {Steam Location}
xywh {175 25 300 25} align 5
}
# ----- Art download -----
Fl_Check_Button enable_steamgrid_db_checkbox {
+1
View File
@@ -33,3 +33,4 @@ enabled = true
enabled = true
[steam]
create_collections = false
+422
View File
@@ -0,0 +1,422 @@
use nom::FindSubstring;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use rusty_leveldb::{LdbIterator, Options, WriteBatch, DB};
const BOILR_TAG: &'static str = "boilr";
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
enum SteamCollection {
Actual(ActualSteamCollection),
Deleted(DeletedCollection),
}
impl SteamCollection {
pub fn is_boilr_collection(&self) -> bool {
match self {
SteamCollection::Actual(actual) => actual.is_boilr_collection(),
SteamCollection::Deleted(_) => false,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct ActualSteamCollection {
key: String,
timestamp: u64,
value: String, //For custom collections, this value is a ValueCollection json serialized
#[serde(
skip_serializing_if = "Option::is_none",
rename = "conflictResolutionMethod"
)]
conflict_resolution_method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "strMethodId")]
str_method_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
}
impl ActualSteamCollection {
fn new<B: AsRef<str>>(name: B, ids: &Vec<usize>) -> Self {
let name = name.as_ref();
let key = format!("user-collections.{}", name_to_key(name));
let value = serialize_collection_value(name, ids);
let start = SystemTime::now();
let since_the_epoch = start
.duration_since(UNIX_EPOCH)
.expect("Time went backwards");
let timestamp = since_the_epoch.as_secs();
ActualSteamCollection {
key,
timestamp,
value,
conflict_resolution_method: Some("custom".to_string()),
str_method_id: Some("union-collections".to_string()),
version: None,
}
}
pub fn is_boilr_collection(&self) -> bool {
self.key
.contains(&format!("user-collections.{}", BOILR_TAG))
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct ValueCollection {
id: String,
name: String,
added: Vec<usize>,
removed: Vec<usize>,
}
impl ValueCollection {
fn new<S: AsRef<str>>(name: S, game_ids: &Vec<usize>) -> Self {
let name = name.as_ref();
let id = name_to_key(name);
let value = ValueCollection {
id,
name: name.to_string(),
added: game_ids.clone(),
removed: vec![],
};
value
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
struct DeletedCollection {
key: String,
timestamp: usize,
is_deleted: bool,
version: String,
}
pub struct Collection {
pub name: String,
pub game_ids: Vec<usize>,
}
pub fn write_collections<S: AsRef<str>>(
steam_user_id: S,
collections_to_add: &Vec<Collection>,
) -> Result<(), Box<dyn Error>> {
let steam_user_id = steam_user_id.as_ref();
let new_collections: Vec<(String, SteamCollection)> = collections_to_add
.iter()
.map(|c| {
let collection = ActualSteamCollection::new(&c.name, &c.game_ids);
(collection.key.clone(), SteamCollection::Actual(collection))
})
.collect();
let mut db = open_db()?;
let current_categories = get_categories(steam_user_id, &mut db)?;
//this is a collection of collections, known as a category
let mut write_batch = WriteBatch::new();
for (category_key, mut collections) in current_categories {
collections.retain(|(_key, collection)| !collection.is_boilr_collection());
collections.extend(new_collections.clone());
save_category(category_key, collections, &mut write_batch)?;
if let Some(path) = get_vdf_path(steam_user_id) {
let content = std::fs::read_to_string(&path).expect("Should be able to read this file");
if let Some(mut vdf_collections) = parse_vdf_collection(content) {
let boilr_keys: Vec<String> = vdf_collections
.keys()
.filter(|k| k.contains(BOILR_TAG))
.map(|k| k.clone())
.collect();
for key in boilr_keys {
vdf_collections.remove(&key);
}
let new_vdfs = collections_to_add.iter().map(|collection| {
let key = name_to_key(&collection.name);
let vd_collection = VdfCollection {
id: key,
added: collection.game_ids.clone(),
removed: vec![],
};
vd_collection
});
for new_vdf in new_vdfs {
vdf_collections.insert(new_vdf.id.clone(), new_vdf.clone());
}
let new_string = write_vdf_collection_to_string(
&path.clone().to_string_lossy(),
&vdf_collections,
);
if let Some(new_string) = new_string {
std::fs::write(path, new_string).unwrap();
}
}
}
}
db.write(write_batch, true)?;
Ok(())
}
#[cfg(target_family = "unix")]
fn get_vdf_path<S: AsRef<str>>(steamid: S) -> Option<PathBuf> {
match std::env::var("HOME") {
Ok(home) => {
let path = Path::new(&home)
.join(".steam")
.join("steam")
.join("userdata")
.join(steamid.as_ref())
.join("config")
.join("localconfig.vdf");
if path.exists() {
return Some(path.to_path_buf());
}
return None;
}
Err(e) => return None,
}
}
#[cfg(target_os = "windows")]
fn get_vdf_path<S: AsRef<str>>(steamid: S) -> Option<PathBuf> {
match std::env::var("PROGRAMFILES(X86)") {
Ok(program_files) => {
let path = Path::new(&program_files)
.join("Steam")
.join("userdata")
.join(steamid.as_ref())
.join("config")
.join("localconfig.vdf");
if path.exists() {
Some(path.to_path_buf())
} else {
None
}
}
Err(_e) => None,
}
}
fn save_category<S: AsRef<str>>(
category_key: S,
category: Vec<(String, SteamCollection)>,
batch: &mut WriteBatch,
) -> Result<(), Box<dyn Error>> {
let json = serde_json::to_string(&category)?;
let prefixed = format!("\u{1}{}", json);
batch.put(category_key.as_ref().as_bytes(), prefixed.as_bytes());
Ok(())
}
fn get_categories<S: AsRef<str>>(
steamid: S,
db: &mut DB,
) -> Result<HashMap<String, Vec<(String, SteamCollection)>>, Box<dyn Error>> {
let namespace_keys = get_namespace_keys(steamid, db);
let mut db_iter = db.new_iter()?;
let mut res = HashMap::new();
while let Some((key_bytes, data_bytes)) = db_iter.next() {
let key = String::from_utf8_lossy(&key_bytes).to_string();
//make sure that what we are looking at is a collection
//there are other things in this db as well
if namespace_keys.contains(&key) {
let data = String::from_utf8_lossy(&data_bytes);
let collections = parse_steam_collections(&data)?;
res.insert(key, collections);
}
}
Ok(res)
}
fn open_db() -> Result<DB, Box<dyn Error>> {
let location = get_level_db_location();
let options = Options::default();
let open_res = DB::open(location.unwrap(), options);
if let Err(e) = &open_res {
match &e.code {
rusty_leveldb::StatusCode::LockError => {
println!("Could not lock the steam level database, make sure steam is turned off when running synchronizations");
}
rusty_leveldb::StatusCode::NotFound => {
println!("Could not find the steam level database, try to open and close steam once and synchronize again");
}
_ => {}
};
}
Ok(open_res?)
}
fn get_namespace_keys<S: AsRef<str>>(steamid: S, db: &mut DB) -> HashSet<String> {
let keyprefix = get_steam_user_prefix(steamid);
let namespaces_key = format!("{}s", keyprefix);
let key_bytes = namespaces_key.as_bytes();
let namespaces = get_namespaces(db, key_bytes).unwrap_or_default();
let namespace_keys = namespaces
.iter()
.map(|c| format!("{}-{}", keyprefix, c.0))
.collect();
namespace_keys
}
fn get_namespaces(db: &mut DB, key_bytes: &[u8]) -> Option<Vec<(i32, String)>> {
match db.get(key_bytes) {
Some(got) => {
let collection_bytes = got.as_slice();
let collectin_str = String::from_utf8_lossy(&collection_bytes)[1..].to_string();
let collection = serde_json::from_str(&collectin_str).unwrap_or_default();
Some(collection)
}
_ => None,
}
}
#[cfg(target_family = "unix")]
fn get_level_db_location() -> Option<PathBuf> {
match std::env::var("HOME") {
Ok(home) => {
let path = Path::new(&home)
.join(".steam")
.join("steam")
.join("config")
.join("htmlcache")
.join("Local Storage")
.join("leveldb");
if path.exists() {
return Some(path.to_path_buf());
}
return None;
}
Err(e) => return None,
}
}
#[cfg(target_os = "windows")]
fn get_level_db_location() -> Option<PathBuf> {
match std::env::var("LOCALAPPDATA") {
Ok(localdata) => {
let path = Path::new(&localdata)
.join("Steam")
.join("htmlcache")
.join("Local Storage")
.join("leveldb");
if path.exists() {
return Some(path.to_path_buf());
}
return None;
}
Err(_e) => return None,
}
}
fn serialize_collection_value<S: AsRef<str>>(name: S, game_ids: &Vec<usize>) -> String {
let value = ValueCollection::new(name, game_ids);
let value_json = serde_json::to_string(&value).expect("Should be able to serialize known type");
value_json
}
fn name_to_key<S: AsRef<str>>(name: S) -> String {
let config = base64::Config::new(base64::CharacterSet::Standard, false);
let base64 = base64::encode_config(name.as_ref(), config);
let key = format!("{}-{}", BOILR_TAG, base64);
key
}
fn parse_steam_collections<S: AsRef<str>>(
input: S,
) -> Result<Vec<(String, SteamCollection)>, Box<dyn Error>> {
let input = input.as_ref();
let input = if input.starts_with("\u{1}") {
input[1..].to_string()
} else {
input.to_string()
};
let res = serde_json::from_str::<Vec<(String, SteamCollection)>>(&input)?;
Ok(res)
}
fn get_steam_user_prefix<S: AsRef<str>>(steamid: S) -> String {
let keyprefix = format!(
"_https://steamloopback.host\u{0000}\u{0001}U{}-cloud-storage-namespace",
steamid.as_ref()
);
keyprefix
}
pub fn parse_vdf_collection<S: AsRef<str>>(input: S) -> Option<HashMap<String, VdfCollection>> {
let input = input.as_ref();
serde_json::from_str(input).ok()
}
pub fn write_vdf_collection_to_string<S: AsRef<str>>(
input: S,
vdf: &HashMap<String, VdfCollection>,
) -> Option<String> {
let input = input.as_ref();
let str = serde_json::to_string(vdf).expect("Should be able to serialize known type");
let encoded_json = format!("\"{}\"", str.replace("\"", "\\\""));
let key = "\t\"user-collections\"\t\t";
if let Some(start_index) = input.find_substring(key) {
let start_index_plus_key = start_index + key.len();
if let Some(line_index) = input[start_index_plus_key..].find("\n") {
let end_index_in_full = line_index + start_index_plus_key;
let result = format!(
"{}{}{}",
input[..start_index_plus_key].to_string(),
encoded_json,
input[end_index_in_full..].to_string()
);
return Some(result);
}
}
None
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct VdfCollection {
id: String,
added: Vec<usize>,
removed: Vec<usize>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_serialize_collection() {
let games = vec![312200];
let mut collection = ActualSteamCollection::new("Itch", &games);
collection.timestamp = 1647763452;
let json = serde_json::to_string(&collection).unwrap();
let expected = include_str!("../testdata/leveldb/test_collection.json");
assert_eq!(json, expected);
}
#[test]
fn can_serialize_collection_inner() {
let games = vec![312200];
let res = serialize_collection_value("Itch", &games);
let expected = include_str!("../testdata/leveldb/test_collection_value.json");
assert_eq!(res, expected);
}
#[test]
fn can_parse_categories() {
let input = include_str!("../testdata/leveldb/testcollections.json");
let collection = parse_steam_collections(input).unwrap();
assert_eq!(28, collection.len())
}
}
+2
View File
@@ -1,5 +1,7 @@
mod settings;
mod utils;
mod collections;
pub use settings::SteamSettings;
pub use utils::*;
pub use collections::*;
+1
View File
@@ -3,4 +3,5 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SteamSettings {
pub location: Option<String>,
pub create_collections: bool,
}
+4
View File
@@ -46,6 +46,7 @@ pub struct ShortcutInfo {
pub struct SteamUsersInfo {
pub steam_user_data_folder: String,
pub shortcut_path: Option<String>,
pub user_id : String,
}
/// Get the paths to the steam users shortcuts (one for each user)
@@ -80,6 +81,7 @@ pub fn get_shortcuts_paths(
_ => false,
})
.map(|folder| {
let user_id = folder.file_name().to_string_lossy().to_string();
let folder_path = folder.path();
let folder_str = folder_path
.to_str()
@@ -91,11 +93,13 @@ pub fn get_shortcuts_paths(
return SteamUsersInfo {
steam_user_data_folder: folder_string,
shortcut_path: Some(shortcuts_path.to_str().unwrap().to_string()),
user_id
};
} else {
return SteamUsersInfo {
steam_user_data_folder: folder_string,
shortcut_path: None,
user_id
};
}
})
+71 -52
View File
@@ -2,13 +2,17 @@ use steam_shortcuts_util::{shortcut::ShortcutOwned, shortcuts_to_bytes};
use crate::{
egs::EpicPlatform,
heroic::HeroicPlatform,
legendary::LegendaryPlatform,
lutris::lutris_platform::LutrisPlatform,
platform::Platform,
settings::Settings,
steam::{get_shortcuts_for_user, get_shortcuts_paths, ShortcutInfo, SteamUsersInfo},
steam::{
get_shortcuts_for_user, get_shortcuts_paths, write_collections, Collection, ShortcutInfo,
SteamUsersInfo,
},
steamgriddb::download_images_for_users,
uplay::Uplay, heroic::HeroicPlatform,
uplay::Uplay,
};
use std::error::Error;
@@ -19,6 +23,12 @@ const BOILR_TAG: &str = "boilr";
pub async fn run_sync(settings: &Settings) -> Result<(), Box<dyn Error>> {
let mut userinfo_shortcuts = get_shortcuts_paths(&settings.steam)?;
let platform_shortcuts = get_platform_shortcuts(settings);
let all_shortcuts: Vec<ShortcutOwned> = platform_shortcuts
.iter()
.flat_map(|s| s.1.clone())
.collect();
println!("Found {} user(s)", userinfo_shortcuts.len());
for user in userinfo_shortcuts.iter_mut() {
let start_time = std::time::Instant::now();
@@ -27,14 +37,22 @@ pub async fn run_sync(settings: &Settings) -> Result<(), Box<dyn Error>> {
println!(
"Found {} shortcuts for user: {}",
shortcut_info.shortcuts.len(),
user.steam_user_data_folder
user.user_id
);
remove_old_shortcuts(&mut shortcut_info);
update_platforms(settings, &mut shortcut_info.shortcuts);
shortcut_info.shortcuts.extend(all_shortcuts.clone());
fix_shortcut_icons(user, &mut shortcut_info.shortcuts);
save_shortcuts(&shortcut_info.shortcuts, Path::new(&shortcut_info.path));
user.shortcut_path = Some(shortcut_info.path.to_string_lossy().to_string());
if settings.steam.create_collections {
match write_shortcut_collections(&user.user_id, &platform_shortcuts) {
Ok(_) => (),
Err(_e) => eprintln!("Could not write collections, make sure steam is shut down"),
}
}
let duration = start_time.elapsed();
println!("Finished synchronizing games in: {:?}", duration);
@@ -84,50 +102,46 @@ fn fix_shortcut_icons(user: &SteamUsersInfo, shortcuts: &mut Vec<ShortcutOwned>)
}
}
fn update_platforms(settings: &Settings, new_user_shortcuts: &mut Vec<ShortcutOwned>) {
update_platform_shortcuts(
&EpicPlatform::new(settings.epic_games.clone()),
new_user_shortcuts,
);
update_platform_shortcuts(
&LegendaryPlatform::new(settings.legendary.clone()),
new_user_shortcuts,
);
update_platform_shortcuts(
&ItchPlatform::new(settings.itch.clone()),
new_user_shortcuts,
);
update_platform_shortcuts(
&OriginPlatform {
settings: settings.origin.clone(),
},
new_user_shortcuts,
);
update_platform_shortcuts(
&GogPlatform {
settings: settings.gog.clone(),
},
new_user_shortcuts,
);
update_platform_shortcuts(
&Uplay {
settings: settings.uplay.clone(),
},
new_user_shortcuts,
);
fn write_shortcut_collections<S: AsRef<str>>(
steam_id: S,
platform_results: &Vec<(String, Vec<ShortcutOwned>)>,
) -> Result<(), Box<dyn Error>> {
let mut collections = vec![];
update_platform_shortcuts(
&LutrisPlatform {
for (name, shortcuts) in platform_results {
let game_ids = shortcuts.iter().map(|s| (s.app_id as usize)).collect();
collections.push(Collection {
name: name.clone(),
game_ids,
});
}
println!("Writing {} collections ", collections.len());
write_collections(steam_id.as_ref(), &collections)?;
Ok(())
}
fn get_platform_shortcuts(settings: &Settings) -> Vec<(String, Vec<ShortcutOwned>)> {
let platform_results = vec![
update_platform_shortcuts(&EpicPlatform::new(settings.epic_games.clone())),
update_platform_shortcuts(&LegendaryPlatform::new(settings.legendary.clone())),
update_platform_shortcuts(&ItchPlatform::new(settings.itch.clone())),
update_platform_shortcuts(&OriginPlatform {
settings: settings.origin.clone(),
}),
update_platform_shortcuts(&GogPlatform {
settings: settings.gog.clone(),
}),
update_platform_shortcuts(&Uplay {
settings: settings.uplay.clone(),
}),
update_platform_shortcuts(&LutrisPlatform {
settings: settings.lutris.clone(),
},
new_user_shortcuts,
);
update_platform_shortcuts(
&HeroicPlatform {
}),
update_platform_shortcuts(&HeroicPlatform {
settings: settings.heroic.clone(),
},
new_user_shortcuts,
);
}),
];
platform_results.iter().filter_map(|p| p.clone()).collect()
}
fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) {
@@ -157,7 +171,7 @@ fn save_shortcuts(shortcuts: &[ShortcutOwned], path: &Path) {
}
}
fn update_platform_shortcuts<P, T, E>(platform: &P, current_shortcuts: &mut Vec<ShortcutOwned>)
fn update_platform_shortcuts<P, T, E>(platform: &P) -> Option<(String, Vec<ShortcutOwned>)>
where
P: Platform<T, E>,
E: std::fmt::Debug + std::fmt::Display,
@@ -170,9 +184,11 @@ where
platform.name(),
reason
);
return;
return None;
}
let mut current_shortcuts = vec![];
#[cfg(target_family = "unix")]
if platform.create_symlinks() {
let name = platform.name();
@@ -181,13 +197,13 @@ where
let shortcuts_to_add_result = platform.get_shortcuts();
match shortcuts_to_add_result {
Ok(shortcuts_to_add) => {
let mut shortcuts_to_add_transformed = vec![];
for shortcut in shortcuts_to_add{
for shortcut in shortcuts_to_add {
let mut shortcut_owned: ShortcutOwned = shortcut.into();
shortcut_owned.dev_kit_game_id = format!("{}-{}",BOILR_TAG,shortcut_owned.app_id);
shortcut_owned.dev_kit_game_id =
format!("{}-{}", BOILR_TAG, shortcut_owned.app_id);
shortcuts_to_add_transformed.push(shortcut_owned);
}
@@ -206,8 +222,10 @@ where
} else {
shortcut_owned
};
current_shortcuts.push(shortcut_owned);
current_shortcuts.push(shortcut_owned.clone());
}
let name = platform.name();
return Some((name.to_string(), current_shortcuts));
}
Err(err) => {
eprintln!("Error getting shortcuts from platform: {}", platform.name());
@@ -215,4 +233,5 @@ where
}
}
}
None
}
+1
View File
@@ -0,0 +1 @@
{"key":"user-collections.boilr-SXRjaA","timestamp":1647763452,"value":"{\"id\":\"boilr-SXRjaA\",\"name\":\"Itch\",\"added\":[312200],\"removed\":[]}","conflictResolutionMethod":"custom","strMethodId":"union-collections"}
+1
View File
@@ -0,0 +1 @@
{"id":"boilr-SXRjaA","name":"Itch","added":[312200],"removed":[]}
File diff suppressed because one or more lines are too long
+271
View File
@@ -0,0 +1,271 @@
[
[
"showcases.2",
{
"key": "showcases.2",
"timestamp": 1627185068,
"value": "{\"nShowcaseId\":2,\"strCollectionId\":\"type-games\",\"eSortBy\":11,\"bExpanded\":true,\"nOrder\":1572675717433,\"nLastChangedMS\":1627185068325}"
}
],
[
"showcases.-1",
{
"key": "showcases.-1",
"timestamp": 1572551831,
"value": "{\"nShowcaseId\":-1,\"strCollectionId\":\"\",\"eSortBy\":1,\"bExpanded\":false}",
"conflictResolutionMethod": "initial"
}
],
[
"user-collections.favorite",
{
"key": "user-collections.favorite",
"timestamp": 1646915179,
"value": "{\"id\":\"favorite\",\"name\":\"Favorites\",\"added\":[265930,751780,433340,361420,337340,1055540,960990,240720,1364100,398850,233290,1245620],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "766"
}
],
[
"user-collections.hidden",
{
"key": "user-collections.hidden",
"timestamp": 1583777127,
"value": "{\"id\":\"hidden\",\"name\":\"Hidden\",\"added\":[622590,813000],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections"
}
],
[
"collection-bootstrap-complete",
{
"key": "collection-bootstrap-complete",
"timestamp": 1572551831,
"value": "true",
"conflictResolutionMethod": "initial"
}
],
[
"sc-version",
{
"key": "sc-version",
"timestamp": 1627720308,
"value": "6"
}
],
[
"user-collections.uc-tyBrO3av85Yt",
{
"key": "user-collections.uc-tyBrO3av85Yt",
"timestamp": 1647172018,
"value": "{\"id\":\"uc-tyBrO3av85Yt\",\"name\":\"Installed\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2,1],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "767"
}
],
[
"user-collections.uc-m1aOnX+0j8hd",
{
"key": "user-collections.uc-m1aOnX+0j8hd",
"timestamp": 1647172018,
"value": "{\"id\":\"uc-m1aOnX+0j8hd\",\"name\":\"Ready TO Play\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[2],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "767"
}
],
[
"showcases.3",
{
"key": "showcases.3",
"timestamp": 1589544934,
"value": "{\"nShowcaseId\":3,\"strCollectionId\":\"play-next\",\"eSortBy\":1,\"bExpanded\":false,\"nOrder\":1589544935035}",
"conflictResolutionMethod": "initial"
}
],
[
"whatsnew_lastseen",
{
"key": "whatsnew_lastseen",
"timestamp": 1589544939,
"value": "3"
}
],
[
"user-collections.uc-Y33SC/drzUUE",
{
"key": "user-collections.uc-Y33SC/drzUUE",
"timestamp": 1607528488,
"value": "{\"id\":\"uc-Y33SC/drzUUE\",\"name\":\"AllGames\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections"
}
],
[
"user-collections.uc-in3wyYtlPtji",
{
"key": "user-collections.uc-in3wyYtlPtji",
"timestamp": 1640761113,
"is_deleted": true,
"version": "359"
}
],
[
"showcases.1616699857157",
{
"key": "showcases.1616699857157",
"timestamp": 1647376978,
"value": "{\"nShowcaseId\":1616699857157,\"strCollectionId\":\"recent-games\",\"eSortBy\":1,\"nLastChangedMS\":1647376978744}",
"version": "768"
}
],
[
"user-collections.uc-viSLEvPBQo8y",
{
"key": "user-collections.uc-viSLEvPBQo8y",
"timestamp": 1640761088,
"is_deleted": true,
"version": "354"
}
],
[
"user-collections.uc-v49SJCW8hvGt",
{
"key": "user-collections.uc-v49SJCW8hvGt",
"timestamp": 1640761084,
"is_deleted": true,
"version": "353"
}
],
[
"user-collections.uc-qOFB6+htBEw+",
{
"key": "user-collections.uc-qOFB6+htBEw+",
"timestamp": 1640761104,
"is_deleted": true,
"version": "357"
}
],
[
"user-collections.uc-eK5FEQBVV/f4",
{
"key": "user-collections.uc-eK5FEQBVV/f4",
"timestamp": 1640761102,
"is_deleted": true,
"version": "356"
}
],
[
"user-collections.uc-WY8/sP0+o0mi",
{
"key": "user-collections.uc-WY8/sP0+o0mi",
"timestamp": 1640761091,
"is_deleted": true,
"version": "355"
}
],
[
"user-collections.uc-tuuA/xdtEaPL",
{
"key": "user-collections.uc-tuuA/xdtEaPL",
"timestamp": 1640761110,
"is_deleted": true,
"version": "358"
}
],
[
"user-collections.uc-JxLU0W6+VB99",
{
"key": "user-collections.uc-JxLU0W6+VB99",
"timestamp": 1640778296,
"is_deleted": true,
"version": "513"
}
],
[
"user-collections.uc-x7kKBnx8rlm1",
{
"key": "user-collections.uc-x7kKBnx8rlm1",
"timestamp": 1645356629,
"value": "{\"id\":\"uc-x7kKBnx8rlm1\",\"name\":\"Next\",\"added\":[1252330,403640,582160,337340,780290,236870,1182620,214560,105000,514900,480490,900270,384190,473690,34010,934780,536270,484900,984800,208650,250680,960990,692850,774361,1092430,399810,717610,893180,870780,718670,262060,214340,373650,452570,65930,330020,312200,1091500,548430,356650,17470,47780,337000,435150,373420,379720,47810,239140,761030,245280,716500,396750,470260,716490,1047220,330840,792100,368360,255070,331870,882100,268500,857980,427290,391540,499520,297130,205910,1150950,464920,2400,750920,418240,814380,302510,312520,9200,474960,741820,485030,633060,788100,760060,846470,1449560,529660,305620,1405790,414340,394510,863550,323190,233290,1349230,371140,214490,611760,1172450,206190,460950,739630,683320,304430,319630,264710,207690,1147550,601840,362890,200260,843260,557340,238320,1018850,1181400,874390,351920,274500,504210,322190,996580,813630,393190,597760,242760,470780,530320,965680,268130,424840,587430,582560,207170,960690,764790,220780,257850,234270,48000,200900,523780,1054490,428550,224480,547480,963000,613100,388880,230230,356400,475150,890720,701160,1168880,712730,493340,690640,699170,250700,522470,1127610,300550,1063490,233740,412020,291650,20900,223830,581270,281990,584980,209830,605450,1011190,555880,110800,582010,994220,115800,532210,304650,535480,443810,240760,431940,356040,231200,231910,437630],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "765"
}
],
[
"user-collections.uc-BfHgIPPLf03m",
{
"key": "user-collections.uc-BfHgIPPLf03m",
"timestamp": 1640778301,
"is_deleted": true,
"version": "514"
}
],
[
"user-collections.uc-rlBhYJWJVfT7",
{
"key": "user-collections.uc-rlBhYJWJVfT7",
"timestamp": 1644677712,
"value": "{\"id\":\"uc-rlBhYJWJVfT7\",\"name\":\"Completed\",\"added\":[80310,269050,250320,20920,258520,237930,98100,203160,205650,257510,343860,252410,221910,646570,31220,391220,212050,227000,632470,617290,1349230,873940,294100,1102190,1092790,874260,383870,409160,861540,1222140,894020,3830,1145360,546560,28050,205100,1070710,367520,219150,220,434170,213670,420,242680,102600,1057090,221640,286160,8930,35140,246420,341800,550,261570,40700,450390,380,8870,113200,620980,107100,207610,261030,436820,4580,219890,500,204360,212910,240,35700,80350,80360,29720,49400,225080,72850,57300,26800,55230,374320,405820,858260,80340,251470,1079800,201790,248820,50300,688420,211420,283160,266510,320,448280,368230,287700,113020,1172380,462770,80330,236430,12140,322500,244160,26500,40930,400430,17410,335300,238370,631570,1151640,1055540],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "752"
}
],
[
"user-collections.uc-2mg3hoGsWUvy",
{
"key": "user-collections.uc-2mg3hoGsWUvy",
"timestamp": 1640778308,
"is_deleted": true,
"version": "515"
}
],
[
"user-collections.uc-UGQi/7wUEotn",
{
"key": "user-collections.uc-UGQi/7wUEotn",
"timestamp": 1644590139,
"value": "{\"id\":\"uc-UGQi/7wUEotn\",\"name\":\"Single player and Story Rich\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[7],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[1742],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "749"
}
],
[
"user-collections.uc-kiAlA*+Nothpn",
{
"key": "user-collections.uc-kiAlA*+Nothpn",
"timestamp": 1645030805,
"value": "{\"id\":\"uc-kiAlA*+Nothpn\",\"name\":\"Co-op\",\"added\":[],\"removed\":[],\"filterSpec\":{\"nFormatVersion\":2,\"strSearchText\":\"\",\"filterGroups\":[{\"rgOptions\":[],\"bAcceptUnion\":true},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false},{\"rgOptions\":[3841,1685],\"bAcceptUnion\":false},{\"rgOptions\":[],\"bAcceptUnion\":false}]}}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "755"
}
],
[
"user-collections.uc-VLUd605CHPd+",
{
"key": "user-collections.uc-VLUd605CHPd+",
"timestamp": 1647763452,
"value": "{\"id\":\"uc-VLUd605CHPd+\",\"name\":\"Itch\",\"added\":[312200],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "771"
}
],
[
"user-collections.uc-DU8F01868yMa",
{
"key": "user-collections.uc-DU8F01868yMa",
"timestamp": 1647763461,
"value": "{\"id\":\"uc-DU8F01868yMa\",\"name\":\"TestTag\",\"added\":[],\"removed\":[]}",
"conflictResolutionMethod": "custom",
"strMethodId": "union-collections",
"version": "772"
}
]
]
+32
View File
@@ -0,0 +1,32 @@
{
"from-tag-Installed": {
"id": "from-tag-Installed",
"added": [
3601140154,
2719403116
],
"removed": []
},
"from-tag-Itch": {
"id": "from-tag-Itch",
"added": [
3601140154
],
"removed": []
},
"from-tag-Ready TO Play": {
"id": "from-tag-Ready TO Play",
"added": [
3601140154,
2719403116
],
"removed": []
},
"from-tag-Heroic": {
"id": "from-tag-Heroic",
"added": [
2719403116
],
"removed": []
}
}
+4
View File
@@ -61,6 +61,7 @@ fn empty_or_whitespace(input: String) -> Option<String> {
fn update_settings_with_ui_values(settings: &mut Settings, ui: &UserInterface) {
// Steam location
settings.steam.location = empty_or_whitespace(ui.steam_location_input.value());
settings.steam.create_collections = ui.steam_create_collections.value();
// Steamgrid db
settings.steamgrid_db.enabled = ui.enable_steamgrid_db_checkbox.value();
@@ -116,6 +117,9 @@ fn update_ui_with_settings(ui: &mut UserInterface, settings: &Settings) {
.unwrap_or(steam::get_default_location().unwrap_or(String::from(""))),
);
ui.steam_create_collections
.set_value(settings.steam.create_collections);
ui.enable_steamgrid_db_checkbox
.set_value(settings.steamgrid_db.enabled);
ui.steamgrid_db_auth_key_input.set_value(