game_manager_lib/services/integration/
nexus.rs1use crate::utils::text::{normalize_for_matching, strip_edition_suffix};
2
3use serde::Deserialize;
4
5#[derive(Debug, Deserialize)]
8pub struct NexusGame {
9 pub id: i64,
10 pub name: String,
11 pub domain_name: String,
12 pub genre: Option<String>,
13 pub approved_date: Option<i64>,
14}
15
16pub async fn fetch_nexus_games(api_key: &str) -> Result<Vec<NexusGame>, reqwest::Error> {
19 let client = reqwest::Client::new();
20
21 let response = client
22 .get("https://api.nexusmods.com/v1/games.json")
23 .header("accept", "application/json")
24 .header("apikey", api_key)
25 .header("User-Agent", "Playlite/1.0.0 (Windows_NT 10.0; x64)")
26 .send()
27 .await?
28 .error_for_status()?;
29
30 response.json::<Vec<NexusGame>>().await
31}
32
33fn normalize_title(name: &str) -> String {
37 normalize_for_matching(&strip_edition_suffix(name))
38}
39
40pub fn find_best_nexus_match<'a>(
41 game_name: &str,
42 nexus_games: &'a [NexusGame],
43) -> Option<&'a NexusGame> {
44 let normalized_target = normalize_title(game_name);
45
46 if let Some(exact) = nexus_games
48 .iter()
49 .find(|g| normalize_title(&g.name) == normalized_target)
50 {
51 return Some(exact);
52 }
53
54 const SIMILARITY_THRESHOLD: f64 = 0.92;
56
57 nexus_games
58 .iter()
59 .map(|g| {
60 let score = strsim::jaro_winkler(&normalized_target, &normalize_title(&g.name));
61 (g, score)
62 })
63 .filter(|(_, score)| *score >= SIMILARITY_THRESHOLD)
64 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
65 .map(|(g, _)| g)
66}