using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Web.Framework.Clients;
namespace StardewModdingAPI.Web.Framework
{
/// Handles fetching data from mod sites.
internal class ModSiteManager
{
/*********
** Fields
*********/
/// The mod sites which provide mod metadata.
private readonly IDictionary ModSites;
/*********
** Public methods
*********/
/// Construct an instance.
/// The mod sites which provide mod metadata.
public ModSiteManager(IModSiteClient[] modSites)
{
this.ModSites = modSites.ToDictionary(p => p.SiteKey);
}
/// Get the mod info for an update key.
/// The namespaced update key.
public async Task GetModPageAsync(UpdateKey updateKey)
{
// get site
if (!this.ModSites.TryGetValue(updateKey.Site, out IModSiteClient client))
return new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{updateKey.Site}'. Expected one of [{string.Join(", ", this.ModSites.Keys)}].");
// fetch mod
IModPage mod;
try
{
mod = await client.GetModData(updateKey.ID);
}
catch (Exception ex)
{
mod = new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.TemporaryError, ex.ToString());
}
// handle errors
return mod ?? new GenericModPage(updateKey.Site, updateKey.ID).SetError(RemoteModStatus.DoesNotExist, $"Found no {updateKey.Site} mod with ID '{updateKey.ID}'.");
}
/// Parse version info for the given mod page info.
/// The mod page info.
/// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)
/// Maps remote versions to a semantic version for update checks.
/// Whether to allow non-standard versions.
public ModInfoModel GetPageVersions(IModPage page, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions)
{
// get base model
ModInfoModel model = new ModInfoModel()
.SetBasicInfo(page.Name, page.Url)
.SetError(page.Status, page.Error);
if (page.Status != RemoteModStatus.Ok)
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);
if (!hasVersions)
return model.SetError(RemoteModStatus.InvalidData, $"The {page.Site} mod with ID '{page.Id}' has no valid versions.");
// return info
return model.SetVersions(mainVersion, previewVersion);
}
/// Get a semantic local version for update checks.
/// The version to parse.
/// A map of version replacements.
/// Whether to allow non-standard versions.
public ISemanticVersion GetMappedVersion(string version, IDictionary 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;
}
/*********
** Private methods
*********/
/// Get the mod version numbers for the given mod.
/// The mod to check.
/// The optional update subkey to match in available files. (If no file names or descriptions contain the subkey, it'll be ignored.)
/// Whether to allow non-standard versions.
/// Maps remote versions to a semantic version for update checks.
/// The main mod version.
/// The latest prerelease version, if newer than .
private bool TryGetLatestVersions(IModPage mod, string subkey, bool allowNonStandardVersions, IDictionary mapRemoteVersions, out ISemanticVersion main, out ISemanticVersion preview)
{
main = null;
preview = null;
ISemanticVersion ParseVersion(string raw)
{
raw = this.NormalizeVersion(raw);
return this.GetMappedVersion(raw, mapRemoteVersions, allowNonStandardVersions);
}
if (mod != null)
{
// get mod version
if (subkey == null)
main = ParseVersion(mod.Version);
// get file versions
foreach (IModDownload download in mod.Downloads)
{
// check for subkey if specified
if (subkey != null && download.Name?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true && download.Description?.Contains(subkey, StringComparison.OrdinalIgnoreCase) != true)
continue;
// parse version
ISemanticVersion cur = ParseVersion(download.Version);
if (cur == null)
continue;
// track highest versions
if (main == null || cur.IsNewerThan(main))
main = cur;
if (cur.IsPrerelease() && (preview == null || cur.IsNewerThan(preview)))
preview = cur;
}
if (preview != null && !preview.IsNewerThan(main))
preview = null;
}
return main != null;
}
/// Get a semantic local version for update checks.
/// The version to map.
/// A map of version replacements.
/// Whether to allow non-standard versions.
private string GetRawMappedVersion(string version, IDictionary map, bool allowNonStandard)
{
if (version == null || map == null || !map.Any())
return version;
// match exact raw version
if (map.ContainsKey(version))
return map[version];
// match parsed version
if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed))
{
if (map.ContainsKey(parsed.ToString()))
return map[parsed.ToString()];
foreach ((string fromRaw, string toRaw) in map)
{
if (SemanticVersion.TryParse(fromRaw, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(toRaw, allowNonStandard, out ISemanticVersion newVersion))
return newVersion.ToString();
}
}
return version;
}
/// Normalize a version string.
/// The version to normalize.
private string NormalizeVersion(string version)
{
if (string.IsNullOrWhiteSpace(version))
return null;
version = version.Trim();
if (Regex.IsMatch(version, @"^v\d", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) // common version prefix
version = version.Substring(1);
return version;
}
}
}