modde_sources/nexus/
updates.rs1use std::collections::HashMap;
5
6use anyhow::Result;
7use modde_core::NexusModId;
8use serde::{Deserialize, Serialize};
9use tracing::{debug, info};
10
11use super::api::NexusApi;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ModUpdate {
16 pub mod_id: String,
18 pub nexus_mod_id: NexusModId,
20 pub installed_version: Option<String>,
22 pub installed_timestamp: i64,
24 pub latest_file_update: u64,
26 pub latest_mod_activity: u64,
28}
29
30#[derive(Debug, Clone)]
32pub struct TrackedMod {
33 pub mod_id: String,
34 pub nexus_mod_id: NexusModId,
35 pub nexus_game_domain: String,
36 pub installed_version: Option<String>,
37 pub installed_timestamp: i64,
38}
39
40pub async fn check_updates(
45 api: &NexusApi,
46 tracked: &[TrackedMod],
47 period: &str,
48) -> Result<Vec<ModUpdate>> {
49 if tracked.is_empty() {
50 return Ok(Vec::new());
51 }
52
53 let mut by_domain: HashMap<&str, Vec<&TrackedMod>> = HashMap::new();
55 for t in tracked {
56 by_domain
57 .entry(t.nexus_game_domain.as_str())
58 .or_default()
59 .push(t);
60 }
61
62 let mut updates = Vec::new();
63
64 for (domain, domain_mods) in &by_domain {
65 debug!(
66 domain,
67 mod_count = domain_mods.len(),
68 "checking updates for domain"
69 );
70
71 let lookup: HashMap<NexusModId, &&TrackedMod> =
73 domain_mods.iter().map(|m| (m.nexus_mod_id, m)).collect();
74
75 let updated = api.updated_mods(domain, period).await?;
77
78 for nexus_mod in &updated {
79 if let Some(tracked_mod) = lookup.get(&nexus_mod.mod_id) {
80 let install_ts = tracked_mod.installed_timestamp as u64;
82 if nexus_mod.latest_file_update > install_ts {
83 updates.push(ModUpdate {
84 mod_id: tracked_mod.mod_id.clone(),
85 nexus_mod_id: nexus_mod.mod_id,
86 installed_version: tracked_mod.installed_version.clone(),
87 installed_timestamp: tracked_mod.installed_timestamp,
88 latest_file_update: nexus_mod.latest_file_update,
89 latest_mod_activity: nexus_mod.latest_mod_activity,
90 });
91 }
92 }
93 }
94 }
95
96 if updates.is_empty() {
97 info!("all tracked mods are up to date");
98 } else {
99 info!(count = updates.len(), "found mods with available updates");
100 }
101
102 Ok(updates)
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_tracked_mod_grouping() {
111 let tracked = vec![
112 TrackedMod {
113 mod_id: "skyui".to_string(),
114 nexus_mod_id: NexusModId::from(12604),
115 nexus_game_domain: "skyrimspecialedition".to_string(),
116 installed_version: Some("5.2".to_string()),
117 installed_timestamp: 1700000000,
118 },
119 TrackedMod {
120 mod_id: "ussep".to_string(),
121 nexus_mod_id: NexusModId::from(266),
122 nexus_game_domain: "skyrimspecialedition".to_string(),
123 installed_version: Some("4.2.8".to_string()),
124 installed_timestamp: 1700000000,
125 },
126 TrackedMod {
127 mod_id: "fo4_patch".to_string(),
128 nexus_mod_id: NexusModId::from(4598),
129 nexus_game_domain: "fallout4".to_string(),
130 installed_version: None,
131 installed_timestamp: 1700000000,
132 },
133 ];
134
135 let mut by_domain: HashMap<&str, Vec<&TrackedMod>> = HashMap::new();
136 for t in &tracked {
137 by_domain
138 .entry(t.nexus_game_domain.as_str())
139 .or_default()
140 .push(t);
141 }
142
143 assert_eq!(by_domain.len(), 2);
144 assert_eq!(by_domain["skyrimspecialedition"].len(), 2);
145 assert_eq!(by_domain["fallout4"].len(), 1);
146 }
147}