summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs2
-rw-r--r--src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs47
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs6
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs27
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs19
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs3
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs18
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs37
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs40
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs32
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs80
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs28
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs13
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs18
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs37
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs72
17 files changed, 293 insertions, 216 deletions
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
index d1dd9049..195b0367 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/ModSiteKey.cs
@@ -21,7 +21,7 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
/// <summary>The Nexus Mods mod repository.</summary>
Nexus,
- /// <summary>An arbitrary URL for an update manifest file.</summary>
+ /// <summary>An arbitrary URL to a JSON file containing update data.</summary>
UpdateManifest
}
}
diff --git a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
index 7c2cc73c..3e8064fd 100644
--- a/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
+++ b/src/SMAPI.Toolkit/Framework/UpdateData/UpdateKey.cs
@@ -54,29 +54,6 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
public UpdateKey(ModSiteKey site, string? id, string? subkey)
: this(UpdateKey.GetString(site, id, subkey), site, id, subkey) { }
- /// <summary>
- /// Split a string into two at a delimiter. If the delimiter does not appear in the string then the second
- /// value of the returned tuple is null. Both returned strings are trimmed of whitespace.
- /// </summary>
- /// <param name="s">The string to split.</param>
- /// <param name="delimiter">The character on which to split.</param>
- /// <param name="keepDelimiter">
- /// If <c>true</c> then the second string returned will include the delimiter character
- /// (provided that the string is not <c>null</c>)
- /// </param>
- /// <returns>
- /// A pair containing the string consisting of all characters in <paramref name="s"/> before the first
- /// occurrence of <paramref name="delimiter"/>, and a string consisting of all characters in <paramref name="s"/>
- /// after the first occurrence of <paramref name="delimiter"/> or <c>null</c> if the delimiter does not
- /// exist in s. Both strings are trimmed of whitespace.
- /// </returns>
- private static (string, string?) Bifurcate(string s, char delimiter, bool keepDelimiter = false) {
- int pos = s.IndexOf(delimiter);
- if (pos < 0)
- return (s.Trim(), null);
- return (s.Substring(0, pos).Trim(), s.Substring(pos + (keepDelimiter ? 0 : 1)).Trim());
- }
-
/// <summary>Parse a raw update key.</summary>
/// <param name="raw">The raw update key to parse.</param>
public static UpdateKey Parse(string? raw)
@@ -84,14 +61,14 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
if (raw is null)
return new UpdateKey(raw, ModSiteKey.Unknown, null, null);
// extract site + ID
- (string rawSite, string? id) = Bifurcate(raw, ':');
- if (string.IsNullOrWhiteSpace(id))
+ (string rawSite, string? id) = UpdateKey.SplitTwoParts(raw, ':');
+ if (string.IsNullOrEmpty(id))
id = null;
// extract subkey
string? subkey = null;
if (id != null)
- (id, subkey) = Bifurcate(id, '@', true);
+ (id, subkey) = UpdateKey.SplitTwoParts(id, '@', true);
// parse
if (!Enum.TryParse(rawSite, true, out ModSiteKey site))
@@ -160,5 +137,23 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
{
return $"{site}:{id}{subkey}".Trim();
}
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Split a string into two parts at a delimiter and trim whitespace.</summary>
+ /// <param name="str">The string to split.</param>
+ /// <param name="delimiter">The character on which to split.</param>
+ /// <param name="keepDelimiter">Whether to include the delimiter in the second string.</param>
+ /// <returns>Returns a tuple containing the two strings, with the second value <c>null</c> if the delimiter wasn't found.</returns>
+ private static (string, string?) SplitTwoParts(string str, char delimiter, bool keepDelimiter = false)
+ {
+ int splitIndex = str.IndexOf(delimiter);
+
+ return splitIndex >= 0
+ ? (str.Substring(0, splitIndex).Trim(), str.Substring(splitIndex + (keepDelimiter ? 0 : 1)).Trim())
+ : (str.Trim(), null);
+ }
}
}
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 1c34f2af..5fc4987d 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -64,7 +64,7 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- /// <param name="updateManifest">The UpdateManifest client.</param>
+ /// <param name="updateManifest">The API client for arbitrary update manifest URLs.</param>
public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus, IUpdateManifestClient updateManifest)
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
@@ -163,9 +163,9 @@ namespace StardewModdingAPI.Web.Controllers
// if there's only a prerelease version (e.g. from GitHub), don't override the main version
ISemanticVersion? curMain = data.Version;
- string? curMainUrl = data.MainVersionUrl;
ISemanticVersion? curPreview = data.PreviewVersion;
- string? curPreviewUrl = data.PreviewVersionUrl;
+ string? curMainUrl = data.MainModPageUrl;
+ string? curPreviewUrl = data.PreviewModPageUrl;
if (curPreview == null && curMain?.IsPrerelease() == true)
{
curPreview = curMain;
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
index 5cc03aba..6c9c08ef 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
@@ -17,10 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <summary>The download's file version.</summary>
public string? Version { get; }
- /// <summary>
- /// The URL for this download, if it has one distinct from the mod page's URL.
- /// </summary>
- public string? Url { get; }
+ /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary>
+ public string? ModPageUrl { get; }
+
/*********
** Public methods
@@ -29,23 +28,21 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <param name="name">The download's display name.</param>
/// <param name="description">The download's description.</param>
/// <param name="version">The download's file version.</param>
- /// <param name="url">The download's URL (if different from the mod page's URL).</param>
- public GenericModDownload(string name, string? description, string? version, string? url = null)
+ /// <param name="modPageUrl">The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</param>
+ public GenericModDownload(string name, string? description, string? version, string? modPageUrl = null)
{
this.Name = name;
this.Description = description;
this.Version = version;
- this.Url = url;
+ this.ModPageUrl = modPageUrl;
}
- /// <summary>
- /// Return <see langword="true"/> if the subkey matches this download. A subkey matches if it appears as
- /// a substring in the name or description.
- /// </summary>
- /// <param name="subkey">the subkey</param>
- /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns>
- public virtual bool MatchesSubkey(string subkey) {
- return this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
+ public virtual bool MatchesSubkey(string subkey)
+ {
+ return
+ this.Name.Contains(subkey, StringComparison.OrdinalIgnoreCase)
|| this.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true;
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
index 4c66e1a0..e939f1d8 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
@@ -40,9 +40,10 @@ namespace StardewModdingAPI.Web.Framework.Clients
[MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
public bool IsValid => this.Status == RemoteModStatus.Ok;
- /// <summary>Whether to use strict subkey matching or not.</summary>
+ /// <summary>Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without.</summary>
public bool IsSubkeyStrict { get; set; } = false;
+
/*********
** Public methods
*********/
@@ -82,17 +83,17 @@ namespace StardewModdingAPI.Web.Framework.Clients
return this;
}
- /// <summary>Returns the mod page name.</summary>
- /// <param name="subkey">ignored</param>
- /// <returns>The mod page name.</returns>
- public virtual string? GetName(string? subkey) {
+ /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary>
+ /// <param name="subkey">The update subkey.</param>
+ public virtual string? GetName(string? subkey)
+ {
return this.Name;
}
- /// <summary>Returns the mod page URL.</summary>
- /// <param name="subkey">ignored</param>
- /// <returns>The mod page URL.</returns>
- public virtual string? GetUrl(string? subkey) {
+ /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary>
+ /// <param name="subkey">The update subkey.</param>
+ public virtual string? GetUrl(string? subkey)
+ {
return this.Url;
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
index 36d018c7..dd9f5811 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
@@ -3,7 +3,6 @@ using System;
namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
{
- /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary>
+ /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary>
internal interface IUpdateManifestClient : IModSiteClient, IDisposable { }
}
-
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs
index 48e3c294..02722cb1 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/TextAsJsonMediaTypeFormatter.cs
@@ -1,14 +1,18 @@
-// Copyright 2022 Jamie Taylor
+// Copyright 2022 Jamie Taylor
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
- /// <summary>
- /// A <see cref="JsonMediaTypeFormatter"/> that can parse from content of type <c>text/plain</c>.
- /// </summary>
- internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>A <see cref="JsonMediaTypeFormatter"/> that can parse from content of type <c>text/plain</c>.</summary>
+ internal class TextAsJsonMediaTypeFormatter : JsonMediaTypeFormatter
+ {
+ /*********
+ ** Public methods
+ *********/
/// <summary>Construct a new <see cref="JsonMediaTypeFormatter"/></summary>
- public TextAsJsonMediaTypeFormatter() {
+ public TextAsJsonMediaTypeFormatter()
+ {
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
index d7cf4945..88a5c2f6 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
@@ -1,19 +1,21 @@
// Copyright 2022 Jamie Taylor
-using System;
+using System.Net;
+using System.Threading.Tasks;
using Pathoschild.Http.Client;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
-using System.Threading.Tasks;
-using System.Net;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
- /// <summary>An HTTP client for fetching an update manifest from an arbitrary URL.</summary>
- internal class UpdateManifestClient : IUpdateManifestClient {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>An API client for fetching update metadata from an arbitrary JSON URL.</summary>
+ internal class UpdateManifestClient : IUpdateManifestClient
+ {
/*********
** Fields
*********/
/// <summary>The underlying HTTP client.</summary>
private readonly IClient Client;
+
/*********
** Accessors
*********/
@@ -26,30 +28,35 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
*********/
/// <summary>Construct an instance.</summary>
/// <param name="userAgent">The user agent for the API client.</param>
- public UpdateManifestClient(string userAgent) {
+ public UpdateManifestClient(string userAgent)
+ {
this.Client = new FluentClient()
.SetUserAgent(userAgent);
this.Client.Formatters.Add(new TextAsJsonMediaTypeFormatter());
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- public void Dispose() {
+ public void Dispose()
+ {
this.Client.Dispose();
}
/// <inheritdoc/>
- public async Task<IModPage?> GetModData(string id) {
+ public async Task<IModPage?> GetModData(string id)
+ {
UpdateManifestModel? manifest;
- try {
+ try
+ {
manifest = await this.Client.GetAsync(id).As<UpdateManifestModel?>();
- } catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) {
- return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}");
}
- if (manifest is null) {
- return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"Error parsing manifest at {id}");
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}");
}
- return new UpdateManifestModPage(id, manifest);
+ return manifest is not null
+ ? new UpdateManifestModPage(id, manifest)
+ : new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"The update manifest at {id} has an invalid format");
}
}
}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
index 117ae15c..0a6d4736 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
@@ -1,27 +1,35 @@
// Copyright 2022 Jamie Taylor
-using System;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
/// <summary>Metadata about a mod download in an update manifest file.</summary>
- internal class UpdateManifestModDownload : GenericModDownload {
- /// <summary>The subkey for this mod download</summary>
- private readonly string subkey;
+ internal class UpdateManifestModDownload : GenericModDownload
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The update subkey for this mod download.</summary>
+ private readonly string Subkey;
+
+
+ /*********
+ ** Public methods
+ *********/
/// <summary>Construct an instance.</summary>
- /// <param name="subkey">The subkey for this download.</param>
+ /// <param name="fieldName">The field name for this mod download in the manifest.</param>
/// <param name="name">The mod name for this download.</param>
/// <param name="version">The download's version.</param>
/// <param name="url">The download's URL.</param>
- public UpdateManifestModDownload(string subkey, string name, string? version, string? url) : base(name, null, version, url) {
- this.subkey = subkey;
+ public UpdateManifestModDownload(string fieldName, string name, string? version, string? url)
+ : base(name, null, version, url)
+ {
+ this.Subkey = fieldName;
}
- /// <summary>
- /// Returns <see langword="true"/> iff the given subkey is the same as the subkey for this download.
- /// </summary>
- /// <param name="subkey">The subkey to match</param>
- /// <returns><see langword="true"/> if <paramref name="subkey"/> is the same as the subkey for this download, <see langword="false"/> otherwise.</returns>
- public override bool MatchesSubkey(string subkey) {
- return this.subkey == subkey;
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
+ public override bool MatchesSubkey(string subkey)
+ {
+ return subkey == this.Subkey;
}
}
}
-
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs
index 4ec9c03d..92642321 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModModel.cs
@@ -1,26 +1,34 @@
// Copyright 2022 Jamie Taylor
-using System;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
- /// <summary>Data model for a mod in an update manifest.</summary>
- internal class UpdateManifestModModel {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>The data model for a mod in an update manifest file.</summary>
+ internal class UpdateManifestModModel
+ {
+ /*********
+ ** Accessors
+ *********/
/// <summary>The mod's name.</summary>
- public string Name { get; }
+ public string? Name { get; }
- /// <summary>The mod's URL.</summary>
+ /// <summary>The mod page URL from which to download updates.</summary>
public string? Url { get; }
- /// <summary>The versions for this mod.</summary>
- public UpdateManifestVersionModel[] Versions { get; }
+ /// <summary>The available versions for this mod.</summary>
+ public UpdateManifestVersionModel[]? Versions { get; }
+
+ /*********
+ ** Public methods
+ *********/
/// <summary>Construct an instance.</summary>
/// <param name="name">The mod's name.</param>
- /// <param name="url">The mod's URL.</param>
- /// <param name="versions">The versions for this mod.</param>
- public UpdateManifestModModel(string name, string? url, UpdateManifestVersionModel[] versions) {
+ /// <param name="url">The mod page URL from which to download updates.</param>
+ /// <param name="versions">The available versions for this mod.</param>
+ public UpdateManifestModModel(string? name, string? url, UpdateManifestVersionModel[]? versions)
+ {
this.Name = name;
this.Url = url;
this.Versions = versions;
}
}
}
-
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
index 109175b5..bbc7b5da 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
@@ -1,58 +1,74 @@
-// Copyright 2022 Jamie Taylor
-using System;
+// Copyright 2022 Jamie Taylor
using System.Collections.Generic;
using System.Linq;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
/// <summary>Metadata about an update manifest "page".</summary>
- internal class UpdateManifestModPage : GenericModPage {
- /// <summary>The update manifest model.</summary>
- private UpdateManifestModel manifest;
-
- /// <summary>Constuct an instance.</summary>
- /// <param name="id">The "id" (i.e., URL) of this update manifest.</param>
- /// <param name="manifest">The manifest object model.</param>
- public UpdateManifestModPage(string id, UpdateManifestModel manifest) : base(ModSiteKey.UpdateManifest, id) {
+ internal class UpdateManifestModPage : GenericModPage
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The mods from the update manifest.</summary>
+ private readonly IDictionary<string, UpdateManifestModModel> Mods;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="url">The URL of the update manifest file.</param>
+ /// <param name="manifest">The parsed update manifest.</param>
+ public UpdateManifestModPage(string url, UpdateManifestModel manifest)
+ : base(ModSiteKey.UpdateManifest, url)
+ {
this.IsSubkeyStrict = true;
- this.manifest = manifest;
- this.SetInfo(name: id, url: id, version: null, downloads: TranslateDownloads(manifest).ToArray());
+ this.Mods = manifest.Mods ?? new Dictionary<string, UpdateManifestModModel>();
+ this.SetInfo(name: url, url: url, version: null, downloads: this.ParseDownloads(manifest.Mods).ToArray());
}
/// <summary>Return the mod name for the given subkey, if it exists in this update manifest.</summary>
/// <param name="subkey">The subkey.</param>
/// <returns>The mod name for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
- public override string? GetName(string? subkey) {
- if (subkey is null)
- return null;
- this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel);
- return modModel?.Name;
+ public override string? GetName(string? subkey)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel)
+ ? modModel.Name
+ : null;
}
/// <summary>Return the mod URL for the given subkey, if it exists in this update manifest.</summary>
/// <param name="subkey">The subkey.</param>
/// <returns>The mod URL for the given subkey, or <see langword="null"/> if this manifest does not contain the given subkey.</returns>
- public override string? GetUrl(string? subkey) {
- if (subkey is null)
- return null;
- this.manifest.Subkeys.TryGetValue(subkey, out UpdateManifestModModel? modModel);
- return modModel?.Url;
+ public override string? GetUrl(string? subkey)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey, out UpdateManifestModModel? modModel)
+ ? modModel.Url
+ : null;
}
/*********
** Private methods
*********/
- /// <summary>Translate the downloads from the manifest's object model into <see cref="IModDownload"/> objects.</summary>
- /// <param name="manifest">The manifest object model.</param>
- /// <returns>An <see cref="IModDownload"/> for each <see cref="UpdateManifestVersionModel"/> in the manifest.</returns>
- private static IEnumerable<IModDownload> TranslateDownloads(UpdateManifestModel manifest) {
- foreach (var entry in manifest.Subkeys) {
- foreach (var version in entry.Value.Versions) {
- yield return new UpdateManifestModDownload(entry.Key, entry.Value.Name, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl);
- }
+ /// <summary>Convert the raw download info from an update manifest to <see cref="IModDownload"/>.</summary>
+ /// <param name="mods">The mods from the update manifest.</param>
+ private IEnumerable<IModDownload> ParseDownloads(IDictionary<string, UpdateManifestModModel>? mods)
+ {
+ if (mods is null)
+ yield break;
+
+ foreach ((string modKey, UpdateManifestModModel mod) in mods)
+ {
+ if (mod.Versions is null)
+ continue;
+
+ foreach (UpdateManifestVersionModel version in mod.Versions)
+ yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.DownloadFileUrl ?? version.DownloadPageUrl);
}
}
}
-} \ No newline at end of file
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs
index 03f89726..ad618022 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModel.cs
@@ -1,23 +1,31 @@
// Copyright 2022 Jamie Taylor
-using System;
using System.Collections.Generic;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
- /// <summary>Data model for an update manifest.</summary>
- internal class UpdateManifestModel {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>The data model for an update manifest file.</summary>
+ internal class UpdateManifestModel
+ {
+ /*********
+ ** Accessors
+ *********/
/// <summary>The manifest format version.</summary>
- public string ManifestVersion { get; }
+ public string? ManifestVersion { get; }
- /// <summary>The subkeys in this update manifest.</summary>
- public IDictionary<string, UpdateManifestModModel> Subkeys { get; }
+ /// <summary>The mod info in this update manifest.</summary>
+ public IDictionary<string, UpdateManifestModModel>? Mods { get; }
+
+ /*********
+ ** Public methods
+ *********/
/// <summary>Construct an instance.</summary>
/// <param name="manifestVersion">The manifest format version.</param>
- /// <param name="subkeys">The subkeys in this update manifest.</param>
- public UpdateManifestModel(string manifestVersion, IDictionary<string, UpdateManifestModModel> subkeys) {
+ /// <param name="mods">The mod info in this update manifest.</param>
+ public UpdateManifestModel(string manifestVersion, IDictionary<string, UpdateManifestModModel> mods)
+ {
this.ManifestVersion = manifestVersion;
- this.Subkeys = subkeys;
+ this.Mods = mods;
}
}
}
-
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs
index 55b6db61..90e054d8 100644
--- a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestVersionModel.cs
@@ -1,10 +1,14 @@
// Copyright 2022 Jamie Taylor
-using System;
-namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
/// <summary>Data model for a Version in an update manifest.</summary>
- internal class UpdateManifestVersionModel {
- /// <summary>The semantic version string.</summary>
- public string Version { get; }
+ internal class UpdateManifestVersionModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's semantic version.</summary>
+ public string? Version { get; }
/// <summary>The URL for this version's download page (if any).</summary>
public string? DownloadPageUrl { get; }
@@ -12,15 +16,19 @@ namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest {
/// <summary>The URL for this version's direct file download (if any).</summary>
public string? DownloadFileUrl { get; }
+
+ /*********
+ ** Public methods
+ *********/
/// <summary>Construct an instance.</summary>
- /// <param name="version">The semantic version string.</param>
- /// <param name="downloadPageUrl">This version's download page URL (if any).</param>
- /// <param name="downloadFileUrl">This version's direct file download URL (if any).</param>
- public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl) {
+ /// <param name="version">The mod's semantic version.</param>
+ /// <param name="downloadPageUrl">This version's download page URL, if any.</param>
+ /// <param name="downloadFileUrl">This version's direct file download URL, if any.</param>
+ public UpdateManifestVersionModel(string version, string? downloadPageUrl, string? downloadFileUrl)
+ {
this.Version = version;
this.DownloadPageUrl = downloadPageUrl;
this.DownloadFileUrl = downloadFileUrl;
}
}
}
-
diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs
index b0e9a664..8cb82989 100644
--- a/src/SMAPI.Web/Framework/IModDownload.cs
+++ b/src/SMAPI.Web/Framework/IModDownload.cs
@@ -15,12 +15,15 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The download's file version.</summary>
string? Version { get; }
- /// <summary>This download's URL (if it has a URL that is different from the containing mod page's URL).</summary>
- string? Url { get; }
+ /// <summary>The mod URL page from which to download this update, if different from the URL of the mod page it was fetched from.</summary>
+ string? ModPageUrl { get; }
- /// <summary>Return <see langword="true"/> iff the subkey matches this download</summary>
- /// <param name="subkey">the subkey</param>
- /// <returns><see langword="true"/> if <paramref name="subkey"/> matches this download, otherwise <see langword="false"/></returns>
+
+ /*********
+ ** Methods
+ *********/
+ /// <summary>Get whether the subkey matches this download.</summary>
+ /// <param name="subkey">The update subkey to check.</param>
bool MatchesSubkey(string subkey);
}
}
diff --git a/src/SMAPI.Web/Framework/IModPage.cs b/src/SMAPI.Web/Framework/IModPage.cs
index 3fc8bb81..ef1513eb 100644
--- a/src/SMAPI.Web/Framework/IModPage.cs
+++ b/src/SMAPI.Web/Framework/IModPage.cs
@@ -39,25 +39,19 @@ namespace StardewModdingAPI.Web.Framework
[MemberNotNullWhen(false, nameof(IModPage.Error))]
bool IsValid { get; }
- /// <summary>
- /// Does this page use strict subkey matching. Pages that use string subkey matching do not fall back
- /// to searching for versions without a subkey if there are no versions found when given a subkey.
- /// Additionally, the leading <c>@</c> is stripped from the subkey value before searching for matches.
- /// </summary>
+ /// <summary>Whether this mod page requires string subkey matching, in which case a subkey that isn't found will return no update instead of falling back to one without. Additionally, the leading <c>@</c> is stripped from the subkey value before searching for matches.</summary>
bool IsSubkeyStrict { get; }
+
/*********
** Methods
*********/
-
- /// <summary>Get the mod name associated with the given subkey, if any.</summary>
- /// <param name="subkey">The subkey.</param>
- /// <returns>The mod name associated with the given subkey (if any)</returns>
+ /// <summary>Get the mod name for an update subkey, if different from the mod page name.</summary>
+ /// <param name="subkey">The update subkey.</param>
string? GetName(string? subkey);
- /// <summary>Get the URL for the mod associated with the given subkey, if any.</summary>
- /// <param name="subkey">The subkey.</param>
- /// <returns>The URL for the mod associated with the given subkey (if any)</returns>
+ /// <summary>Get the mod page URL for an update subkey, if different from the mod page it was fetched from.</summary>
+ /// <param name="subkey">The update subkey.</param>
string? GetUrl(string? subkey);
/// <summary>Set the fetched mod info.</summary>
diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs
index 17415589..502c0827 100644
--- a/src/SMAPI.Web/Framework/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModInfoModel.cs
@@ -27,11 +27,12 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The error message indicating why the mod is invalid (if applicable).</summary>
public string? Error { get; private set; }
- /// <summary>The URL associated with the mod's latest version (if distinct from the mod page's URL).</summary>
- public string? MainVersionUrl { get; private set; }
+ /// <summary>The mod page URL from which <see cref="Version"/> can be downloaded, if different from the <see cref="Url"/>.</summary>
+ public string? MainModPageUrl { get; private set; }
+
+ /// <summary>The mod page URL from which <see cref="PreviewVersion"/> can be downloaded, if different from the <see cref="Url"/>.</summary>
+ public string? PreviewModPageUrl { get; private set; }
- /// <summary>The URL associated with the mod's <see cref="PreviewVersion"/> (if distinct from the mod page's URL).</summary>
- public string? PreviewVersionUrl { get; private set; }
/*********
** Public methods
@@ -51,7 +52,8 @@ namespace StardewModdingAPI.Web.Framework
{
this
.SetBasicInfo(name, url)
- .SetVersions(version!, previewVersion)
+ .SetMainVersion(version!)
+ .SetPreviewVersion(previewVersion)
.SetError(status, error!);
}
@@ -67,18 +69,25 @@ namespace StardewModdingAPI.Web.Framework
return this;
}
- /// <summary>Set the mod version info.</summary>
- /// <param name="version">The semantic version for the mod's latest release.</param>
- /// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
- /// <param name="mainVersionUrl">The URL associated with <paramref name="version"/>, if different from the mod page's URL.</param>
- /// <param name="previewVersionUrl">The URL associated with <paramref name="previewVersion"/>, if different from the mod page's URL.</param>
+ /// <summary>Set the mod's main version info.</summary>
+ /// <param name="version">The semantic version for the mod's latest stable release.</param>
+ /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param>
[MemberNotNull(nameof(ModInfoModel.Version))]
- public ModInfoModel SetVersions(ISemanticVersion version, ISemanticVersion? previewVersion = null, string? mainVersionUrl = null, string? previewVersionUrl = null)
+ public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null)
{
this.Version = version;
- this.PreviewVersion = previewVersion;
- this.MainVersionUrl = mainVersionUrl;
- this.PreviewVersionUrl = previewVersionUrl;
+ this.MainModPageUrl = modPageUrl;
+
+ return this;
+ }
+
+ /// <summary>Set the mod's preview version info.</summary>
+ /// <param name="version">The semantic version for the mod's latest preview release.</param>
+ /// <param name="modPageUrl">The mod page URL from which <paramref name="version"/> can be downloaded, if different from the <see cref="Url"/>.</param>
+ public ModInfoModel SetPreviewVersion(ISemanticVersion? version, string? modPageUrl = null)
+ {
+ this.PreviewVersion = version;
+ this.PreviewModPageUrl = modPageUrl;
return this;
}
diff --git a/src/SMAPI.Web/Framework/ModSiteManager.cs b/src/SMAPI.Web/Framework/ModSiteManager.cs
index 5581d799..350159a3 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -66,25 +66,34 @@ namespace StardewModdingAPI.Web.Framework
{
// get base model
ModInfoModel model = new();
- if (!page.IsValid) {
+ if (!page.IsValid)
+ {
model.SetError(page.Status, page.Error);
return model;
}
- if (page.IsSubkeyStrict && subkey is not null) {
- if (subkey.Length > 0 && subkey[0] == '@') {
+ // trim subkey in strict mode
+ if (page.IsSubkeyStrict && subkey is not null)
+ {
+ if (subkey.StartsWith('@'))
subkey = subkey.Substring(1);
- }
}
// fetch versions
- bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl);
+ bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainModPageUrl, out string? previewModPageUrl);
if (!hasVersions)
return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}{subkey ?? ""}' has no valid versions.");
- model.SetBasicInfo(page.GetName(subkey) ?? page.Name, page.GetUrl(subkey) ?? page.Url);
+ // apply mod page info
+ model.SetBasicInfo(
+ name: page.GetName(subkey) ?? page.Name,
+ url: page.GetUrl(subkey) ?? page.Url
+ );
+
// return info
- return model.SetVersions(mainVersion!, previewVersion, mainVersionUrl, previewVersionUrl);
+ return model
+ .SetMainVersion(mainVersion!, mainModPageUrl)
+ .SetPreviewVersion(previewVersion, previewModPageUrl);
}
/// <summary>Get a semantic local version for update checks.</summary>
@@ -115,12 +124,14 @@ namespace StardewModdingAPI.Web.Framework
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="main">The main mod version.</param>
/// <param name="preview">The latest prerelease version, if newer than <paramref name="main"/>.</param>
- private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainVersionUrl, out string? previewVersionUrl)
+ /// <param name="mainModPageUrl">The mod page URL from which <paramref name="main"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param>
+ /// <param name="previewModPageUrl">The mod page URL from which <paramref name="preview"/> can be downloaded, if different from the <see cref="mod"/>'s URL.</param>
+ private bool TryGetLatestVersions(IModPage? mod, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions, [NotNullWhen(true)] out ISemanticVersion? main, out ISemanticVersion? preview, out string? mainModPageUrl, out string? previewModPageUrl)
{
main = null;
preview = null;
- mainVersionUrl = null;
- previewVersionUrl = null;
+ mainModPageUrl = null;
+ previewModPageUrl = null;
// parse all versions from the mod page
IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions()
@@ -152,12 +163,12 @@ namespace StardewModdingAPI.Web.Framework
.ToArray();
// get main + preview versions
- void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainVersionUrl, out string? previewVersionUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null)
+ void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, out string? mainUrl, out string? previewUrl, Func<(IModDownload? download, ISemanticVersion? version), bool>? filter = null)
{
mainVersion = null;
previewVersion = null;
- mainVersionUrl = null;
- previewVersionUrl = null;
+ mainUrl = null;
+ previewUrl = null;
// get latest main + preview version
foreach ((IModDownload? download, ISemanticVersion? version) entry in versions)
@@ -165,14 +176,18 @@ namespace StardewModdingAPI.Web.Framework
if (entry.version is null || filter?.Invoke(entry) == false)
continue;
- if (entry.version.IsPrerelease()) {
- if (previewVersion is null) {
+ if (entry.version.IsPrerelease())
+ {
+ if (previewVersion is null)
+ {
previewVersion = entry.version;
- previewVersionUrl = entry.download?.Url;
+ previewUrl = entry.download?.ModPageUrl;
}
- } else {
+ }
+ else
+ {
mainVersion = entry.version;
- mainVersionUrl = entry.download?.Url;
+ mainUrl = entry.download?.ModPageUrl;
break; // any others will be older since entries are sorted by version
}
}
@@ -180,26 +195,31 @@ namespace StardewModdingAPI.Web.Framework
// normalize values
if (previewVersion is not null)
{
- if (mainVersion is null) {
+ if (mainVersion is null)
+ {
// if every version is prerelease, latest one is the main version
mainVersion = previewVersion;
- mainVersionUrl = previewVersionUrl;
+ mainUrl = previewUrl;
}
- if (!previewVersion.IsNewerThan(mainVersion)) {
+ if (!previewVersion.IsNewerThan(mainVersion))
+ {
previewVersion = null;
- previewVersionUrl = null;
+ previewUrl = null;
}
}
}
- if (subkey is not null) {
- TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl, entry => entry.download?.MatchesSubkey(subkey) == true);
+ // get versions for subkey
+ if (subkey is not null)
+ {
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true);
if (mod?.IsSubkeyStrict == true)
return main != null;
}
- if (main is null)
- TryGetVersions(out main, out preview, out mainVersionUrl, out previewVersionUrl);
+ // fallback to non-subkey versions
+ if (main is null)
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl);
return main != null;
}