diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs | 133 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs | 3 | ||||
-rw-r--r-- | src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs | 3 | ||||
-rw-r--r-- | src/SMAPI.Web/SMAPI.Web.csproj | 1 | ||||
-rw-r--r-- | src/SMAPI.Web/Startup.cs | 10 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.Development.json | 2 | ||||
-rw-r--r-- | src/SMAPI.Web/appsettings.json | 1 |
7 files changed, 119 insertions, 34 deletions
diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs index 87393367..753d3b4f 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusClient.cs @@ -4,8 +4,10 @@ using System.Linq; using System.Net; using System.Threading.Tasks; using HtmlAgilityPack; +using Pathoschild.FluentNexus.Models; using Pathoschild.Http.Client; using StardewModdingAPI.Toolkit; +using FluentNexusClient = Pathoschild.FluentNexus.NexusClient; namespace StardewModdingAPI.Web.Framework.Clients.Nexus { @@ -16,28 +18,34 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus ** Fields *********/ /// <summary>The URL for a Nexus mod page for the user, excluding the base URL, where {0} is the mod ID.</summary> - private readonly string ModUrlFormat; + private readonly string WebModUrlFormat; /// <summary>The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</summary> - public string ModScrapeUrlFormat { get; set; } + public string WebModScrapeUrlFormat { get; set; } - /// <summary>The underlying HTTP client.</summary> - private readonly IClient Client; + /// <summary>The underlying HTTP client for the Nexus Mods website.</summary> + private readonly IClient WebClient; + + /// <summary>The underlying HTTP client for the Nexus API.</summary> + private readonly FluentNexusClient ApiClient; /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="userAgent">The user agent for the Nexus Mods API client.</param> - /// <param name="baseUrl">The base URL for the Nexus Mods site.</param> - /// <param name="modUrlFormat">The URL for a Nexus Mods mod page for the user, excluding the <paramref name="baseUrl"/>, where {0} is the mod ID.</param> - /// <param name="modScrapeUrlFormat">The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</param> - public NexusClient(string userAgent, string baseUrl, string modUrlFormat, string modScrapeUrlFormat) + /// <param name="webUserAgent">The user agent for the Nexus Mods web client.</param> + /// <param name="webBaseUrl">The base URL for the Nexus Mods site.</param> + /// <param name="webModUrlFormat">The URL for a Nexus Mods mod page for the user, excluding the <paramref name="webBaseUrl"/>, where {0} is the mod ID.</param> + /// <param name="webModScrapeUrlFormat">The URL for a Nexus mod page to scrape for versions, excluding the base URL, where {0} is the mod ID.</param> + /// <param name="apiAppVersion">The app version to show in API user agents.</param> + /// <param name="apiKey">The Nexus API authentication key.</param> + public NexusClient(string webUserAgent, string webBaseUrl, string webModUrlFormat, string webModScrapeUrlFormat, string apiAppVersion, string apiKey) { - this.ModUrlFormat = modUrlFormat; - this.ModScrapeUrlFormat = modScrapeUrlFormat; - this.Client = new FluentClient(baseUrl).SetUserAgent(userAgent); + this.WebModUrlFormat = webModUrlFormat; + this.WebModScrapeUrlFormat = webModScrapeUrlFormat; + this.WebClient = new FluentClient(webBaseUrl).SetUserAgent(webUserAgent); + this.ApiClient = new FluentNexusClient(apiKey, "SMAPI", apiAppVersion); } /// <summary>Get metadata about a mod.</summary> @@ -45,12 +53,38 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// <returns>Returns the mod info if found, else <c>null</c>.</returns> public async Task<NexusMod> GetModAsync(uint 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); + if (mod?.Status == NexusModStatus.AdultContentForbidden) + mod = await this.GetModFromApiAsync(id); + + return mod; + } + + /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> + public void Dispose() + { + this.WebClient?.Dispose(); + } + + + /********* + ** Private methods + *********/ + /// <summary>Get metadata about a mod by scraping the Nexus website.</summary> + /// <param name="id">The Nexus mod ID.</param> + /// <returns>Returns the mod info if found, else <c>null</c>.</returns> + private async Task<NexusMod> GetModFromWebsiteAsync(uint id) + { // fetch HTML string html; try { - html = await this.Client - .GetAsync(string.Format(this.ModScrapeUrlFormat, id)) + html = await this.WebClient + .GetAsync(string.Format(this.WebModScrapeUrlFormat, id)) .AsString(); } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) @@ -74,14 +108,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus case "not found": return null; - case "hidden mod": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Hidden }; - - case "not published": - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.NotPublished }; - default: - return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = NexusModStatus.Other }; + return new NexusMod { Error = $"Nexus error: {errorCode} ({errorText}).", Status = this.GetWebStatus(errorCode) }; } } @@ -130,23 +158,68 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus }; } - /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary> - public void Dispose() + /// <summary>Get metadata about a mod from the Nexus API.</summary> + /// <param name="id">The Nexus mod ID.</param> + /// <returns>Returns the mod info if found, else <c>null</c>.</returns> + private async Task<NexusMod> GetModFromApiAsync(uint id) { - this.Client?.Dispose(); - } + // fetch mod + 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) + }; + } - /********* - ** Private methods - *********/ /// <summary>Get the full mod page URL for a given ID.</summary> /// <param name="id">The mod ID.</param> private string GetModUrl(uint id) { - UriBuilder builder = new UriBuilder(this.Client.BaseClient.BaseAddress); - builder.Path += string.Format(this.ModUrlFormat, id); + UriBuilder builder = new UriBuilder(this.WebClient.BaseClient.BaseAddress); + builder.Path += string.Format(this.WebModUrlFormat, id); return builder.Uri.ToString(); } + + /// <summary>Get the mod status for a web error code.</summary> + /// <param name="errorCode">The Nexus error code.</param> + private NexusModStatus GetWebStatus(string errorCode) + { + switch (errorCode.Trim().ToLower()) + { + case "adult content": + return NexusModStatus.AdultContentForbidden; + + case "hidden mod": + return NexusModStatus.Hidden; + + case "not published": + return NexusModStatus.NotPublished; + + default: + return NexusModStatus.Other; + } + } } } diff --git a/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs index c5723093..9ef314cd 100644 --- a/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs +++ b/src/SMAPI.Web/Framework/Clients/Nexus/NexusModStatus.cs @@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework.Clients.Nexus /// <summary>The mod hasn't been published yet.</summary> NotPublished, + /// <summary>The mod contains adult content which is hidden for anonymous web users.</summary> + AdultContentForbidden, + /// <summary>The Nexus API returned an unhandled error.</summary> Other } diff --git a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs index c27cadab..d2e9a2fe 100644 --- a/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs +++ b/src/SMAPI.Web/Framework/ConfigModels/ApiClientsConfig.cs @@ -65,6 +65,9 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels /// <summary>The URL for a Nexus mod page to scrape for versions, excluding the <see cref="NexusBaseUrl"/>, where {0} is the mod ID.</summary> public string NexusModScrapeUrlFormat { get; set; } + /// <summary>The Nexus API authentication key.</summary> + public string NexusApiKey { get; set; } + /**** ** Pastebin ****/ diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index f4e99e0c..26b29fce 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -26,6 +26,7 @@ <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" /> <PackageReference Include="MongoDB.Driver" Version="2.8.1" /> + <PackageReference Include="Pathoschild.FluentNexus" Version="0.7.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" /> </ItemGroup> <ItemGroup> diff --git a/src/SMAPI.Web/Startup.cs b/src/SMAPI.Web/Startup.cs index 66e1f00d..4d23fe65 100644 --- a/src/SMAPI.Web/Startup.cs +++ b/src/SMAPI.Web/Startup.cs @@ -136,10 +136,12 @@ namespace StardewModdingAPI.Web )); services.AddSingleton<INexusClient>(new NexusClient( - userAgent: userAgent, - baseUrl: api.NexusBaseUrl, - modUrlFormat: api.NexusModUrlFormat, - modScrapeUrlFormat: api.NexusModScrapeUrlFormat + webUserAgent: userAgent, + webBaseUrl: api.NexusBaseUrl, + webModUrlFormat: api.NexusModUrlFormat, + webModScrapeUrlFormat: api.NexusModScrapeUrlFormat, + apiAppVersion: version, + apiKey: api.NexusApiKey )); services.AddSingleton<IPastebinClient>(new PastebinClient( diff --git a/src/SMAPI.Web/appsettings.Development.json b/src/SMAPI.Web/appsettings.Development.json index 5856b96c..e6b4a1b1 100644 --- a/src/SMAPI.Web/appsettings.Development.json +++ b/src/SMAPI.Web/appsettings.Development.json @@ -20,6 +20,8 @@ "GitHubUsername": null, "GitHubPassword": null, + "NexusApiKey": null, + "PastebinUserKey": null, "PastebinDevKey": null }, diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json index 77d13924..3ea37dea 100644 --- a/src/SMAPI.Web/appsettings.json +++ b/src/SMAPI.Web/appsettings.json @@ -39,6 +39,7 @@ "ModDropApiUrl": "https://www.moddrop.com/api/mods/data", "ModDropModPageUrl": "https://www.moddrop.com/sdv/mod/{0}", + "NexusApiKey": null, // see top note "NexusBaseUrl": "https://www.nexusmods.com/stardewvalley/", "NexusModUrlFormat": "mods/{0}", "NexusModScrapeUrlFormat": "mods/{0}?tab=files", |