/// <summary>Get the mod info for an update key.</summary> /// <param name="updateKey">The namespaced update key.</param> private async Task <ModInfoModel> GetInfoForUpdateKeyAsync(string updateKey) { // parse update key if (!this.TryParseModKey(updateKey, out string vendorKey, out string modID)) { return(new ModInfoModel($"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.")); } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { return(new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].")); } // fetch mod info return(await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { ModInfoModel result = await repository.GetModInfoAsync(modID); if (result.Error != null) { if (result.Version == null) { result.Error = $"The update key '{updateKey}' matches a mod with no version number."; } else if (!Regex.IsMatch(result.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) { result.Error = $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."; } } entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(result.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); return result; })); }
/// <summary>Get the mod info for an update key.</summary> /// <param name="updateKey">The namespaced update key.</param> private async Task <ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey) { // get mod if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes)) { // 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)}].")); } // fetch mod ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID); if (result.Error == null) { 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, out _)) { result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'."); } } // cache mod this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod); } return(mod.GetModel()); }
public async Task <IDictionary <string, ModInfoModel> > PostAsync([FromBody] ModSearchModel search) { // parse model bool allowInvalidVersions = search?.AllowInvalidVersions ?? false; string[] modKeys = (search?.ModKeys?.ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) .ToArray(); // fetch mod info IDictionary <string, ModInfoModel> result = new Dictionary <string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase); foreach (string modKey in modKeys) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'."); continue; } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { result[modKey] = new ModInfoModel($"There's no mod site with key '{vendorKey}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}]."); continue; } // fetch mod info result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { // fetch info ModInfoModel info = await repository.GetModInfoAsync(modID); // validate if (info.Error == null) { if (info.Version == null) { info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: "Mod has no version number."); } if (!allowInvalidVersions && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) { info = new ModInfoModel(name: info.Name, version: info.Version, url: info.Url, error: $"Mod has invalid semantic version '{info.Version}'."); } } // cache & return entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(info.Error == null ? this.SuccessCacheMinutes : this.ErrorCacheMinutes); return(info); }); } return(result); }
public async Task <IDictionary <string, ModInfoModel> > GetAsync(string modKeys) { // sort & filter keys string[] modKeysArray = (modKeys?.Split(',').Select(p => p.Trim()).ToArray() ?? new string[0]) .Distinct(StringComparer.CurrentCultureIgnoreCase) .OrderBy(p => p, StringComparer.CurrentCultureIgnoreCase) .ToArray(); // fetch mod info IDictionary <string, ModInfoModel> result = new Dictionary <string, ModInfoModel>(StringComparer.CurrentCultureIgnoreCase); foreach (string modKey in modKeysArray) { // parse mod key if (!this.TryParseModKey(modKey, out string vendorKey, out string modID)) { result[modKey] = new ModInfoModel("The mod key isn't in a valid format. It should contain the mod repository key and mod ID like 'Nexus:541'."); continue; } // get matching repository if (!this.Repositories.TryGetValue(vendorKey, out IModRepository repository)) { result[modKey] = new ModInfoModel("There's no mod repository matching this namespaced mod ID."); continue; } // fetch mod info result[modKey] = await this.Cache.GetOrCreateAsync($"{repository.VendorKey}:{modID}".ToLower(), async entry => { entry.AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(this.CacheMinutes); ModInfoModel info = await repository.GetModInfoAsync(modID); if (info.Error == null && !Regex.IsMatch(info.Version, this.VersionRegex, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)) { info = new ModInfoModel(info.Name, info.Version, info.Url, $"Mod has invalid semantic version '{info.Version}'."); } return(info); }); } return(result); }
/// <summary>Construct an instance.</summary> /// <param name="site">The mod site on which the mod is found.</param> /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The mod data.</param> public CachedMod(ModRepositoryKey site, string id, ModInfoModel mod) { // tracking this.LastUpdated = DateTimeOffset.UtcNow; this.LastRequested = DateTimeOffset.UtcNow; // metadata this.Site = site; this.ID = id; this.FetchStatus = mod.Status; this.FetchError = mod.Error; // mod info this.Name = mod.Name; this.MainVersion = mod.Version; this.PreviewVersion = mod.PreviewVersion; this.Url = mod.Url; this.LicenseUrl = mod.LicenseUrl; this.LicenseName = mod.LicenseName; }
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> /// <param name="mods">The mods to include in the update check (if eligible).</param> private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) { return; } new Thread(() => { // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); // check SMAPI version try { this.Monitor.Log("Checking for SMAPI update...", LogLevel.Trace); ModInfoModel response = client.GetModInfo($"GitHub:{this.Settings.GitHubProjectName}").Single().Value; if (response.Error != null) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {response.Error}"); } else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) { this.Monitor.Log($"You can update SMAPI to {response.Version}: {response.Url}", LogLevel.Alert); } else { this.VerboseLog(" OK."); } } catch (Exception ex) { this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn); this.Monitor.Log($"Error: {ex.GetLogSummary()}"); } // check mod versions try { // log issues if (this.Settings.VerboseLogging) { this.VerboseLog("Validating mod update keys..."); foreach (IModMetadata mod in mods) { if (mod.Manifest == null) { this.VerboseLog($" {mod.DisplayName}: no manifest."); } else if (mod.Manifest.UpdateKeys == null || !mod.Manifest.UpdateKeys.Any()) { this.VerboseLog($" {mod.DisplayName}: no update keys."); } } } // prepare update keys Dictionary <string, IModMetadata[]> modsByKey = ( from mod in mods where mod.Manifest?.UpdateKeys != null from key in mod.Manifest.UpdateKeys select new { key, mod } ) .GroupBy(p => p.key, StringComparer.InvariantCultureIgnoreCase) .ToDictionary( group => group.Key, group => group.Select(p => p.mod).ToArray(), StringComparer.InvariantCultureIgnoreCase ); // fetch results this.Monitor.Log($"Checking for updates to {modsByKey.Keys.Count} keys...", LogLevel.Trace); var results = ( from entry in client.GetModInfo(modsByKey.Keys.ToArray()) from mod in modsByKey[entry.Key] orderby mod.DisplayName select new { entry.Key, Mod = mod, Info = entry.Value } ) .ToArray(); // extract latest versions IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>(); foreach (var result in results) { IModMetadata mod = result.Mod; ModInfoModel info = result.Info; // handle error if (info.Error != null) { this.Monitor.Log($" {mod.DisplayName} ({result.Key}): update error: {info.Error}", LogLevel.Trace); continue; } // track update ISemanticVersion localVersion = mod.DataRecord != null ? new SemanticVersion(mod.DataRecord.GetLocalVersionForUpdateChecks(mod.Manifest.Version.ToString())) : mod.Manifest.Version; ISemanticVersion latestVersion = new SemanticVersion(mod.DataRecord != null ? mod.DataRecord.GetRemoteVersionForUpdateChecks(new SemanticVersion(info.Version).ToString()) : info.Version ); bool isUpdate = latestVersion.IsNewerThan(localVersion); this.VerboseLog($" {mod.DisplayName} ({result.Key}): {(isUpdate ? $"{mod.Manifest.Version}{(!localVersion.Equals(mod.Manifest.Version) ? $" [{localVersion}]" : "")} => {info.Version}{(!latestVersion.Equals(new SemanticVersion(info.Version)) ? $" [{latestVersion}]" : "")}" : "OK")}."); if (isUpdate) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || latestVersion.IsNewerThan(other.Version)) { updatesByMod[mod] = info; } } } // output if (updatesByMod.Any()) { this.Monitor.Newline(); this.Monitor.Log($"You can update {updatesByMod.Count} mod{(updatesByMod.Count != 1 ? "s" : "")}:", LogLevel.Alert); foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) { this.Monitor.Log($" {entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}", LogLevel.Alert); } } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } }).Start(); }
/// <summary>Save data fetched for a mod.</summary> /// <param name="site">The mod site on which the mod is found.</param> /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The mod data.</param> /// <param name="cachedMod">The stored mod record.</param> public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) { id = this.NormalizeId(id); cachedMod = this.SaveMod(new CachedMod(site, id, mod)); }
/********* ** Private methods *********/ /// <summary>Get the metadata for a mod.</summary> /// <param name="search">The mod data to match.</param> /// <param name="wikiData">The wiki data.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <returns>Returns the mod data if found, else <c>null</c>.</returns> private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiCompatibilityEntry[] wikiData, bool includeExtendedMetadata) { // resolve update keys var updateKeys = new HashSet <string>(search.UpdateKeys ?? new string[0], StringComparer.InvariantCultureIgnoreCase); ModDataRecord record = this.ModDatabase.Get(search.ID); if (record?.Fields != null) { string defaultUpdateKey = record.Fields.FirstOrDefault(p => p.Key == ModDataFieldKey.UpdateKey && p.IsDefault)?.Value; if (!string.IsNullOrWhiteSpace(defaultUpdateKey)) { updateKeys.Add(defaultUpdateKey); } } // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList <string> errors = new List <string>(); foreach (string updateKey in updateKeys) { // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); if (data.Error != null) { errors.Add(data.Error); continue; } // handle main version if (data.Version != null) { if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } if (this.IsNewer(version, result.Main?.Version)) { result.Main = new ModEntryVersionModel(version, data.Url); } } // handle optional version if (data.PreviewVersion != null) { if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } if (this.IsNewer(version, result.Optional?.Version)) { result.Optional = new ModEntryVersionModel(version, data.Url); } } } // get unofficial version WikiCompatibilityEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(result.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); if (wikiEntry?.UnofficialVersion != null && this.IsNewer(wikiEntry.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.UnofficialVersion, result.Optional?.Version)) { result.Unofficial = new ModEntryVersionModel(wikiEntry.UnofficialVersion, this.WikiCompatibilityPageUrl); } // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { result.Main = result.Optional; result.Optional = null; } // special cases if (result.ID == "Pathoschild.SMAPI") { if (result.Main != null) { result.Main.Url = "https://smapi.io/"; } if (result.Optional != null) { result.Optional.Url = "https://smapi.io/"; } } // add extended metadata if (includeExtendedMetadata && (wikiEntry != null || record != null)) { result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); } // add result result.Errors = errors.ToArray(); return(result); }
/********* ** Private methods *********/ /// <summary>Get the metadata for a mod.</summary> /// <param name="search">The mod data to match.</param> /// <param name="wikiData">The wiki data.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <param name="apiVersion">The SMAPI version installed by the player.</param> /// <returns>Returns the mod data if found, else <c>null</c>.</returns> private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata, ISemanticVersion apiVersion) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.OrdinalIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.OrdinalIgnoreCase)); bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false; // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList <string> errors = new List <string>(); ModEntryVersionModel main = null; ModEntryVersionModel optional = null; ModEntryVersionModel unofficial = null; ModEntryVersionModel unofficialForBeta = null; foreach (UpdateKey updateKey in updateKeys) { // 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', with an optional subkey like 'Nexus:541@subkey'."); continue; } // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions, wikiEntry?.MapRemoteVersions); if (data.Status != RemoteModStatus.Ok) { errors.Add(data.Error ?? data.Status.ToString()); continue; } // 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 if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, optional?.Version)) { unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}"); } // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { unofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, optional?.Version)) ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.Url.PlainAction("Index", "Mods", absoluteUrl: true)}#{wikiEntry.Anchor}") : null; } else { unofficialForBeta = unofficial; } } } // fallback to preview if latest is invalid if (main == null && optional != null) { main = optional; optional = null; } // special cases if (overrides?.SetUrl != null) { if (main != null) { main.Url = overrides.SetUrl; } if (optional != null) { optional.Url = overrides.SetUrl; } } // get recommended update (if any) ISemanticVersion installedVersion = this.ModSites.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions); if (apiVersion != null && installedVersion != null) { // get newer versions List <ModEntryVersionModel> updates = new List <ModEntryVersionModel>(); if (this.IsRecommendedUpdate(installedVersion, main?.Version, useBetaChannel: true)) { updates.Add(main); } if (this.IsRecommendedUpdate(installedVersion, optional?.Version, useBetaChannel: installedVersion.IsPrerelease() || search.IsBroken)) { updates.Add(optional); } if (this.IsRecommendedUpdate(installedVersion, unofficial?.Version, useBetaChannel: true)) { updates.Add(unofficial); } if (this.IsRecommendedUpdate(installedVersion, unofficialForBeta?.Version, useBetaChannel: apiVersion.IsPrerelease())) { updates.Add(unofficialForBeta); } // get newest version ModEntryVersionModel newest = null; foreach (ModEntryVersionModel update in updates) { if (newest == null || update.Version.IsNewerThan(newest.Version)) { newest = update; } } // set field result.SuggestedUpdate = newest != null ? new ModEntryVersionModel(newest.Version, newest.Url) : null; } // add extended metadata if (includeExtendedMetadata) { result.Metadata = new ModExtendedMetadataModel(wikiEntry, record, main: main, optional: optional, unofficial: unofficial, unofficialForBeta: unofficialForBeta); } // add result result.Errors = errors.ToArray(); return(result); }
/// <summary>Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.</summary> /// <param name="mods">The mods to include in the update check (if eligible).</param> private void CheckForUpdatesAsync(IModMetadata[] mods) { if (!this.Settings.CheckForUpdates) { return; } new Thread(() => { // update info List <string> updates = new List <string>(); bool smapiUpdate = false; int modUpdates = 0; // create client WebApiClient client = new WebApiClient(this.Settings.WebApiBaseUrl, Constants.ApiVersion); // fetch SMAPI version try { ModInfoModel response = client.GetModInfoAsync($"GitHub:{this.Settings.GitHubProjectName}").Result.Single().Value; if (response.Error != null) { this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{response.Error}", LogLevel.Warn); } else if (new SemanticVersion(response.Version).IsNewerThan(Constants.ApiVersion)) { smapiUpdate = true; updates.Add($"SMAPI {response.Version}: {response.Url}"); } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.\n{ex.GetLogSummary()}"); } // fetch mod versions try { // prepare update-check data IDictionary <string, IModMetadata> modsByKey = new Dictionary <string, IModMetadata>(StringComparer.InvariantCultureIgnoreCase); foreach (IModMetadata mod in mods) { if (!string.IsNullOrWhiteSpace(mod.Manifest.ChucklefishID)) { modsByKey[$"Chucklefish:{mod.Manifest.ChucklefishID}"] = mod; } if (!string.IsNullOrWhiteSpace(mod.Manifest.NexusID)) { modsByKey[$"Nexus:{mod.Manifest.NexusID}"] = mod; } if (!string.IsNullOrWhiteSpace(mod.Manifest.GitHubProject)) { modsByKey[$"GitHub:{mod.Manifest.GitHubProject}"] = mod; } } // fetch results IDictionary <string, ModInfoModel> response = client.GetModInfoAsync(modsByKey.Keys.ToArray()).Result; IDictionary <IModMetadata, ModInfoModel> updatesByMod = new Dictionary <IModMetadata, ModInfoModel>(); foreach (var entry in response) { // handle error if (entry.Value.Error != null) { this.Monitor.Log($"Couldn't fetch version of {modsByKey[entry.Key].DisplayName} with key {entry.Key}:\n{entry.Value.Error}", LogLevel.Trace); continue; } // collect latest mod version IModMetadata mod = modsByKey[entry.Key]; ISemanticVersion version = new SemanticVersion(entry.Value.Version); if (version.IsNewerThan(mod.Manifest.Version)) { if (!updatesByMod.TryGetValue(mod, out ModInfoModel other) || version.IsNewerThan(other.Version)) { updatesByMod[mod] = entry.Value; modUpdates++; } } } // add to output queue if (updatesByMod.Any()) { foreach (var entry in updatesByMod.OrderBy(p => p.Key.DisplayName)) { updates.Add($"{entry.Key.DisplayName} {entry.Value.Version}: {entry.Value.Url}"); } } } catch (Exception ex) { this.Monitor.Log($"Couldn't check for new mod versions:\n{ex.GetLogSummary()}", LogLevel.Trace); } // output if (updates.Any()) { this.Monitor.Newline(); // print intro string intro = ""; if (smapiUpdate) { intro = "You can update SMAPI"; } if (modUpdates > 0) { intro += $"{(smapiUpdate ? " and" : "You can update")} {modUpdates} mod{(modUpdates != 1 ? "s" : "")}"; } intro += ":"; this.Monitor.Log(intro, LogLevel.Alert); // print update list foreach (string line in updates) { this.Monitor.Log($" {line}", LogLevel.Alert); } } }).Start(); }
/********* ** Private methods *********/ /// <summary>Get the metadata for a mod.</summary> /// <param name="search">The mod data to match.</param> /// <param name="wikiData">The wiki data.</param> /// <param name="includeExtendedMetadata">Whether to include extended metadata for each mod.</param> /// <returns>Returns the mod data if found, else <c>null</c>.</returns> private async Task <ModEntryModel> GetModData(ModSearchEntryModel search, WikiModEntry[] wikiData, bool includeExtendedMetadata) { // cross-reference data ModDataRecord record = this.ModDatabase.Get(search.ID); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); // get latest versions ModEntryModel result = new ModEntryModel { ID = search.ID }; IList <string> errors = new List <string>(); foreach (UpdateKey updateKey in updateKeys) { // 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'."); continue; } // fetch data ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); if (data.Error != null) { errors.Add(data.Error); continue; } // handle main version if (data.Version != null) { if (!SemanticVersion.TryParse(data.Version, out ISemanticVersion version)) { errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); continue; } if (this.IsNewer(version, result.Main?.Version)) { result.Main = new ModEntryVersionModel(version, data.Url); } } // handle optional version if (data.PreviewVersion != null) { if (!SemanticVersion.TryParse(data.PreviewVersion, out ISemanticVersion version)) { errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); continue; } if (this.IsNewer(version, result.Optional?.Version)) { result.Optional = new ModEntryVersionModel(version, data.Url); } } } // get unofficial version if (wikiEntry?.Compatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.Compatibility.UnofficialVersion, result.Optional?.Version)) { result.Unofficial = new ModEntryVersionModel(wikiEntry.Compatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}"); } // get unofficial version for beta if (wikiEntry?.HasBetaInfo == true) { result.HasBetaInfo = true; if (wikiEntry.BetaCompatibility.Status == WikiCompatibilityStatus.Unofficial) { if (wikiEntry.BetaCompatibility.UnofficialVersion != null) { result.UnofficialForBeta = (wikiEntry.BetaCompatibility.UnofficialVersion != null && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Main?.Version) && this.IsNewer(wikiEntry.BetaCompatibility.UnofficialVersion, result.Optional?.Version)) ? new ModEntryVersionModel(wikiEntry.BetaCompatibility.UnofficialVersion, $"{this.CompatibilityPageUrl}/#{wikiEntry.Anchor}") : null; } else { result.UnofficialForBeta = result.Unofficial; } } } // fallback to preview if latest is invalid if (result.Main == null && result.Optional != null) { result.Main = result.Optional; result.Optional = null; } // special cases if (result.ID == "Pathoschild.SMAPI") { if (result.Main != null) { result.Main.Url = "https://smapi.io/"; } if (result.Optional != null) { result.Optional.Url = "https://smapi.io/"; } } // add extended metadata if (includeExtendedMetadata && (wikiEntry != null || record != null)) { result.Metadata = new ModExtendedMetadataModel(wikiEntry, record); } // add result result.Errors = errors.ToArray(); return(result); }
/// <summary>Save data fetched for a mod.</summary> /// <param name="site">The mod site on which the mod is found.</param> /// <param name="id">The mod's unique ID within the <paramref name="site"/>.</param> /// <param name="mod">The mod data.</param> /// <param name="cachedMod">The stored mod record.</param> public void SaveMod(ModRepositoryKey site, string id, ModInfoModel mod, out CachedMod cachedMod) { string key = this.GetKey(site, id); cachedMod = this.SaveMod(new CachedMod(site, id, mod)); }