Fix Modrinth version number display issues and improve plugin matching algorithm
This commit is contained in:
parent
4adb291592
commit
78f22f65f4
87
ROADMAP.md
87
ROADMAP.md
@ -6,33 +6,75 @@
|
|||||||
- [x] Setup basic project structure
|
- [x] Setup basic project structure
|
||||||
- [x] Create Remote repository (optional)
|
- [x] Create Remote repository (optional)
|
||||||
|
|
||||||
## Core Infrastructure (In Progress)
|
## Core Infrastructure (Completed) ✅
|
||||||
- [x] Setup SQLite or JSON storage for plugin data
|
- [x] Setup SQLite or JSON storage for plugin data
|
||||||
- [x] Create core data models
|
- [x] Create core data models
|
||||||
- [x] Build server/plugin directory scanner (basic implementation)
|
- [x] Build server/plugin directory scanner (basic implementation)
|
||||||
- [x] Implement asynchronous server scanning (non-blocking)
|
- [x] Implement asynchronous server scanning (non-blocking)
|
||||||
- [x] Implement JAR file parser for plugin.yml extraction
|
- [x] Implement JAR file parser for plugin.yml extraction
|
||||||
|
|
||||||
## Plugin Discovery (Needs Refinement)
|
## Plugin Discovery (Completed) ✅
|
||||||
- [ ] Improve server type detection (Paper/Spigot distinction, etc.)
|
- [x] Improve server type detection (Paper/Spigot distinction, etc.)
|
||||||
- [x] Implement plugins folder detection logic
|
- [x] Implement plugins folder detection logic
|
||||||
- [x] Design plugin metadata extraction system
|
- [x] Design plugin metadata extraction system
|
||||||
|
- [x] Extract basic fields (name, version, author, etc.)
|
||||||
|
- [x] Extract website field from plugin.yml
|
||||||
- [x] Build plugin hash identification system
|
- [x] Build plugin hash identification system
|
||||||
|
- [x] Create plugin matching algorithm
|
||||||
|
|
||||||
## Web Crawler Development (In Progress)
|
## Web Crawler Development (Mostly Completed)
|
||||||
- [x] Create base web crawler architecture
|
- [x] Create base web crawler architecture
|
||||||
- [x] Implement HangarMC crawler
|
- [x] Implement HangarMC crawler
|
||||||
- [ ] Implement SpigotMC crawler
|
- [x] Implement SpigotMC data source using SpiGet API
|
||||||
- [ ] Implement Modrinth crawler
|
- [x] Basic implementation
|
||||||
|
- [x] Fix version information retrieval issues
|
||||||
|
- [ ] Handle rate limiting and 403 errors
|
||||||
|
- [x] Implement Modrinth crawler
|
||||||
|
- [x] Basic implementation
|
||||||
|
- [x] Version compatibility checking
|
||||||
|
- [x] Fix version number display issues
|
||||||
|
- [ ] Handle rate limit issues
|
||||||
- [ ] Implement GitHub releases crawler
|
- [ ] Implement GitHub releases crawler
|
||||||
- [ ] Create plugin matching algorithm
|
- [x] Basic implementation
|
||||||
|
- [ ] Complete API authentication
|
||||||
|
|
||||||
## Update Management (Upcoming)
|
## Update Management (Mostly Completed)
|
||||||
- [ ] Build update detection system
|
- [x] Build update detection system (Basic logic)
|
||||||
- [ ] Implement changelog extraction
|
- [x] Improve update detection reliability
|
||||||
- [ ] Create plugin backup functionality
|
- [x] Correctly fetch detailed version information from repositories
|
||||||
- [ ] Develop plugin replacement logic
|
- [x] Implement robust version comparison logic (handling non-SemVer, suffixes, etc.)
|
||||||
- [ ] Add server restart capabilities (optional)
|
- [x] Implement proper request headers (User-Agent, etc.) to mimic browser behavior
|
||||||
|
- [x] Add delays between API requests to mitigate rate limiting
|
||||||
|
- [x] Implement automatic retry logic for rate limits (429) and network errors
|
||||||
|
- [x] Implement API response caching (reduce requests, improve performance)
|
||||||
|
- [x] Implement intelligent name variation searches for plugins
|
||||||
|
- [x] Fix SpigotMC version retrieval issues
|
||||||
|
- [x] Fix update check progress getting stuck
|
||||||
|
- [x] Complete Modrinth integration
|
||||||
|
- [x] Fix incorrect plugin matching issues
|
||||||
|
- [x] Improve matching algorithm to reduce false positives
|
||||||
|
- [ ] Complete GitHub integration
|
||||||
|
- [ ] Add GitHub API authentication (optional token)
|
||||||
|
- [ ] Investigate SpigotMC 403 errors (potentially add headers/delay)
|
||||||
|
- [x] Implement changelog extraction
|
||||||
|
- [x] Create plugin backup functionality
|
||||||
|
- [x] Develop plugin replacement logic
|
||||||
|
- [x] Implement plugin source persistence (store identified repo/ID to avoid searching)
|
||||||
|
- [ ] Allow manual setting/clearing of persistent source
|
||||||
|
- [x] Add plugin update UI integration
|
||||||
|
- [x] Add update buttons to plugin listing and details view
|
||||||
|
- [x] Show update progress with loading indicators
|
||||||
|
- [x] Handle file access errors gracefully
|
||||||
|
- [x] Ensure correct version numbering in filenames
|
||||||
|
- [ ] Present multiple potential matches for ambiguous plugins
|
||||||
|
- [ ] Make version numbers clickable links to repository sources
|
||||||
|
- [ ] Allow user selection of correct plugin match when multiple are found
|
||||||
|
- [ ] Server platform compatibility matching (High Priority)
|
||||||
|
- [ ] Detect server platform and version accurately (Paper, Spigot, Forge, NeoForge, Fabric, etc.)
|
||||||
|
- [ ] Filter plugin updates to match the server platform
|
||||||
|
- [ ] Prevent incompatible version updates
|
||||||
|
- [ ] Add platform indicators in the UI for available updates
|
||||||
|
- [ ] Allow manual selection of target platform when multiple are available
|
||||||
|
|
||||||
## UI Development (In Progress)
|
## UI Development (In Progress)
|
||||||
- [x] Design and implement main dashboard
|
- [x] Design and implement main dashboard
|
||||||
@ -41,8 +83,15 @@
|
|||||||
- [x] Build server folder selection interface
|
- [x] Build server folder selection interface
|
||||||
- [x] Implement plugin detail view
|
- [x] Implement plugin detail view
|
||||||
- [x] Add update notification system
|
- [x] Add update notification system
|
||||||
|
- [x] Add per-plugin update check button & loading indicator
|
||||||
|
- [x] Add visual feedback for bulk update check (progress, loading state)
|
||||||
|
- [x] Implement error handling in UI with user-friendly messages
|
||||||
- [ ] Create settings panel
|
- [ ] Create settings panel
|
||||||
- [x] Implement dark mode
|
- [x] Implement dark mode
|
||||||
|
- [ ] Implement plugin matching disambiguation UI
|
||||||
|
- [ ] Add clickable version links to repository pages
|
||||||
|
- [ ] Add error recovery UI for failed updates
|
||||||
|
- [ ] Add detailed progress logging in UI for debugging
|
||||||
|
|
||||||
## Security Features (Upcoming)
|
## Security Features (Upcoming)
|
||||||
- [ ] Implement sandboxing for network requests
|
- [ ] Implement sandboxing for network requests
|
||||||
@ -51,14 +100,18 @@
|
|||||||
|
|
||||||
## Testing & Refinement (Upcoming)
|
## Testing & Refinement (Upcoming)
|
||||||
- [ ] Comprehensive testing with various server setups
|
- [ ] Comprehensive testing with various server setups
|
||||||
- [ ] Performance optimization
|
- [ ] Performance optimization (especially concurrent requests)
|
||||||
- [ ] Error handling improvements
|
- [ ] Error handling improvements (parsing, network, etc.)
|
||||||
- [ ] User acceptance testing
|
- [ ] User acceptance testing
|
||||||
|
- [ ] Add automated tests for critical functionality
|
||||||
|
- [ ] Add error telemetry (optional)
|
||||||
|
|
||||||
## Documentation (Upcoming)
|
## Documentation (Upcoming)
|
||||||
- [ ] Create user documentation
|
- [ ] Create user documentation
|
||||||
- [ ] Write developer documentation
|
- [ ] Write developer documentation
|
||||||
- [ ] Create installation guide
|
- [ ] Create installation guide
|
||||||
|
- [ ] Add troubleshooting guide
|
||||||
|
- [ ] Document known limitations and workarounds
|
||||||
|
|
||||||
## Deployment (Upcoming)
|
## Deployment (Upcoming)
|
||||||
- [ ] Prepare release process
|
- [ ] Prepare release process
|
||||||
@ -74,4 +127,6 @@
|
|||||||
- [ ] Scheduled plugin update checks
|
- [ ] Scheduled plugin update checks
|
||||||
- [ ] Rollback system
|
- [ ] Rollback system
|
||||||
- [ ] Plugin recommendation system
|
- [ ] Plugin recommendation system
|
||||||
- [ ] Discord webhook integration
|
- [ ] Discord webhook integration
|
||||||
|
- [ ] Plugin search and browse functionality
|
||||||
|
- [ ] Plugin install from repository
|
328
src-tauri/src/crawlers/modrinth.rs
Normal file
328
src-tauri/src/crawlers/modrinth.rs
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
use std::path::Path;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use crate::{HttpClient, ServerType};
|
||||||
|
use crate::models::repository::{RepositoryPlugin, RepositorySource};
|
||||||
|
use crate::platform_matcher::is_version_compatible_with_server;
|
||||||
|
use urlencoding;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use crate::crawlers::Repository;
|
||||||
|
|
||||||
|
// Modrinth API response structures (Based on https://docs.modrinth.com/api-spec/)
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthSearchResponse {
|
||||||
|
hits: Vec<ModrinthSearchHit>,
|
||||||
|
// Omitting pagination fields (offset, limit, total_hits) for simplicity
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthSearchHit {
|
||||||
|
project_id: String,
|
||||||
|
slug: String, // Use this or project_id for details requests
|
||||||
|
project_type: String, // e.g., "mod", "plugin"
|
||||||
|
author: String, // Sometimes different from the project owner
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
categories: Vec<String>,
|
||||||
|
versions: Vec<String>, // List of supported Minecraft versions
|
||||||
|
downloads: u64,
|
||||||
|
icon_url: Option<String>,
|
||||||
|
latest_version: Option<String>, // Version number of the latest version
|
||||||
|
date_modified: String,
|
||||||
|
// Omitting client_side, server_side, display_categories, gallery, featured_gallery, license, follows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthProject {
|
||||||
|
id: String,
|
||||||
|
slug: String,
|
||||||
|
project_type: String,
|
||||||
|
team: String, // Team ID, need another call potentially for author names?
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
body: String, // Full description
|
||||||
|
categories: Vec<String>,
|
||||||
|
game_versions: Vec<String>,
|
||||||
|
downloads: u64,
|
||||||
|
icon_url: Option<String>,
|
||||||
|
published: String,
|
||||||
|
updated: String,
|
||||||
|
// Omitting donation_urls, license, client_side, server_side, gallery, status, moderator_message, versions
|
||||||
|
// We might need the 'versions' array (list of version IDs) for getting specific version details
|
||||||
|
}
|
||||||
|
|
||||||
|
// Structure for the response from /project/{id}/version
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthVersion {
|
||||||
|
id: String,
|
||||||
|
project_id: String,
|
||||||
|
author_id: String, // User ID, need another call for name?
|
||||||
|
name: String, // Version title
|
||||||
|
version_number: String,
|
||||||
|
changelog: Option<String>,
|
||||||
|
date_published: String,
|
||||||
|
downloads: u64,
|
||||||
|
version_type: String, // e.g., "release", "beta", "alpha"
|
||||||
|
files: Vec<ModrinthVersionFile>,
|
||||||
|
game_versions: Vec<String>,
|
||||||
|
loaders: Vec<String>,
|
||||||
|
// Omitting dependencies, featured, status, requested_status
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthVersionFile {
|
||||||
|
hashes: ModrinthHashes,
|
||||||
|
url: String,
|
||||||
|
filename: String,
|
||||||
|
primary: bool,
|
||||||
|
size: u64,
|
||||||
|
// Omitting file_type (useful if filtering non-JARs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthHashes {
|
||||||
|
sha1: String,
|
||||||
|
sha512: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modrinth crawler implementation
|
||||||
|
pub struct ModrinthCrawler {
|
||||||
|
client: Arc<HttpClient>,
|
||||||
|
api_base_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModrinthCrawler {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ModrinthCrawler {
|
||||||
|
client: Arc::new(HttpClient::new()),
|
||||||
|
api_base_url: "https://api.modrinth.com/v2".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get versions, now async
|
||||||
|
async fn get_project_versions_internal(&self, plugin_id: &str) -> Result<Vec<ModrinthVersion>, Box<dyn Error + Send + Sync>> {
|
||||||
|
let url = format!("{}/project/{}/version", self.api_base_url, plugin_id);
|
||||||
|
let response_body = self.client.get(&url).await?;
|
||||||
|
let versions: Vec<ModrinthVersion> = serde_json::from_str(&response_body)?;
|
||||||
|
Ok(versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get compatible versions for a server type
|
||||||
|
async fn get_compatible_versions(&self, plugin_id: &str, server_type: Option<&ServerType>) -> Result<Vec<ModrinthVersion>, Box<dyn Error + Send + Sync>> {
|
||||||
|
let all_versions = self.get_project_versions_internal(plugin_id).await?;
|
||||||
|
|
||||||
|
// If no server type provided, return all versions
|
||||||
|
if server_type.is_none() {
|
||||||
|
return Ok(all_versions);
|
||||||
|
}
|
||||||
|
|
||||||
|
let server_type = server_type.unwrap();
|
||||||
|
let compatible_versions: Vec<ModrinthVersion> = all_versions.into_iter()
|
||||||
|
.filter(|version| {
|
||||||
|
// Check if this version is compatible with our server
|
||||||
|
is_version_compatible_with_server(&version.loaders, server_type)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// If no compatible versions found, log a warning
|
||||||
|
if compatible_versions.is_empty() {
|
||||||
|
println!("[ModrinthCrawler::get_compatible_versions] Warning: No versions compatible with server type {:?} for plugin {}", server_type, plugin_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(compatible_versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to get user details from ID, now async
|
||||||
|
async fn get_user_details(&self, user_id: &str) -> Result<ModrinthUser, Box<dyn Error + Send + Sync>> {
|
||||||
|
let url = format!("{}/user/{}", self.api_base_url, user_id);
|
||||||
|
let response_body = self.client.get(&url).await?;
|
||||||
|
let user: ModrinthUser = serde_json::from_str(&response_body)?;
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct ModrinthUser {
|
||||||
|
username: String,
|
||||||
|
// other fields like id, name, avatar_url etc. are available if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModrinthCrawler {
|
||||||
|
pub async fn get_plugin_details_with_server_type(&self, plugin_id: &str, server_type: Option<&ServerType>) -> Result<RepositoryPlugin, String> {
|
||||||
|
let project_url = format!("{}/project/{}", self.api_base_url, plugin_id);
|
||||||
|
let project_response_body = match self.client.get(&project_url).await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => return Err(format!("Modrinth project request failed: {}", e)),
|
||||||
|
};
|
||||||
|
let project: ModrinthProject = match serde_json::from_str(&project_response_body) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => return Err(format!("Failed to parse Modrinth project: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let page_url = format!("https://modrinth.com/plugin/{}", project.slug);
|
||||||
|
|
||||||
|
// Fetch compatible versions
|
||||||
|
let versions = match self.get_compatible_versions(&project.id, server_type).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to fetch Modrinth versions for {}: {}", plugin_id, e);
|
||||||
|
Vec::new() // Continue with empty versions if fetch fails
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let latest_version_opt = versions.first();
|
||||||
|
|
||||||
|
// Explicitly use version_number instead of id
|
||||||
|
let latest_version_number = latest_version_opt.map_or("Unknown".to_string(), |v| {
|
||||||
|
// Always use the version_number field which is the actual semantic version
|
||||||
|
// not the internal Modrinth ID
|
||||||
|
v.version_number.clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
let changelog = latest_version_opt.and_then(|v| v.changelog.clone());
|
||||||
|
let author_id = latest_version_opt.map(|v| v.author_id.clone());
|
||||||
|
|
||||||
|
// Fetch author name if ID is available
|
||||||
|
let author_name = if let Some(id) = author_id {
|
||||||
|
match self.get_user_details(&id).await {
|
||||||
|
Ok(user) => user.username,
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to fetch Modrinth author details for ID {}: {}", id, e);
|
||||||
|
"Unknown Author".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
project.team.clone() // Fallback to team ID if no version author found
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_file = latest_version_opt.and_then(|v| v.files.iter().find(|f| f.primary));
|
||||||
|
|
||||||
|
Ok(RepositoryPlugin {
|
||||||
|
id: project.id.clone(),
|
||||||
|
name: project.title,
|
||||||
|
version: latest_version_number,
|
||||||
|
description: Some(project.description),
|
||||||
|
authors: vec![author_name],
|
||||||
|
download_url: primary_file.map_or(String::new(), |f| f.url.clone()),
|
||||||
|
repository: RepositorySource::Modrinth,
|
||||||
|
page_url,
|
||||||
|
download_count: Some(project.downloads),
|
||||||
|
last_updated: Some(project.updated),
|
||||||
|
icon_url: project.icon_url,
|
||||||
|
minecraft_versions: latest_version_opt.map_or(project.game_versions.clone(), |v| v.game_versions.clone()),
|
||||||
|
categories: project.categories,
|
||||||
|
rating: None, // Modrinth API doesn't provide rating directly
|
||||||
|
file_size: primary_file.map(|f| f.size),
|
||||||
|
file_hash: primary_file.map(|f| f.hashes.sha512.clone()), // Use SHA512
|
||||||
|
changelog,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn download_plugin_with_server_type(&self, plugin_id: &str, version_number_str: &str, destination: &Path, server_type: Option<&ServerType>) -> Result<String, String> {
|
||||||
|
let versions = match self.get_compatible_versions(plugin_id, server_type).await {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(e) => return Err(format!("Failed to get Modrinth versions for download: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_version = versions.iter().find(|v| v.version_number == version_number_str);
|
||||||
|
|
||||||
|
let version_to_download = match target_version {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return Err(format!("Version '{}' not found or not compatible for plugin {}", version_number_str, plugin_id)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let primary_file = match version_to_download.files.iter().find(|f| f.primary) {
|
||||||
|
Some(f) => f,
|
||||||
|
None => return Err(format!("No primary file found for version '{}' of plugin {}", version_number_str, plugin_id)),
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.client.download(&primary_file.url, destination).await {
|
||||||
|
Ok(_) => Ok(destination.to_string_lossy().to_string()),
|
||||||
|
Err(e) => Err(format!("Failed to download from Modrinth: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl Repository for ModrinthCrawler {
|
||||||
|
fn get_repository_name(&self) -> String {
|
||||||
|
"Modrinth".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn search(&self, query: &str) -> Result<Vec<RepositoryPlugin>, String> {
|
||||||
|
let encoded_query = urlencoding::encode(query);
|
||||||
|
// Corrected format string with proper quoting for facets
|
||||||
|
let url = format!(
|
||||||
|
"{}/search?query={}&facets=[[\"project_type:plugin\"]]&limit=20",
|
||||||
|
self.api_base_url,
|
||||||
|
encoded_query
|
||||||
|
);
|
||||||
|
|
||||||
|
let response_body = match self.client.get(&url).await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => return Err(format!("Modrinth search request failed: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_response: ModrinthSearchResponse = match serde_json::from_str(&response_body) {
|
||||||
|
Ok(sr) => sr,
|
||||||
|
Err(e) => return Err(format!("Failed to parse Modrinth search results: {}", e)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for hit in search_response.hits {
|
||||||
|
// For search results, we don't have version_number directly,
|
||||||
|
// so we need to fetch the latest version for each project
|
||||||
|
let version_info = if let Some(latest_version) = &hit.latest_version {
|
||||||
|
// If latest_version looks like an ID (no dots and long), fetch the real version number
|
||||||
|
if latest_version.len() >= 8 && !latest_version.contains('.') {
|
||||||
|
match self.get_project_versions_internal(&hit.project_id).await {
|
||||||
|
Ok(versions) if !versions.is_empty() => {
|
||||||
|
versions[0].version_number.clone()
|
||||||
|
},
|
||||||
|
_ => latest_version.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
latest_version.clone()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"Unknown".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a simplified repository plugin from the search hit
|
||||||
|
let repo_plugin = RepositoryPlugin {
|
||||||
|
id: hit.project_id.clone(),
|
||||||
|
name: hit.title,
|
||||||
|
version: version_info,
|
||||||
|
description: Some(hit.description),
|
||||||
|
authors: vec![hit.author],
|
||||||
|
download_url: String::new(), // Will be filled in when getting specific version
|
||||||
|
repository: RepositorySource::Modrinth,
|
||||||
|
page_url: format!("https://modrinth.com/plugin/{}", hit.slug),
|
||||||
|
download_count: Some(hit.downloads),
|
||||||
|
last_updated: Some(hit.date_modified),
|
||||||
|
icon_url: hit.icon_url,
|
||||||
|
minecraft_versions: hit.versions,
|
||||||
|
categories: hit.categories,
|
||||||
|
rating: None,
|
||||||
|
file_size: None,
|
||||||
|
file_hash: None,
|
||||||
|
changelog: None,
|
||||||
|
};
|
||||||
|
results.push(repo_plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_plugin_details(&self, plugin_id: &str) -> Result<RepositoryPlugin, String> {
|
||||||
|
// Just delegate to our server-type aware version with None
|
||||||
|
self.get_plugin_details_with_server_type(plugin_id, None).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn download_plugin(&self, plugin_id: &str, version_number_str: &str, destination: &Path) -> Result<String, String> {
|
||||||
|
// Just delegate to our server-type aware version with None
|
||||||
|
self.download_plugin_with_server_type(plugin_id, version_number_str, destination, None).await
|
||||||
|
}
|
||||||
|
}
|
278
src-tauri/src/services/update_manager/update_checker.rs
Normal file
278
src-tauri/src/services/update_manager/update_checker.rs
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use std::error::Error;
|
||||||
|
use futures::{future, stream::StreamExt};
|
||||||
|
use tauri::async_runtime::spawn;
|
||||||
|
|
||||||
|
use crate::models::plugin::Plugin;
|
||||||
|
use crate::models::repository::{RepositorySource, RepositoryPlugin, BulkUpdateProgress, SingleUpdateResult};
|
||||||
|
use super::version_utils::compare_plugin_versions;
|
||||||
|
|
||||||
|
/// Check for updates for multiple plugins
|
||||||
|
pub async fn check_for_plugin_updates(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
installed_plugins: Vec<Plugin>,
|
||||||
|
repositories_to_check: Vec<RepositorySource>
|
||||||
|
) -> Result<Vec<Plugin>, String> {
|
||||||
|
if installed_plugins.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone plugins for returning
|
||||||
|
let mut updated_plugins = installed_plugins.clone();
|
||||||
|
|
||||||
|
// Initialize progress
|
||||||
|
let total = installed_plugins.len();
|
||||||
|
println!("Starting bulk update check for {} plugins", total);
|
||||||
|
|
||||||
|
if let Err(e) = app_handle.emit("bulk_update_start", total) {
|
||||||
|
return Err(format!("Failed to emit bulk_update_start event: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process plugins in batches to avoid overwhelming the system
|
||||||
|
let batch_size = 5;
|
||||||
|
for (batch_index, chunk) in installed_plugins.chunks(batch_size).enumerate() {
|
||||||
|
let mut batch_futures = Vec::new();
|
||||||
|
|
||||||
|
// Create futures for this batch
|
||||||
|
for (i, plugin) in chunk.iter().enumerate() {
|
||||||
|
let index = batch_index * batch_size + i;
|
||||||
|
let app_handle_clone = app_handle.clone();
|
||||||
|
let repos_clone = repositories_to_check.clone();
|
||||||
|
let plugin_clone = plugin.clone();
|
||||||
|
|
||||||
|
// Report progress
|
||||||
|
let progress = BulkUpdateProgress {
|
||||||
|
processed: index,
|
||||||
|
total,
|
||||||
|
current_plugin_name: plugin.name.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = app_handle_clone.emit("update_check_progress", progress) {
|
||||||
|
println!("Failed to emit update progress: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to batch
|
||||||
|
batch_futures.push(async move {
|
||||||
|
// Process plugin
|
||||||
|
println!("Processing update for plugin {}/{}: {}", index + 1, total, plugin_clone.name);
|
||||||
|
let result = process_plugin_update(plugin_clone.clone(), &repos_clone).await;
|
||||||
|
(index, result)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process batch concurrently
|
||||||
|
let batch_results = futures::future::join_all(batch_futures).await;
|
||||||
|
|
||||||
|
// Update plugins with results from this batch
|
||||||
|
for (index, updated_plugin) in batch_results {
|
||||||
|
if let Some(position) = updated_plugins.iter().position(|p| p.file_path == updated_plugin.file_path) {
|
||||||
|
updated_plugins[position] = updated_plugin;
|
||||||
|
println!("Updated plugin {}: {}", index, updated_plugins[position].name);
|
||||||
|
if let Some(latest) = &updated_plugins[position].latest_version {
|
||||||
|
println!(" Current version: {}, Latest version: {}, Has update: {}",
|
||||||
|
updated_plugins[position].version, latest, updated_plugins[position].has_update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small delay between batches to avoid overwhelming
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final progress report
|
||||||
|
let final_progress = BulkUpdateProgress {
|
||||||
|
processed: total,
|
||||||
|
total,
|
||||||
|
current_plugin_name: "Completed".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = app_handle.emit("update_check_progress", final_progress) {
|
||||||
|
println!("Failed to emit final update progress: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Bulk update check completed for {} plugins", total);
|
||||||
|
Ok(updated_plugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check for updates for a single plugin
|
||||||
|
pub async fn check_single_plugin_update(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
plugin_to_check: Plugin,
|
||||||
|
repositories_to_check: Vec<RepositorySource>,
|
||||||
|
) -> Result<(), String> { // Returns Result<(), String> because result is sent via event
|
||||||
|
// Begin check
|
||||||
|
if let Err(e) = app_handle.emit("single_update_check_started", plugin_to_check.name.clone()) {
|
||||||
|
return Err(format!("Failed to emit single_update_check_started event: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process update
|
||||||
|
let updated_plugin = process_plugin_update(plugin_to_check.clone(), &repositories_to_check).await;
|
||||||
|
|
||||||
|
// Create result
|
||||||
|
let result = SingleUpdateResult {
|
||||||
|
original_file_path: plugin_to_check.file_path.clone(),
|
||||||
|
plugin: Some(updated_plugin),
|
||||||
|
error: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Emit result
|
||||||
|
if let Err(e) = app_handle.emit("single_update_check_completed", result) {
|
||||||
|
return Err(format!("Failed to emit single_update_check_completed event: {}", e));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process update check for a single plugin
|
||||||
|
async fn process_plugin_update(plugin: Plugin, repositories: &[RepositorySource]) -> Plugin {
|
||||||
|
let mut updated_plugin = plugin.clone();
|
||||||
|
println!("Checking for updates for plugin: {}", plugin.name);
|
||||||
|
|
||||||
|
// If plugin already has repository info, check that specific repo
|
||||||
|
if let (Some(repo_source), Some(repo_id)) = (&plugin.repository_source, &plugin.repository_id) {
|
||||||
|
println!("Plugin has existing repository info: {:?}, ID: {}", repo_source, repo_id);
|
||||||
|
match crate::lib_get_plugin_details_from_repository(repo_id, repo_source.clone(), None).await {
|
||||||
|
Ok(repo_plugin) => {
|
||||||
|
println!("Successfully got details from repository for {}", plugin.name);
|
||||||
|
update_plugin_from_repo(&mut updated_plugin, &repo_plugin);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to get update details for {}: {}", plugin.name, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updated_plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, try smart matching with repositories
|
||||||
|
println!("No repository info for {}. Trying smart matching...", plugin.name);
|
||||||
|
let matches = match crate::search_with_variations(&plugin.name, repositories).await {
|
||||||
|
Ok(matches) => {
|
||||||
|
println!("Found {} potential matches for {}", matches.len(), plugin.name);
|
||||||
|
matches
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error searching for plugin {}: {}", plugin.name, e);
|
||||||
|
return updated_plugin;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find best match
|
||||||
|
for repo_plugin in matches {
|
||||||
|
if is_likely_match(&plugin, &repo_plugin) {
|
||||||
|
println!("Found likely match for {} in {:?}: {} ({})",
|
||||||
|
plugin.name, repo_plugin.repository, repo_plugin.name, repo_plugin.version);
|
||||||
|
update_plugin_from_repo(&mut updated_plugin, &repo_plugin);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Update check complete for plugin: {}", plugin.name);
|
||||||
|
updated_plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update plugin with repository information
|
||||||
|
fn update_plugin_from_repo(plugin: &mut Plugin, repo_plugin: &RepositoryPlugin) {
|
||||||
|
// Set repository information
|
||||||
|
plugin.repository_source = Some(repo_plugin.repository.clone());
|
||||||
|
plugin.repository_id = Some(repo_plugin.id.clone());
|
||||||
|
plugin.repository_url = Some(repo_plugin.page_url.clone());
|
||||||
|
|
||||||
|
// For Modrinth, check if the version looks like an ID instead of a version number
|
||||||
|
let latest_version = if repo_plugin.repository == RepositorySource::Modrinth &&
|
||||||
|
repo_plugin.version.len() >= 8 &&
|
||||||
|
!repo_plugin.version.contains('.') {
|
||||||
|
// This looks like an ID rather than a version, use "Latest" as a fallback
|
||||||
|
"Latest".to_string()
|
||||||
|
} else {
|
||||||
|
repo_plugin.version.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set latest version if newer
|
||||||
|
if latest_version != plugin.version {
|
||||||
|
let has_update = compare_plugin_versions(&plugin.version, &latest_version);
|
||||||
|
plugin.latest_version = Some(latest_version);
|
||||||
|
plugin.has_update = has_update;
|
||||||
|
plugin.changelog = repo_plugin.changelog.clone();
|
||||||
|
} else {
|
||||||
|
plugin.has_update = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if a repository plugin is likely a match for the installed plugin
|
||||||
|
fn is_likely_match(installed: &Plugin, repo: &RepositoryPlugin) -> bool {
|
||||||
|
// Require multiple criteria to match to avoid false positives
|
||||||
|
let mut match_points = 0;
|
||||||
|
|
||||||
|
// Name similarity check
|
||||||
|
let name_similarity = calculate_name_similarity(&installed.name, &repo.name);
|
||||||
|
|
||||||
|
// Strong name match (over 0.9 similarity)
|
||||||
|
if name_similarity > 0.9 {
|
||||||
|
match_points += 2;
|
||||||
|
}
|
||||||
|
// Moderate name match
|
||||||
|
else if name_similarity > 0.7 {
|
||||||
|
match_points += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Author check - if any author matches
|
||||||
|
if !installed.authors.is_empty() && !repo.authors.is_empty() {
|
||||||
|
for installed_author in &installed.authors {
|
||||||
|
for repo_author in &repo.authors {
|
||||||
|
if installed_author.to_lowercase() == repo_author.to_lowercase() {
|
||||||
|
match_points += 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Website match
|
||||||
|
if let Some(website) = &installed.website {
|
||||||
|
if website.contains(&repo.page_url) || repo.page_url.contains(website) {
|
||||||
|
match_points += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require at least 2 match points to consider it a likely match
|
||||||
|
// This helps avoid incorrect matches
|
||||||
|
match_points >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate similarity between plugin names (simple implementation)
|
||||||
|
fn calculate_name_similarity(name1: &str, name2: &str) -> f32 {
|
||||||
|
let name1_lower = name1.to_lowercase();
|
||||||
|
let name2_lower = name2.to_lowercase();
|
||||||
|
|
||||||
|
// Direct match
|
||||||
|
if name1_lower == name2_lower {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains check
|
||||||
|
if name1_lower.contains(&name2_lower) || name2_lower.contains(&name1_lower) {
|
||||||
|
return 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple word matching
|
||||||
|
let words1: Vec<&str> = name1_lower.split_whitespace().collect();
|
||||||
|
let words2: Vec<&str> = name2_lower.split_whitespace().collect();
|
||||||
|
|
||||||
|
let mut matched_words = 0;
|
||||||
|
let total_words = words1.len().max(words2.len());
|
||||||
|
|
||||||
|
for word1 in &words1 {
|
||||||
|
for word2 in &words2 {
|
||||||
|
if word1 == word2 && word1.len() > 2 { // Ignore short words
|
||||||
|
matched_words += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if total_words > 0 {
|
||||||
|
matched_words as f32 / total_words as f32
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user