From 786077340f2cea37d82455fc413535ae82a912ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 23 May 2020 21:55:11 -0400 Subject: refactor update check API This simplifies the logic for individual clients, centralises common logic, and prepares for upcoming features. --- .../Framework/Clients/Nexus/INexusClient.cs | 12 +-- .../Framework/Clients/Nexus/NexusClient.cs | 94 +++++++++++----------- src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs | 32 -------- .../Clients/Nexus/ResponseModels/NexusMod.cs | 33 ++++++++ 4 files changed, 80 insertions(+), 91 deletions(-) delete mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs create mode 100644 src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs (limited to 'src/SMAPI.Web/Framework/Clients/Nexus') diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs index e56e7af4..a44b8c66 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/INexusClient.cs @@ -1,17 +1,7 @@ using System; -using System.Threading.Tasks; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { /// An HTTP client for fetching mod metadata from Nexus Mods. - internal interface INexusClient : IDisposable - { - /********* - ** Methods - *********/ - /// Get metadata about a mod. - /// The Nexus mod ID. - /// Returns the mod info if found, else null. - Task GetModAsync(uint id); - } + internal interface INexusClient : IModSiteClient, IDisposable { } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 753d3b4f..ef3ef22e 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -7,6 +7,8 @@ using HtmlAgilityPack; using Pathoschild.FluentNexus.Models; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit; +using StardewModdingAPI.Toolkit.Framework.UpdateData; +using StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels; using FluentNexusClient = Pathoschild.FluentNexus.NexusClient; namespace StardewModdingAPI.Web.Framework.Clients.Nexus @@ -30,6 +32,13 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus private readonly FluentNexusClient ApiClient; + /********* + ** Accessors + *********/ + /// The unique key for the mod site. + public ModSiteKey SiteKey => ModSiteKey.Nexus; + + /********* ** Public methods *********/ @@ -48,20 +57,32 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion); } - /// Get metadata about a mod. - /// The Nexus mod ID. - /// Returns the mod info if found, else null. - public async Task GetModAsync(uint id) + /// Get update check info about a mod. + /// The mod ID. + public async Task GetModData(string id) { + IModPage page = new GenericModPage(this.SiteKey, id); + + if (!uint.TryParse(id, out uint parsedId)) + return page.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID."); + // Fetch from the Nexus website when possible, since it has no rate limits. Mods with // adult content are hidden for anonymous users, so fall back to the API in that case. // Note that the API has very restrictive rate limits which means we can't just use it // for all cases. - NexusMod mod = await this.GetModFromWebsiteAsync(id); + NexusMod mod = await this.GetModFromWebsiteAsync(parsedId); if (mod?.Status == NexusModStatus.AdultContentForbidden) - mod = await this.GetModFromApiAsync(id); + mod = await this.GetModFromApiAsync(parsedId); + + // page doesn't exist + if (mod == null || mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished) + return page.SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID."); - return mod; + // return info + page.SetInfo(name: mod.Name, url: mod.Url, version: mod.Version, downloads: mod.Downloads); + if (mod.Status != NexusModStatus.Ok) + page.SetError(RemoteModStatus.TemporaryError, mod.Error); + return page; } /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. @@ -115,37 +136,28 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus // extract mod info string url = this.GetModUrl(id); - string name = doc.DocumentNode.SelectSingleNode("//h1")?.InnerText.Trim(); + string name = doc.DocumentNode.SelectSingleNode("//div[@id='pagetitle']//h1")?.InnerText.Trim(); string version = doc.DocumentNode.SelectSingleNode("//ul[contains(@class, 'stats')]//li[@class='stat-version']//div[@class='stat']")?.InnerText.Trim(); SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion); - // extract file versions - List rawVersions = new List(); + // extract files + var downloads = new List(); foreach (var fileSection in doc.DocumentNode.SelectNodes("//div[contains(@class, 'files-tabs')]")) { string sectionName = fileSection.Descendants("h2").First().InnerText; if (sectionName != "Main files" && sectionName != "Optional files") continue; - rawVersions.AddRange( - from statBox in fileSection.Descendants().Where(p => p.HasClass("stat-version")) - from versionStat in statBox.Descendants().Where(p => p.HasClass("stat")) - select versionStat.InnerText.Trim() - ); - } - - // choose latest file version - ISemanticVersion latestFileVersion = null; - foreach (string rawVersion in rawVersions) - { - if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) - continue; - if (parsedVersion != null && !cur.IsNewerThan(parsedVersion)) - continue; - if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) - continue; + foreach (var container in fileSection.Descendants("dt")) + { + string fileName = container.GetDataAttribute("name").Value; + string fileVersion = container.GetDataAttribute("version").Value; + string description = container.SelectSingleNode("following-sibling::*[1][self::dd]//div").InnerText?.Trim(); // get text of next
tag; derived from https://stackoverflow.com/a/25535623/262123 - latestFileVersion = cur; + downloads.Add( + new GenericModDownload(fileName, description, fileVersion) + ); + } } // yield info @@ -153,8 +165,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus { Name = name, Version = parsedVersion?.ToString() ?? version, - LatestFileVersion = latestFileVersion, - Url = url + Url = url, + Downloads = downloads.ToArray() }; } @@ -167,29 +179,15 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus Mod mod = await this.ApiClient.Mods.GetMod("stardewvalley", (int)id); ModFileList files = await this.ApiClient.ModFiles.GetModFiles("stardewvalley", (int)id, FileCategory.Main, FileCategory.Optional); - // get versions - if (!SemanticVersion.TryParse(mod.Version, out ISemanticVersion mainVersion)) - mainVersion = null; - ISemanticVersion latestFileVersion = null; - foreach (string rawVersion in files.Files.Select(p => p.FileVersion)) - { - if (!SemanticVersion.TryParse(rawVersion, out ISemanticVersion cur)) - continue; - if (mainVersion != null && !cur.IsNewerThan(mainVersion)) - continue; - if (latestFileVersion != null && !cur.IsNewerThan(latestFileVersion)) - continue; - - latestFileVersion = cur; - } - // yield info return new NexusMod { Name = mod.Name, Version = SemanticVersion.TryParse(mod.Version, out ISemanticVersion version) ? version?.ToString() : mod.Version, - LatestFileVersion = latestFileVersion, - Url = this.GetModUrl(id) + Url = this.GetModUrl(id), + Downloads = files.Files + .Select(file => (IModDownload)new GenericModDownload(file.Name, null, file.FileVersion)) + .ToArray() }; } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs deleted file mode 100644 index 0f1b29d5..00000000 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusMod.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; - -namespace StardewModdingAPI.Web.Framework.Clients.Nexus -{ - /// Mod metadata from Nexus Mods. - internal class NexusMod - { - /********* - ** Accessors - *********/ - /// The mod name. - public string Name { get; set; } - - /// The mod's semantic version number. - public string Version { get; set; } - - /// The latest file version. - public ISemanticVersion LatestFileVersion { get; set; } - - /// The mod's web URL. - [JsonProperty("mod_page_uri")] - public string Url { get; set; } - - /// The mod's publication status. - [JsonIgnore] - public NexusModStatus Status { get; set; } = NexusModStatus.Ok; - - /// A user-friendly error which indicates why fetching the mod info failed (if applicable). - [JsonIgnore] - public string Error { get; set; } - } -} diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs new file mode 100644 index 00000000..aef90ede --- /dev/null +++ b/src/SMAPI.Web/Framework/Clients/Nexus/ResponseModels/NexusMod.cs @@ -0,0 +1,33 @@ +using Newtonsoft.Json; + +namespace StardewModdingAPI.Web.Framework.Clients.Nexus.ResponseModels +{ + /// Mod metadata from Nexus Mods. + internal class NexusMod + { + /********* + ** Accessors + *********/ + /// The mod name. + public string Name { get; set; } + + /// The mod's semantic version number. + public string Version { get; set; } + + /// The mod's web URL. + [JsonProperty("mod_page_uri")] + public string Url { get; set; } + + /// The mod's publication status. + [JsonIgnore] + public NexusModStatus Status { get; set; } = NexusModStatus.Ok; + + /// The files available to download. + [JsonIgnore] + public IModDownload[] Downloads { get; set; } + + /// A custom user-friendly error which indicates why fetching the mod info failed (if applicable). + [JsonIgnore] + public string Error { get; set; } + } +} -- cgit