summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Web/Framework')
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModDownload.cs18
-rw-r--r--src/SMAPI.Web/Framework/Clients/GenericModPage.cs17
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs7
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs35
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs28
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs106
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs34
-rw-r--r--src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs71
-rw-r--r--src/SMAPI.Web/Framework/IModDownload.cs11
-rw-r--r--src/SMAPI.Web/Framework/IModPage.cs11
-rw-r--r--src/SMAPI.Web/Framework/ModInfoModel.cs30
-rw-r--r--src/SMAPI.Web/Framework/ModSiteManager.cs120
-rw-r--r--src/SMAPI.Web/Framework/RemoteModStatus.cs3
14 files changed, 471 insertions, 50 deletions
diff --git a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
index 548f17c3..6c9c08ef 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModDownload.cs
@@ -1,3 +1,5 @@
+using System;
+
namespace StardewModdingAPI.Web.Framework.Clients
{
/// <summary>Generic metadata about a file download on a mod page.</summary>
@@ -15,6 +17,9 @@ namespace StardewModdingAPI.Web.Framework.Clients
/// <summary>The download's file version.</summary>
public string? Version { 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
@@ -23,11 +28,22 @@ 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>
- public GenericModDownload(string name, string? description, string? version)
+ /// <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.ModPageUrl = modPageUrl;
+ }
+
+ /// <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 5353c7e1..63ca5a95 100644
--- a/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
+++ b/src/SMAPI.Web/Framework/Clients/GenericModPage.cs
@@ -40,6 +40,9 @@ namespace StardewModdingAPI.Web.Framework.Clients
[MemberNotNullWhen(true, nameof(IModPage.Name), nameof(IModPage.Url))]
public bool IsValid => this.Status == RemoteModStatus.Ok;
+ /// <summary>Whether this mod page requires update subkeys and does not allow matching downloads without them.</summary>
+ public bool RequireSubkey { get; set; } = false;
+
/*********
** Public methods
@@ -79,5 +82,19 @@ namespace StardewModdingAPI.Web.Framework.Clients
return this;
}
+
+ /// <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>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
new file mode 100644
index 00000000..bf1edd3f
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/IUpdateManifestClient.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <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/ResponseModels/UpdateManifestModModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs
new file mode 100644
index 00000000..ead5c229
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModModel.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <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; }
+
+ /// <summary>The mod page URL from which to download updates.</summary>
+ public string? ModPageUrl { 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="modPageUrl">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? modPageUrl, UpdateManifestVersionModel[]? versions)
+ {
+ this.Name = name;
+ this.ModPageUrl = modPageUrl;
+ this.Versions = versions ?? Array.Empty<UpdateManifestVersionModel>();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs
new file mode 100644
index 00000000..5ccd31b0
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestModel.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <summary>The data model for an update manifest file.</summary>
+ internal class UpdateManifestModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The manifest format version. This is equivalent to the SMAPI version, and is used to parse older manifests correctly if later versions of SMAPI change the expected format.</summary>
+ public string Format { 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="format">The manifest format version.</param>
+ /// <param name="mods">The mod info in this update manifest.</param>
+ public UpdateManifestModel(string format, IDictionary<string, UpdateManifestModModel>? mods)
+ {
+ this.Format = format;
+ this.Mods = mods ?? new Dictionary<string, UpdateManifestModModel>();
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs
new file mode 100644
index 00000000..6678f5eb
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/ResponseModels/UpdateManifestVersionModel.cs
@@ -0,0 +1,28 @@
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels
+{
+ /// <summary>Data model for a Version in an update manifest.</summary>
+ internal class UpdateManifestVersionModel
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The mod's semantic version.</summary>
+ public string? Version { get; }
+
+ /// <summary>The mod page URL from which to download updates, if different from <see cref="UpdateManifestModModel.ModPageUrl"/>.</summary>
+ public string? ModPageUrl { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="version">The mod's semantic version.</param>
+ /// <param name="modPageUrl">The mod page URL from which to download updates, if different from <see cref="UpdateManifestModModel.ModPageUrl"/>.</param>
+ public UpdateManifestVersionModel(string version, string? modPageUrl)
+ {
+ this.Version = version;
+ this.ModPageUrl = modPageUrl;
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
new file mode 100644
index 00000000..27072897
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestClient.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Net;
+using System.Net.Http.Headers;
+using System.Threading.Tasks;
+using Pathoschild.Http.Client;
+using StardewModdingAPI.Toolkit;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels;
+
+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
+ *********/
+ /// <summary>The unique key for the mod site.</summary>
+ public ModSiteKey SiteKey => ModSiteKey.UpdateManifest;
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="userAgent">The user agent for the API client.</param>
+ public UpdateManifestClient(string userAgent)
+ {
+ this.Client = new FluentClient()
+ .SetUserAgent(userAgent);
+
+ this.Client.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
+ }
+
+ /// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
+ public void Dispose()
+ {
+ this.Client.Dispose();
+ }
+
+ /// <inheritdoc/>
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "This is the method which ensures the annotations are correct.")]
+ public async Task<IModPage?> GetModData(string id)
+ {
+ // get raw update manifest
+ UpdateManifestModel? manifest;
+ try
+ {
+ manifest = await this.Client.GetAsync(id).As<UpdateManifestModel?>();
+ if (manifest is null)
+ return this.GetFormatError(id, "manifest can't be empty");
+ }
+ catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
+ {
+ return new GenericModPage(this.SiteKey, id).SetError(RemoteModStatus.DoesNotExist, $"No update manifest found at {id}");
+ }
+ catch (Exception ex)
+ {
+ return this.GetFormatError(id, ex.Message);
+ }
+
+ // validate
+ if (!SemanticVersion.TryParse(manifest.Format, out _))
+ return this.GetFormatError(id, $"invalid format version '{manifest.Format}'");
+ foreach (UpdateManifestModModel mod in manifest.Mods.Values)
+ {
+ if (mod is null)
+ return this.GetFormatError(id, "a mod record can't be null");
+ if (string.IsNullOrWhiteSpace(mod.ModPageUrl))
+ return this.GetFormatError(id, $"all mods must have a {nameof(mod.ModPageUrl)} value");
+ foreach (UpdateManifestVersionModel? version in mod.Versions)
+ {
+ if (version is null)
+ return this.GetFormatError(id, "a version record can't be null");
+ if (string.IsNullOrWhiteSpace(version.Version))
+ return this.GetFormatError(id, $"all version records must have a {nameof(version.Version)} field");
+ if (!SemanticVersion.TryParse(version.Version, out _))
+ return this.GetFormatError(id, $"invalid mod version '{version.Version}'");
+ }
+ }
+
+ // build model
+ return new UpdateManifestModPage(id, manifest);
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get a mod page instance with an error indicating the update manifest is invalid.</summary>
+ /// <param name="url">The full URL to the update manifest.</param>
+ /// <param name="reason">A human-readable reason phrase indicating why it's invalid.</param>
+ private IModPage GetFormatError(string url, string reason)
+ {
+ return new GenericModPage(this.SiteKey, url).SetError(RemoteModStatus.InvalidData, $"The update manifest at {url} is invalid ({reason})");
+ }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
new file mode 100644
index 00000000..f8cb760a
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModDownload.cs
@@ -0,0 +1,34 @@
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>Metadata about a mod download in an update manifest file.</summary>
+ 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="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 fieldName, string name, string? version, string? url)
+ : base(name, null, version, url)
+ {
+ this.Subkey = '@' + fieldName;
+ }
+
+ /// <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/UpdateManifestModPage.cs b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
new file mode 100644
index 00000000..df752713
--- /dev/null
+++ b/src/SMAPI.Web/Framework/Clients/UpdateManifest/UpdateManifestModPage.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Linq;
+using StardewModdingAPI.Toolkit.Framework.UpdateData;
+using StardewModdingAPI.Web.Framework.Clients.UpdateManifest.ResponseModels;
+
+namespace StardewModdingAPI.Web.Framework.Clients.UpdateManifest
+{
+ /// <summary>Metadata about an update manifest "page".</summary>
+ 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.RequireSubkey = true;
+ this.Mods = manifest.Mods;
+ 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)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod)
+ ? mod.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)
+ {
+ return subkey is not null && this.Mods.TryGetValue(subkey.TrimStart('@'), out UpdateManifestModModel? mod)
+ ? mod.ModPageUrl
+ : null;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <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)
+ {
+ foreach (UpdateManifestVersionModel version in mod.Versions)
+ yield return new UpdateManifestModDownload(modKey, mod.Name ?? modKey, version.Version, version.ModPageUrl);
+ }
+ }
+
+ }
+}
diff --git a/src/SMAPI.Web/Framework/IModDownload.cs b/src/SMAPI.Web/Framework/IModDownload.cs
index fe171785..8cb82989 100644
--- a/src/SMAPI.Web/Framework/IModDownload.cs
+++ b/src/SMAPI.Web/Framework/IModDownload.cs
@@ -14,5 +14,16 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The download's file version.</summary>
string? Version { 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; }
+
+
+ /*********
+ ** 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 4d0a8d61..85be41e2 100644
--- a/src/SMAPI.Web/Framework/IModPage.cs
+++ b/src/SMAPI.Web/Framework/IModPage.cs
@@ -39,10 +39,21 @@ namespace StardewModdingAPI.Web.Framework
[MemberNotNullWhen(false, nameof(IModPage.Error))]
bool IsValid { get; }
+ /// <summary>Whether this mod page requires update subkeys and does not allow matching downloads without them.</summary>
+ bool RequireSubkey { get; }
+
/*********
** Methods
*********/
+ /// <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 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>
/// <param name="name">The mod name.</param>
/// <param name="version">The mod's semantic version number.</param>
diff --git a/src/SMAPI.Web/Framework/ModInfoModel.cs b/src/SMAPI.Web/Framework/ModInfoModel.cs
index e70b60bf..502c0827 100644
--- a/src/SMAPI.Web/Framework/ModInfoModel.cs
+++ b/src/SMAPI.Web/Framework/ModInfoModel.cs
@@ -27,6 +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 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; }
+
/*********
** Public methods
@@ -46,7 +52,8 @@ namespace StardewModdingAPI.Web.Framework
{
this
.SetBasicInfo(name, url)
- .SetVersions(version!, previewVersion)
+ .SetMainVersion(version!)
+ .SetPreviewVersion(previewVersion)
.SetError(status, error!);
}
@@ -62,14 +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>
+ /// <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)
+ public ModInfoModel SetMainVersion(ISemanticVersion version, string? modPageUrl = null)
{
this.Version = version;
- this.PreviewVersion = previewVersion;
+ 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 674b9ffc..4bb72f78 100644
--- a/src/SMAPI.Web/Framework/ModSiteManager.cs
+++ b/src/SMAPI.Web/Framework/ModSiteManager.cs
@@ -59,30 +59,42 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>Parse version info for the given mod page info.</summary>
/// <param name="page">The mod page info.</param>
- /// <param name="subkey">The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)</param>
+ /// <param name="updateKey">The update key to match in available files.</param>
/// <param name="mapRemoteVersions">The changes to apply to remote versions for update checks.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
- public ModInfoModel GetPageVersions(IModPage page, string? subkey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
+ public ModInfoModel GetPageVersions(IModPage page, UpdateKey updateKey, bool allowNonStandardVersions, ChangeDescriptor? mapRemoteVersions)
{
- // get base model
+ // get ID to show in errors
+ string displayId = page.RequireSubkey
+ ? page.Id + updateKey.Subkey
+ : page.Id;
+
+ // validate
ModInfoModel model = new();
- if (page.IsValid)
+ if (!page.IsValid)
+ return model.SetError(page.Status, page.Error);
+ if (page.RequireSubkey && updateKey.Subkey is null)
+ return model.SetError(RemoteModStatus.RequiredSubkeyMissing, $"The {page.Site} mod with ID '{displayId}' requires an update subkey indicating which mod to fetch.");
+
+ // add basic info (unless it's a manifest, in which case the 'mod page' is the JSON file)
+ if (updateKey.Site != ModSiteKey.UpdateManifest)
model.SetBasicInfo(page.Name, page.Url);
- else
- {
- model.SetError(page.Status, page.Error);
- return model;
- }
// fetch versions
- bool hasVersions = this.TryGetLatestVersions(page, subkey, allowNonStandardVersions, mapRemoteVersions, out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion);
- if (!hasVersions && subkey != null)
- hasVersions = this.TryGetLatestVersions(page, null, allowNonStandardVersions, mapRemoteVersions, out mainVersion, out previewVersion);
+ bool hasVersions = this.TryGetLatestVersions(page, updateKey.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}' has no valid versions.");
+ return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{displayId}' has no valid versions.");
+
+ // apply mod page info
+ model.SetBasicInfo(
+ name: page.GetName(updateKey.Subkey) ?? page.Name,
+ url: page.GetUrl(updateKey.Subkey) ?? page.Url
+ );
// return info
- return model.SetVersions(mainVersion!, previewVersion);
+ return model
+ .SetMainVersion(mainVersion!, mainModPageUrl)
+ .SetPreviewVersion(previewVersion, previewModPageUrl);
}
/// <summary>Get a semantic local version for update checks.</summary>
@@ -113,34 +125,37 @@ 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)
+ /// <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;
+ mainModPageUrl = null;
+ previewModPageUrl = null;
+ if (mod is null)
+ return false;
// parse all versions from the mod page
- IEnumerable<(string? name, string? description, ISemanticVersion? version)> GetAllVersions()
+ IEnumerable<(IModDownload? download, ISemanticVersion? version)> GetAllVersions()
{
- if (mod != null)
+ ISemanticVersion? ParseAndMapVersion(string? raw)
{
- ISemanticVersion? ParseAndMapVersion(string? raw)
- {
- raw = this.NormalizeVersion(raw);
- return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
- }
+ raw = this.NormalizeVersion(raw);
+ return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
+ }
- // get mod version
- ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
- if (modVersion != null)
- yield return (name: null, description: null, version: ParseAndMapVersion(mod.Version));
+ // get mod version
+ ISemanticVersion? modVersion = ParseAndMapVersion(mod.Version);
+ if (modVersion != null)
+ yield return (download: null, version: modVersion);
- // get file versions
- foreach (IModDownload download in mod.Downloads)
- {
- ISemanticVersion? cur = ParseAndMapVersion(download.Version);
- if (cur != null)
- yield return (download.Name, download.Description, cur);
- }
+ // get file versions
+ foreach (IModDownload download in mod.Downloads)
+ {
+ ISemanticVersion? cur = ParseAndMapVersion(download.Version);
+ if (cur != null)
+ yield return (download, cur);
}
}
var versions = GetAllVersions()
@@ -148,40 +163,59 @@ namespace StardewModdingAPI.Web.Framework
.ToArray();
// get main + preview versions
- void TryGetVersions([NotNullWhen(true)] out ISemanticVersion? mainVersion, out ISemanticVersion? previewVersion, Func<(string? name, string? description, 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;
+ mainUrl = null;
+ previewUrl = null;
// get latest main + preview version
- foreach ((string? name, string? description, ISemanticVersion? version) entry in versions)
+ foreach ((IModDownload? download, ISemanticVersion? version) entry in versions)
{
if (entry.version is null || filter?.Invoke(entry) == false)
continue;
if (entry.version.IsPrerelease())
- previewVersion ??= entry.version;
+ {
+ if (previewVersion is null)
+ {
+ previewVersion = entry.version;
+ previewUrl = entry.download?.ModPageUrl;
+ }
+ }
else
- mainVersion ??= entry.version;
-
- if (mainVersion != null)
+ {
+ mainVersion = entry.version;
+ mainUrl = entry.download?.ModPageUrl;
break; // any others will be older since entries are sorted by version
+ }
}
// normalize values
if (previewVersion is not null)
{
- mainVersion ??= previewVersion; // if every version is prerelease, latest one is the main version
+ if (mainVersion is null)
+ {
+ // if every version is prerelease, latest one is the main version
+ mainVersion = previewVersion;
+ mainUrl = previewUrl;
+ }
if (!previewVersion.IsNewerThan(mainVersion))
+ {
previewVersion = null;
+ previewUrl = null;
+ }
}
}
+ // get versions for subkey
if (subkey is not null)
- TryGetVersions(out main, out preview, entry => entry.name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true || entry.description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) == true);
- if (main is null)
- TryGetVersions(out main, out preview);
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl, filter: entry => entry.download?.MatchesSubkey(subkey) == true);
+ // fallback to non-subkey versions
+ if (main is null && !mod.RequireSubkey)
+ TryGetVersions(out main, out preview, out mainModPageUrl, out previewModPageUrl);
return main != null;
}
diff --git a/src/SMAPI.Web/Framework/RemoteModStatus.cs b/src/SMAPI.Web/Framework/RemoteModStatus.cs
index 139ecfd3..235bcec4 100644
--- a/src/SMAPI.Web/Framework/RemoteModStatus.cs
+++ b/src/SMAPI.Web/Framework/RemoteModStatus.cs
@@ -12,6 +12,9 @@ namespace StardewModdingAPI.Web.Framework
/// <summary>The mod does not exist.</summary>
DoesNotExist,
+ /// <summary>The mod page exists, but it requires a subkey and none was provided.</summary>
+ RequiredSubkeyMissing,
+
/// <summary>The mod was temporarily unavailable (e.g. the site could not be reached or an unknown error occurred).</summary>
TemporaryError
}