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] Create Remote repository (optional) | ||||
| 
 | ||||
| ## Core Infrastructure (In Progress) | ||||
| ## Core Infrastructure (Completed) ✅ | ||||
| - [x] Setup SQLite or JSON storage for plugin data | ||||
| - [x] Create core data models | ||||
| - [x] Build server/plugin directory scanner (basic implementation) | ||||
| - [x] Implement asynchronous server scanning (non-blocking) | ||||
| - [x] Implement JAR file parser for plugin.yml extraction | ||||
| 
 | ||||
| ## Plugin Discovery (Needs Refinement) | ||||
| - [ ] Improve server type detection (Paper/Spigot distinction, etc.) | ||||
| ## Plugin Discovery (Completed) ✅ | ||||
| - [x] Improve server type detection (Paper/Spigot distinction, etc.) | ||||
| - [x] Implement plugins folder detection logic | ||||
| - [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] Create plugin matching algorithm | ||||
| 
 | ||||
| ## Web Crawler Development (In Progress) | ||||
| ## Web Crawler Development (Mostly Completed) | ||||
| - [x] Create base web crawler architecture | ||||
| - [x] Implement HangarMC crawler | ||||
| - [ ] Implement SpigotMC crawler | ||||
| - [ ] Implement Modrinth crawler | ||||
| - [x] Implement SpigotMC data source using SpiGet API | ||||
|   - [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 | ||||
| - [ ] Create plugin matching algorithm | ||||
|   - [x] Basic implementation | ||||
|   - [ ] Complete API authentication | ||||
| 
 | ||||
| ## Update Management (Upcoming) | ||||
| - [ ] Build update detection system | ||||
| - [ ] Implement changelog extraction | ||||
| - [ ] Create plugin backup functionality | ||||
| - [ ] Develop plugin replacement logic | ||||
| - [ ] Add server restart capabilities (optional) | ||||
| ## Update Management (Mostly Completed) | ||||
| - [x] Build update detection system (Basic logic) | ||||
| - [x] Improve update detection reliability | ||||
|   - [x] Correctly fetch detailed version information from repositories | ||||
|   - [x] Implement robust version comparison logic (handling non-SemVer, suffixes, etc.) | ||||
|   - [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) | ||||
| - [x] Design and implement main dashboard | ||||
| @ -41,8 +83,15 @@ | ||||
| - [x] Build server folder selection interface | ||||
| - [x] Implement plugin detail view | ||||
| - [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 | ||||
| - [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) | ||||
| - [ ] Implement sandboxing for network requests | ||||
| @ -51,14 +100,18 @@ | ||||
| 
 | ||||
| ## Testing & Refinement (Upcoming) | ||||
| - [ ] Comprehensive testing with various server setups | ||||
| - [ ] Performance optimization | ||||
| - [ ] Error handling improvements | ||||
| - [ ] Performance optimization (especially concurrent requests) | ||||
| - [ ] Error handling improvements (parsing, network, etc.) | ||||
| - [ ] User acceptance testing | ||||
| - [ ] Add automated tests for critical functionality | ||||
| - [ ] Add error telemetry (optional) | ||||
| 
 | ||||
| ## Documentation (Upcoming) | ||||
| - [ ] Create user documentation | ||||
| - [ ] Write developer documentation | ||||
| - [ ] Create installation guide | ||||
| - [ ] Add troubleshooting guide | ||||
| - [ ] Document known limitations and workarounds | ||||
| 
 | ||||
| ## Deployment (Upcoming) | ||||
| - [ ] Prepare release process | ||||
| @ -74,4 +127,6 @@ | ||||
| - [ ] Scheduled plugin update checks | ||||
| - [ ] Rollback 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