Skip to main content

modde_sources/nexus/
updates.rs

1use std::collections::HashMap;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use tracing::{debug, info};
6
7use super::api::NexusApi;
8
9/// Information about a mod that has an available update on Nexus.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ModUpdate {
12    /// The mod_id string used in the profile (local identifier).
13    pub mod_id: String,
14    /// Nexus mod ID.
15    pub nexus_mod_id: u64,
16    /// Currently installed version (if known).
17    pub installed_version: Option<String>,
18    /// Timestamp when the mod was installed locally.
19    pub installed_timestamp: i64,
20    /// Timestamp of the latest file update on Nexus.
21    pub latest_file_update: u64,
22    /// Timestamp of the latest mod activity on Nexus.
23    pub latest_mod_activity: u64,
24}
25
26/// A mod tracked for update checking.
27#[derive(Debug, Clone)]
28pub struct TrackedMod {
29    pub mod_id: String,
30    pub nexus_mod_id: u64,
31    pub nexus_game_domain: String,
32    pub installed_version: Option<String>,
33    pub installed_timestamp: i64,
34}
35
36/// Check for updates for a set of tracked mods.
37///
38/// Groups mods by game domain, calls `updated_mods` once per domain,
39/// then cross-references against installed timestamps.
40pub async fn check_updates(
41    api: &NexusApi,
42    tracked: &[TrackedMod],
43    period: &str,
44) -> Result<Vec<ModUpdate>> {
45    if tracked.is_empty() {
46        return Ok(Vec::new());
47    }
48
49    // Group tracked mods by game domain
50    let mut by_domain: HashMap<&str, Vec<&TrackedMod>> = HashMap::new();
51    for t in tracked {
52        by_domain.entry(t.nexus_game_domain.as_str()).or_default().push(t);
53    }
54
55    let mut updates = Vec::new();
56
57    for (domain, domain_mods) in &by_domain {
58        debug!(domain, mod_count = domain_mods.len(), "checking updates for domain");
59
60        // Build a lookup: nexus_mod_id -> TrackedMod
61        let lookup: HashMap<u64, &&TrackedMod> = domain_mods
62            .iter()
63            .map(|m| (m.nexus_mod_id, m))
64            .collect();
65
66        // One API call per domain
67        let updated = api.updated_mods(domain, period).await?;
68
69        for nexus_mod in &updated {
70            if let Some(tracked_mod) = lookup.get(&nexus_mod.mod_id) {
71                // Mod is in our tracked list — check if update is newer
72                let install_ts = tracked_mod.installed_timestamp as u64;
73                if nexus_mod.latest_file_update > install_ts {
74                    updates.push(ModUpdate {
75                        mod_id: tracked_mod.mod_id.clone(),
76                        nexus_mod_id: nexus_mod.mod_id,
77                        installed_version: tracked_mod.installed_version.clone(),
78                        installed_timestamp: tracked_mod.installed_timestamp,
79                        latest_file_update: nexus_mod.latest_file_update,
80                        latest_mod_activity: nexus_mod.latest_mod_activity,
81                    });
82                }
83            }
84        }
85    }
86
87    if updates.is_empty() {
88        info!("all tracked mods are up to date");
89    } else {
90        info!(count = updates.len(), "found mods with available updates");
91    }
92
93    Ok(updates)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_tracked_mod_grouping() {
102        let tracked = vec![
103            TrackedMod {
104                mod_id: "skyui".to_string(),
105                nexus_mod_id: 12604,
106                nexus_game_domain: "skyrimspecialedition".to_string(),
107                installed_version: Some("5.2".to_string()),
108                installed_timestamp: 1700000000,
109            },
110            TrackedMod {
111                mod_id: "ussep".to_string(),
112                nexus_mod_id: 266,
113                nexus_game_domain: "skyrimspecialedition".to_string(),
114                installed_version: Some("4.2.8".to_string()),
115                installed_timestamp: 1700000000,
116            },
117            TrackedMod {
118                mod_id: "fo4_patch".to_string(),
119                nexus_mod_id: 4598,
120                nexus_game_domain: "fallout4".to_string(),
121                installed_version: None,
122                installed_timestamp: 1700000000,
123            },
124        ];
125
126        let mut by_domain: HashMap<&str, Vec<&TrackedMod>> = HashMap::new();
127        for t in &tracked {
128            by_domain.entry(t.nexus_game_domain.as_str()).or_default().push(t);
129        }
130
131        assert_eq!(by_domain.len(), 2);
132        assert_eq!(by_domain["skyrimspecialedition"].len(), 2);
133        assert_eq!(by_domain["fallout4"].len(), 1);
134    }
135}