summaryrefslogtreecommitdiff
path: root/src/SMAPI.Web/Controllers/ModsApiController.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI.Web/Controllers/ModsApiController.cs')
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs225
1 files changed, 91 insertions, 134 deletions
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index 06768f03..db669bf9 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -12,15 +12,16 @@ using StardewModdingAPI.Toolkit.Framework.Clients.Wiki;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework;
+using StardewModdingAPI.Web.Framework.Caching;
using StardewModdingAPI.Web.Framework.Caching.Mods;
using StardewModdingAPI.Web.Framework.Caching.Wiki;
+using StardewModdingAPI.Web.Framework.Clients;
using StardewModdingAPI.Web.Framework.Clients.Chucklefish;
using StardewModdingAPI.Web.Framework.Clients.CurseForge;
using StardewModdingAPI.Web.Framework.Clients.GitHub;
using StardewModdingAPI.Web.Framework.Clients.ModDrop;
using StardewModdingAPI.Web.Framework.Clients.Nexus;
using StardewModdingAPI.Web.Framework.ConfigModels;
-using StardewModdingAPI.Web.Framework.ModRepositories;
namespace StardewModdingAPI.Web.Controllers
{
@@ -32,8 +33,8 @@ namespace StardewModdingAPI.Web.Controllers
/*********
** Fields
*********/
- /// <summary>The mod repositories which provide mod metadata.</summary>
- private readonly IDictionary<ModRepositoryKey, IModRepository> Repositories;
+ /// <summary>The mod sites which provide mod metadata.</summary>
+ private readonly ModSiteManager ModSites;
/// <summary>The cache in which to store wiki data.</summary>
private readonly IWikiCacheRepository WikiCache;
@@ -61,23 +62,14 @@ 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>
- public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
+ public ModsApiController(IWebHostEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
this.WikiCache = wikiCache;
this.ModCache = modCache;
this.Config = config;
- this.Repositories =
- new IModRepository[]
- {
- new ChucklefishRepository(chucklefish),
- new CurseForgeRepository(curseForge),
- new GitHubRepository(github),
- new ModDropRepository(modDrop),
- new NexusRepository(nexus)
- }
- .ToDictionary(p => p.VendorKey);
+ this.ModSites = new ModSiteManager(new IModSiteClient[] { chucklefish, curseForge, github, modDrop, nexus });
}
/// <summary>Fetch version metadata for the given mods.</summary>
@@ -90,7 +82,7 @@ namespace StardewModdingAPI.Web.Controllers
return new ModEntryModel[0];
// fetch wiki data
- WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray();
+ WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.Data).ToArray();
IDictionary<string, ModEntryModel> mods = new Dictionary<string, ModEntryModel>(StringComparer.CurrentCultureIgnoreCase);
foreach (ModSearchEntryModel mod in model.Mods)
{
@@ -143,45 +135,23 @@ namespace StardewModdingAPI.Web.Controllers
// validate update key
if (!updateKey.LooksValid)
{
- errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
+ errors.Add($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541', with an optional subkey like 'Nexus:541@subkey'.");
continue;
}
// fetch data
- ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
- if (data.Error != null)
+ ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions);
+ if (data.Status != RemoteModStatus.Ok)
{
- errors.Add(data.Error);
+ errors.Add(data.Error ?? data.Status.ToString());
continue;
}
- // handle main version
- if (data.Version != null)
- {
- ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
- if (version == null)
- {
- errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
- continue;
- }
-
- if (this.IsNewer(version, main?.Version))
- main = new ModEntryVersionModel(version, data.Url);
- }
-
- // handle optional version
- if (data.PreviewVersion != null)
- {
- ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
- if (version == null)
- {
- errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
- continue;
- }
-
- if (this.IsNewer(version, optional?.Version))
- optional = new ModEntryVersionModel(version, data.Url);
- }
+ // handle versions
+ if (this.IsNewer(data.Version, main?.Version))
+ main = new ModEntryVersionModel(data.Version, data.Url);
+ if (this.IsNewer(data.PreviewVersion, optional?.Version))
+ optional = new ModEntryVersionModel(data.PreviewVersion, data.Url);
}
// get unofficial version
@@ -221,7 +191,7 @@ namespace StardewModdingAPI.Web.Controllers
}
// get recommended update (if any)
- ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
+ ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
if (apiVersion != null && installedVersion != null)
{
// get newer versions
@@ -281,29 +251,27 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get the mod info for an update key.</summary>
/// <param name="updateKey">The namespaced update key.</param>
/// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
- private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions)
+ /// <param name="mapRemoteVersions">Maps remote versions to a semantic version for update checks.</param>
+ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions, IDictionary<string, string> mapRemoteVersions)
{
- // get mod
- if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes))
+ // get mod page
+ IModPage page;
{
- // get site
- if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
- return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
+ bool isCached =
+ this.ModCache.TryGetMod(updateKey.Site, updateKey.ID, out Cached<IModPage> cachedMod)
+ && !this.ModCache.IsStale(cachedMod.LastUpdated, cachedMod.Data.Status == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes);
- // fetch mod
- ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);
- if (result.Error == null)
+ if (isCached)
+ page = cachedMod.Data;
+ else
{
- if (result.Version == null)
- result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
- else if (!SemanticVersion.TryParse(result.Version, allowNonStandardVersions, out _))
- result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
+ page = await this.ModSites.GetModPageAsync(updateKey);
+ this.ModCache.SaveMod(updateKey.Site, updateKey.ID, page);
}
-
- // cache mod
- this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod);
}
- return mod.GetModel();
+
+ // get version info
+ return this.ModSites.GetPageVersions(page, updateKey.Subkey, allowNonStandardVersions, mapRemoteVersions);
}
/// <summary>Get update keys based on the available mod metadata, while maintaining the precedence order.</summary>
@@ -312,90 +280,79 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="entry">The mod's entry in the wiki list.</param>
private IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
{
- IEnumerable<string> GetRaw()
- {
- // specified update keys
- if (specifiedKeys != null)
- {
- foreach (string key in specifiedKeys)
- yield return key?.Trim();
- }
+ // get unique update keys
+ List<UpdateKey> updateKeys = this.GetUnfilteredUpdateKeys(specifiedKeys, record, entry)
+ .Select(UpdateKey.Parse)
+ .Distinct()
+ .ToList();
- // default update key
- string defaultKey = record?.GetDefaultUpdateKey();
- if (defaultKey != null)
- yield return defaultKey;
-
- // wiki metadata
- if (entry != null)
- {
- if (entry.NexusID.HasValue)
- yield return $"{ModRepositoryKey.Nexus}:{entry.NexusID}";
- if (entry.ModDropID.HasValue)
- yield return $"{ModRepositoryKey.ModDrop}:{entry.ModDropID}";
- if (entry.CurseForgeID.HasValue)
- yield return $"{ModRepositoryKey.CurseForge}:{entry.CurseForgeID}";
- if (entry.ChucklefishID.HasValue)
- yield return $"{ModRepositoryKey.Chucklefish}:{entry.ChucklefishID}";
- }
+ // apply remove overrides from wiki
+ {
+ var removeKeys = new HashSet<UpdateKey>(
+ from key in entry?.ChangeUpdateKeys ?? new string[0]
+ where key.StartsWith('-')
+ select UpdateKey.Parse(key.Substring(1))
+ );
+ if (removeKeys.Any())
+ updateKeys.RemoveAll(removeKeys.Contains);
}
- HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
- foreach (string rawKey in GetRaw())
+ // if the list has both an update key (like "Nexus:2400") and subkey (like "Nexus:2400@subkey") for the same page, the subkey takes priority
{
- if (string.IsNullOrWhiteSpace(rawKey))
- continue;
-
- UpdateKey key = UpdateKey.Parse(rawKey);
- if (seen.Add(key))
- yield return key;
+ var removeKeys = new HashSet<UpdateKey>();
+ foreach (var key in updateKeys)
+ {
+ if (key.Subkey != null)
+ removeKeys.Add(new UpdateKey(key.Site, key.ID, null));
+ }
+ if (removeKeys.Any())
+ updateKeys.RemoveAll(removeKeys.Contains);
}
- }
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The version to parse.</param>
- /// <param name="map">A map of version replacements.</param>
- /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
- {
- // try mapped version
- string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
- if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
- return parsedNew;
-
- // return original version
- return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
- ? parsedOld
- : null;
+ return updateKeys;
}
- /// <summary>Get a semantic local version for update checks.</summary>
- /// <param name="version">The version to map.</param>
- /// <param name="map">A map of version replacements.</param>
- /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
- private string GetRawMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
+ /// <summary>Get every available update key based on the available mod metadata, including duplicates and keys which should be filtered.</summary>
+ /// <param name="specifiedKeys">The specified update keys.</param>
+ /// <param name="record">The mod's entry in SMAPI's internal database.</param>
+ /// <param name="entry">The mod's entry in the wiki list.</param>
+ private IEnumerable<string> GetUnfilteredUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
{
- if (version == null || map == null || !map.Any())
- return version;
-
- // match exact raw version
- if (map.ContainsKey(version))
- return map[version];
+ // specified update keys
+ foreach (string key in specifiedKeys ?? Array.Empty<string>())
+ {
+ if (!string.IsNullOrWhiteSpace(key))
+ yield return key.Trim();
+ }
- // match parsed version
- if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed))
+ // default update key
{
- if (map.ContainsKey(parsed.ToString()))
- return map[parsed.ToString()];
+ string defaultKey = record?.GetDefaultUpdateKey();
+ if (!string.IsNullOrWhiteSpace(defaultKey))
+ yield return defaultKey;
+ }
- foreach (var pair in map)
- {
- if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion))
- return newVersion.ToString();
- }
+ // wiki metadata
+ if (entry != null)
+ {
+ if (entry.NexusID.HasValue)
+ yield return UpdateKey.GetString(ModSiteKey.Nexus, entry.NexusID.ToString());
+ if (entry.ModDropID.HasValue)
+ yield return UpdateKey.GetString(ModSiteKey.ModDrop, entry.ModDropID.ToString());
+ if (entry.CurseForgeID.HasValue)
+ yield return UpdateKey.GetString(ModSiteKey.CurseForge, entry.CurseForgeID.ToString());
+ if (entry.ChucklefishID.HasValue)
+ yield return UpdateKey.GetString(ModSiteKey.Chucklefish, entry.ChucklefishID.ToString());
}
- return version;
+ // overrides from wiki
+ foreach (string key in entry?.ChangeUpdateKeys ?? Array.Empty<string>())
+ {
+ if (key.StartsWith('+'))
+ yield return key.Substring(1);
+ else if (!key.StartsWith("-"))
+ yield return key;
+ }
}
}
}