diff --git a/Emerald.CoreX/Emerald.CoreX.csproj b/Emerald.CoreX/Emerald.CoreX.csproj index 367cd3d7..d875fd8b 100644 --- a/Emerald.CoreX/Emerald.CoreX.csproj +++ b/Emerald.CoreX/Emerald.CoreX.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -11,5 +11,8 @@ + + + \ No newline at end of file diff --git a/Emerald.CoreX/Store/Modrinth/IMinecraftStore.cs b/Emerald.CoreX/Store/Modrinth/IMinecraftStore.cs new file mode 100644 index 00000000..678486af --- /dev/null +++ b/Emerald.CoreX/Store/Modrinth/IMinecraftStore.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Emerald.CoreX.Store.Modrinth.JSON; + +namespace Emerald.CoreX.Store.Modrinth; + +public interface IModrinthStore +{ + Task SearchAsync(string query, int limit = 15, + SearchSortOptions sortOptions = SearchSortOptions.Relevance, string[]? categories = null); + + Task GetItemAsync(string id); + Task?> GetVersionsAsync(string id); + Task DownloadItemAsync(ItemFile file, string projectType); +} diff --git a/Emerald.CoreX/Store/Modrinth/JSON.cs b/Emerald.CoreX/Store/Modrinth/JSON.cs new file mode 100644 index 00000000..fee9bfea --- /dev/null +++ b/Emerald.CoreX/Store/Modrinth/JSON.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Emerald.CoreX.Store.Modrinth.JSON; + +public class SearchResult +{ + [JsonProperty("hits")] public List Hits { get; set; } + + [JsonProperty("offset")] public int Offset { get; set; } + + [JsonProperty("limit")] public int Limit { get; set; } + + [JsonProperty("total_hits")] public int TotalHits { get; set; } +} + +public class Category +{ + public string icon { get; set; } + public string name { get; set; } + public string project_type { get; set; } + public string header { get; set; } +} + +public class SearchHit +{ + [JsonProperty("slug")] public string Slug { get; set; } + + [JsonProperty("title")] public string Title { get; set; } + + [JsonProperty("description")] public string Description { get; set; } + + [JsonProperty("categories")] public string[] Categories { get; set; } + + [JsonProperty("client_side")] public string ClientSide { get; set; } + + [JsonProperty("server_side")] public string ServerSide { get; set; } + + [JsonProperty("project_type")] public string ProjectType { get; set; } + + [JsonProperty("downloads")] public int Downloads { get; set; } + + [JsonProperty("icon_url")] public string IconUrl { get; set; } + + [JsonProperty("project_id")] public string ProjectId { get; set; } + + [JsonProperty("author")] public string Author { get; set; } + + [JsonProperty("versions")] public string[] Versions { get; set; } + + [JsonProperty("follows")] public int Follows { get; set; } + + [JsonProperty("date_created")] public DateTime DateCreated { get; set; } + + [JsonProperty("date_modified")] public DateTime DateModified { get; set; } + + [JsonProperty("latest_version")] public string LatestVersion { get; set; } + + [JsonProperty("license")] public string License { get; set; } + + [JsonProperty("gallery")] public string[] Gallery { get; set; } +} + +public class StoreItem +{ + [JsonProperty("id")] public string ID { get; set; } + + [JsonProperty("slug")] public string Slug { get; set; } + + [JsonProperty("project_type")] public string ProjectType { get; set; } + + [JsonProperty("team")] public string Team { get; set; } + + [JsonProperty("title")] public string Title { get; set; } + + [JsonProperty("description")] public string Description { get; set; } + + [JsonProperty("body")] public string Body { get; set; } + + [JsonProperty("body_url")] public string BodyUrl { get; set; } + + [JsonProperty("published")] public DateTime PublishedDate { get; set; } + + [JsonProperty("updated")] public DateTime UpdatedDate { get; set; } + + [JsonProperty("status")] public string Status { get; set; } + + [JsonProperty("moderator_message")] public object? ModeratorMessage { get; set; } + + [JsonProperty("license")] public License License { get; set; } + + [JsonProperty("client_side")] public string ClientSide { get; set; } + + [JsonProperty("server_side")] public string ServerSide { get; set; } + + [JsonProperty("downloads")] public int Downloads { get; set; } + + [JsonProperty("followers")] public int Followers { get; set; } + + [JsonProperty("categories")] public string[] Categories { get; set; } + + [JsonProperty("versions")] public string[] Versions { get; set; } + + [JsonProperty("icon_url")] public string IconUrl { get; set; } + + [JsonProperty("issues_url")] public string IssuesUrl { get; set; } + + [JsonProperty("source_url")] public string SourceUrl { get; set; } + + [JsonProperty("wiki_url")] public object? WikiUrl { get; set; } + + [JsonProperty("discord_url")] public string DiscordUrl { get; set; } + + [JsonProperty("donation_urls")] public DonationUrls[] DonationUrls { get; set; } + + [JsonProperty("gallery")] public object[] Gallery { get; set; } +} + +public class ItemVersion : INotifyPropertyChanged +{ + public bool IsDetailsVisible { get; set; } = false; + public string? FileName => Files.FirstOrDefault(x => x.Primary)?.Filename; + + [JsonProperty("id")] public string ID { get; set; } + + [JsonProperty("project_id")] public string ProjectId { get; set; } + + [JsonProperty("author_id")] public string AuthorId { get; set; } + + [JsonProperty("featured")] public bool Featured { get; set; } + + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("version_number")] public string VersionNumber { get; set; } + + [JsonProperty("changelog")] public string Changelog { get; set; } + + [JsonProperty("changelog_url")] public string? ChangelogUrl { get; set; } + + [JsonProperty("date_published")] public DateTime DatePublished { get; set; } + + [JsonProperty("downloads")] public int Downloads { get; set; } + + [JsonProperty("version_type")] public string VersionType { get; set; } + + [JsonProperty("files")] public ItemFile[] Files { get; set; } + + [JsonProperty("dependencies")] public Dependency[] Dependencies { get; set; } + + [JsonProperty("game_versions")] public string[] GameVersions { get; set; } + + [JsonProperty("loaders")] public string[] Loaders { get; set; } + + public event PropertyChangedEventHandler? PropertyChanged; + + public void InvokePropertyChanged(string? propertyName = null) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); +} + +public class Dependency +{ + [JsonProperty("version_id")] public string VersionId { get; set; } + + [JsonProperty("project_id")] public string ProjectId { get; set; } + + [JsonProperty("file_name")] public string FileName { get; set; } + + [JsonProperty("dependency_type")] public string DependencyType { get; set; } +} + +public class ItemFile +{ + [JsonProperty("hashes")] public Hashes Hashes { get; set; } + + [JsonProperty("url")] public string Url { get; set; } + + [JsonProperty("filename")] public string Filename { get; set; } + + [JsonProperty("primary")] public bool Primary { get; set; } + + [JsonProperty("size")] public int Size { get; set; } +} + +public class Hashes +{ + [JsonProperty("sha512")] public string Sha512 { get; set; } + + [JsonProperty("sha1")] public string Sha1 { get; set; } +} + +public class License +{ + [JsonProperty("id")] public string ID { get; set; } + + [JsonProperty("name")] public string Name { get; set; } + + [JsonProperty("url")] public string Url { get; set; } +} + +public class DonationUrls +{ + [JsonProperty("id")] public string ID { get; set; } + + [JsonProperty("platform")] public string Platform { get; set; } + + [JsonProperty("url")] public string Url { get; set; } +} diff --git a/Emerald.CoreX/Store/Modrinth/ModrinthStore.cs b/Emerald.CoreX/Store/Modrinth/ModrinthStore.cs new file mode 100644 index 00000000..9f1b16e3 --- /dev/null +++ b/Emerald.CoreX/Store/Modrinth/ModrinthStore.cs @@ -0,0 +1,188 @@ +using CmlLib.Core; +using Newtonsoft.Json; +using RestSharp; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Emerald.CoreX.Store.Modrinth.JSON; + +namespace Emerald.CoreX.Store.Modrinth; + +public abstract class ModrinthStore : IMinecraftStore +{ + protected readonly RestClient _client; + public MinecraftPath MCPath { get; } + protected readonly ILogger _logger; + protected readonly string _projectType; + protected Category[] Categories; + protected ModrinthStore(MinecraftPath path, ILogger logger, string projectType) + { + _client = new RestClient("https://api.modrinth.com/v2/"); + _client.AddDefaultHeader("Accept", "application/json"); + MCPath = path; + _logger = logger; + _projectType = projectType; + } + + + protected async Task LoadCategoriesAsync() + { + var request = new RestRequest("tag/category"); + + try + { + var response = await _client.ExecuteAsync(request); + if (response.IsSuccessful) + { + var all = JsonConvert.DeserializeObject>(response.Content); + + var _categories = all + .Where(i => i.header == "categories" + && i.project_type == _projectType + && !string.IsNullOrWhiteSpace(i.icon) + && !string.IsNullOrWhiteSpace(i.name)) + .ToList(); + Categories = _categories.ToArray(); + _logger.LogInformation($"Loaded {_categories.Count} {_projectType} categories."); + } + else + { + _logger.LogError($"Failed to load {_projectType} categories. Status code: {response.StatusCode}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred while loading {_projectType} categories."); + } + } + public virtual async Task SearchAsync(string query, int limit = 15, + SearchSortOptions sortOptions = SearchSortOptions.Relevance, string[]? categories = null) + { + _logger.LogInformation($"Searching store for {_projectType}s with query: {query}"); + + try + { + string categoriesString = (categories != null && categories.Any()) + ? $"[\"categories:{string.Join("\"],[\"categories:", categories)}]\"]," + : ""; + + var request = new RestRequest("search") + .AddParameter("index", sortOptions.ToString().ToLowerInvariant()) + .AddParameter("facets", $"[{categoriesString}[\"project_type:{_projectType}\"]]") + .AddParameter("limit", limit); + + if (!string.IsNullOrEmpty(query)) + { + request.AddParameter("query", query); + } + + var response = await _client.ExecuteAsync(request); + if (response.IsSuccessful) + { + var result = JsonConvert.DeserializeObject(response.Content); + _logger.LogInformation($"Search completed successfully. Found {result.TotalHits} {_projectType}s."); + return result; + } + else + { + _logger.LogError($"API request failed: {response.ErrorMessage}"); + throw new Exception($"API request failed: {response.ErrorMessage}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred while searching for {_projectType}s"); + return null; + } + } + + public virtual async Task GetItemAsync(string id) + { + _logger.LogInformation($"Fetching {_projectType} with ID: {id}"); + + try + { + var request = new RestRequest($"project/{id}"); + var response = await _client.ExecuteAsync(request); + + if (response.IsSuccessful) + { + var item = JsonConvert.DeserializeObject(response.Content); + _logger.LogInformation($"Successfully fetched {_projectType} with ID: {id}"); + return item; + } + else + { + _logger.LogError($"API request failed: {response.ErrorMessage}"); + throw new Exception($"API request failed: {response.ErrorMessage}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred while fetching {_projectType} with ID: {id}"); + return null; + } + } + + public virtual async Task?> GetVersionsAsync(string id) + { + _logger.LogInformation($"Fetching versions for {_projectType} with ID: {id}"); + + try + { + var request = new RestRequest($"project/{id}/version"); + var response = await _client.ExecuteAsync(request); + + if (response.IsSuccessful) + { + var versions = JsonConvert.DeserializeObject>(response.Content); + _logger.LogInformation($"Successfully fetched versions for {_projectType} with ID: {id}"); + return versions; + } + else + { + _logger.LogError($"API request failed: {response.ErrorMessage}"); + throw new Exception($"API request failed: {response.ErrorMessage}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred while fetching versions for {_projectType} with ID: {id}"); + return null; + } + } + + public virtual async Task DownloadItemAsync(ItemFile file, string projectType) + { + _logger.LogInformation($"Downloading {projectType} file from URL: {file.Url}"); + + try + { + var request = new RestRequest(file.Url); + var response = await _client.ExecuteAsync(request); + + if (response.IsSuccessful) + { + var filePath = Path.Combine(MCPath.BasePath, projectType, file.Filename); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + await File.WriteAllBytesAsync(filePath, response.RawBytes); + + _logger.LogInformation($"Successfully downloaded {projectType} file to: {filePath}"); + } + else + { + _logger.LogError($"File download failed: {response.ErrorMessage}"); + throw new Exception($"File download failed: {response.ErrorMessage}"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error occurred while downloading {projectType} file from URL: {file.Url}"); + throw; + } + } +} diff --git a/Emerald.CoreX/Store/Modrinth/SearchSortOptions.cs b/Emerald.CoreX/Store/Modrinth/SearchSortOptions.cs new file mode 100644 index 00000000..416e8af9 --- /dev/null +++ b/Emerald.CoreX/Store/Modrinth/SearchSortOptions.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Emerald.CoreX.Store.Modrinth; +public enum SearchSortOptions +{ + Relevance, + Downloads, + Follows, + Updated, + Newest +} diff --git a/Emerald.CoreX/Store/Modrinth/Stores.cs b/Emerald.CoreX/Store/Modrinth/Stores.cs new file mode 100644 index 00000000..d9172302 --- /dev/null +++ b/Emerald.CoreX/Store/Modrinth/Stores.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Emerald.CoreX.Store.Modrinth.JSON; +using Microsoft.Extensions.Logging; +using CMLLib.Core; +namespace Emerald.CoreX.Store.Modrinth; + +public class ModStore : ModrinthStore +{ + public ModStore(MinecraftPath path, ILogger logger) : base(path, logger, "mod") + { + } + + public ModStore(ILogger logger) : this(new MinecraftPath(MinecraftPath.GetOSDefaultPath()), logger) + { + } + + public override async Task GetItemAsync(string id) + { + // Implement mod-specific logic if needed + return await base.GetItemAsync(id); + } + + public override async Task?> GetVersionsAsync(string id) + { + // Implement mod-specific logic if needed + return await base.GetVersionsAsync(id); + } + + public override async Task DownloadItemAsync(ItemFile file, string projectType) + { + // Implement mod-specific download logic + await base.DownloadItemAsync(file, "mods"); + } +} + +public class PluginStore : ModrinthStore +{ + public PluginStore(MinecraftPath path, ILogger logger) : base(path, logger, "plugin") + { + } + + public PluginStore(ILogger logger) : this(new MinecraftPath(MinecraftPath.GetOSDefaultPath()), logger) + { + } + + public override async Task GetItemAsync(string id) + { + return await base.GetItemAsync(id); + } + + public override async Task?> GetVersionsAsync(string id) + { + return await base.GetVersionsAsync(id); + } + + public override async Task DownloadItemAsync(ItemFile file, string projectType) + { + await base.DownloadItemAsync(file, "mods"); + } +} + +public class ResourcePackStore : ModrinthStore +{ + public ResourcePackStore(MinecraftPath path, ILogger logger) : base(path, logger, "resourcepack") + { + } + + public ResourcePackStore(ILogger logger) : this(new MinecraftPath(MinecraftPath.GetOSDefaultPath()), logger) + { + } + + public override async Task GetItemAsync(string id) + { + return await base.GetItemAsync(id); + } + + public override async Task?> GetVersionsAsync(string id) + { + return await base.GetVersionsAsync(id); + } + + public override async Task DownloadItemAsync(ItemFile file, string projectType) + { + await base.DownloadItemAsync(file, "resourcepacks"); + } +} + +public class ShaderStore : ModrinthStore +{ + public ShaderStore(MinecraftPath path, ILogger logger) : base(path, logger, "shader") + { + } + + public ShaderStore(ILogger logger) : this(new MinecraftPath(MinecraftPath.GetOSDefaultPath()), logger) + { + } + + public override async Task GetItemAsync(string id) + { + // Implement shader-specific logic if needed + return await base.GetItemAsync(id); + } + + public override async Task?> GetVersionsAsync(string id) + { + // Implement shader-specific logic if needed + return await base.GetVersionsAsync(id); + } + + public override async Task DownloadItemAsync(ItemFile file, string projectType) + { + // Implement shader-specific download logic + await base.DownloadItemAsync(file, "shaders"); + } +} + +public class ModpackStore : ModrinthStore +{ + public ModpackStore(MinecraftPath path, ILogger logger) : base(path, logger, "modpack") + { + } + + public ModpackStore(ILogger logger) : this(new MinecraftPath(MinecraftPath.GetOSDefaultPath()), logger) + { + } + + public override async Task GetItemAsync(string id) + { + // Implement modpack-specific logic if needed + return await base.GetItemAsync(id); + } + + public override async Task?> GetVersionsAsync(string id) + { + // Implement modpack-specific logic if needed + return await base.GetVersionsAsync(id); + } + + public override async Task DownloadItemAsync(ItemFile file, string projectType) + { + // Implement modpack-specific download logic + await base.DownloadItemAsync(file, "modpacks"); + } +}