track license info for mod GitHub repos (#651)
This commit is contained in:
parent
2b4bc2c282
commit
1d085df5b7
|
@ -26,6 +26,7 @@ These changes have not been released yet.
|
|||
* Fixed map reloads not updating door warps.
|
||||
* Fixed outdoor tilesheets being seasonalised when added to an indoor location.
|
||||
* Fixed update checks failing for Nexus mods marked as adult content.
|
||||
* Fixed update checks not recognising releases on GitHub if they're not explicitly listed as update keys.
|
||||
|
||||
* For the mod compatibility list:
|
||||
* Now loads faster (since data is fetched in a background service).
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@ using System;
|
|||
namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
||||
{
|
||||
/// <summary>A namespaced mod ID which uniquely identifies a mod within a mod repository.</summary>
|
||||
public class UpdateKey
|
||||
public class UpdateKey : IEquatable<UpdateKey>
|
||||
{
|
||||
/*********
|
||||
** Accessors
|
||||
|
@ -38,6 +38,12 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
&& !string.IsNullOrWhiteSpace(id);
|
||||
}
|
||||
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="repository">The mod repository containing the mod.</param>
|
||||
/// <param name="id">The mod ID within the repository.</param>
|
||||
public UpdateKey(ModRepositoryKey repository, string id)
|
||||
: this($"{repository}:{id}", repository, id) { }
|
||||
|
||||
/// <summary>Parse a raw update key.</summary>
|
||||
/// <param name="raw">The raw update key to parse.</param>
|
||||
public static UpdateKey Parse(string raw)
|
||||
|
@ -69,5 +75,29 @@ namespace StardewModdingAPI.Toolkit.Framework.UpdateData
|
|||
? $"{this.Repository}:{this.ID}"
|
||||
: this.RawText;
|
||||
}
|
||||
|
||||
/// <summary>Indicates whether the current object is equal to another object of the same type.</summary>
|
||||
/// <param name="other">An object to compare with this object.</param>
|
||||
public bool Equals(UpdateKey other)
|
||||
{
|
||||
return
|
||||
other != null
|
||||
&& this.Repository == other.Repository
|
||||
&& string.Equals(this.ID, other.ID, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Determines whether the specified object is equal to the current object.</summary>
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is UpdateKey other && this.Equals(other);
|
||||
}
|
||||
|
||||
/// <summary>Serves as the default hash function. </summary>
|
||||
/// <returns>A hash code for the current object.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return $"{this.Repository}:{this.ID}".ToLower().GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,11 +50,11 @@ namespace StardewModdingAPI.Web
|
|||
|
||||
// set startup tasks
|
||||
BackgroundJob.Enqueue(() => BackgroundService.UpdateWikiAsync());
|
||||
BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleMods());
|
||||
BackgroundJob.Enqueue(() => BackgroundService.RemoveStaleModsAsync());
|
||||
|
||||
// set recurring tasks
|
||||
RecurringJob.AddOrUpdate(() => BackgroundService.UpdateWikiAsync(), "*/10 * * * *"); // every 10 minutes
|
||||
RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleMods(), "0 * * * *"); // hourly
|
||||
RecurringJob.AddOrUpdate(() => BackgroundService.RemoveStaleModsAsync(), "0 * * * *"); // hourly
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -84,9 +84,10 @@ namespace StardewModdingAPI.Web
|
|||
}
|
||||
|
||||
/// <summary>Remove mods which haven't been requested in over 48 hours.</summary>
|
||||
public static async Task RemoveStaleMods()
|
||||
public static Task RemoveStaleModsAsync()
|
||||
{
|
||||
BackgroundService.ModCache.RemoveStaleMods(TimeSpan.FromHours(48));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -123,18 +123,33 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
// crossreference data
|
||||
ModDataRecord record = this.ModDatabase.Get(search.ID);
|
||||
WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
|
||||
string[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
|
||||
UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
|
||||
|
||||
// add soft lookups (don't log errors if the target doesn't exist)
|
||||
UpdateKey[] softUpdateKeys = updateKeys.All(key => key.Repository != ModRepositoryKey.GitHub) && !string.IsNullOrWhiteSpace(wikiEntry?.GitHubRepo)
|
||||
? new[] { new UpdateKey(ModRepositoryKey.GitHub, wikiEntry.GitHubRepo) }
|
||||
: new UpdateKey[0];
|
||||
|
||||
// get latest versions
|
||||
ModEntryModel result = new ModEntryModel { ID = search.ID };
|
||||
IList<string> errors = new List<string>();
|
||||
foreach (string updateKey in updateKeys)
|
||||
foreach (UpdateKey updateKey in updateKeys.Concat(softUpdateKeys))
|
||||
{
|
||||
bool isSoftLookup = softUpdateKeys.Contains(updateKey);
|
||||
|
||||
// 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);
|
||||
if (!isSoftLookup || data.Status != RemoteModStatus.DoesNotExist)
|
||||
errors.Add(data.Error);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -221,32 +236,27 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
|
||||
/// <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)
|
||||
private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey)
|
||||
{
|
||||
// parse update key
|
||||
UpdateKey parsed = UpdateKey.Parse(updateKey);
|
||||
if (!parsed.LooksValid)
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The update key '{updateKey}' isn't in a valid format. It should contain the site key and mod ID like 'Nexus:541'.");
|
||||
|
||||
// get mod
|
||||
if (!this.ModCache.TryGetMod(parsed.Repository, parsed.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
|
||||
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(parsed.Repository, out IModRepository repository))
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"There's no mod site with key '{parsed.Repository}'. Expected one of [{string.Join(", ", this.Repositories.Keys)}].");
|
||||
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(parsed.ID);
|
||||
ModInfoModel result = await repository.GetModInfoAsync(updateKey.ID);
|
||||
if (result.Error == null)
|
||||
{
|
||||
if (result.Version == null)
|
||||
result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
|
||||
else if (!SemanticVersion.TryParse(result.Version, out _))
|
||||
result.WithError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
|
||||
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
|
||||
}
|
||||
|
||||
// cache mod
|
||||
this.ModCache.SaveMod(repository.VendorKey, parsed.ID, result, out mod);
|
||||
this.ModCache.SaveMod(repository.VendorKey, updateKey.ID, result, out mod);
|
||||
}
|
||||
return mod.GetModel();
|
||||
}
|
||||
|
@ -255,7 +265,7 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
/// <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>
|
||||
public IEnumerable<string> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
|
||||
public IEnumerable<UpdateKey> GetUpdateKeys(string[] specifiedKeys, ModDataRecord record, WikiModEntry entry)
|
||||
{
|
||||
IEnumerable<string> GetRaw()
|
||||
{
|
||||
|
@ -283,10 +293,14 @@ namespace StardewModdingAPI.Web.Controllers
|
|||
}
|
||||
}
|
||||
|
||||
HashSet<string> seen = new HashSet<string>(StringComparer.InvariantCulture);
|
||||
foreach (string key in GetRaw())
|
||||
HashSet<UpdateKey> seen = new HashSet<UpdateKey>();
|
||||
foreach (string rawKey in GetRaw())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key) && seen.Add(key))
|
||||
if (string.IsNullOrWhiteSpace(rawKey))
|
||||
continue;
|
||||
|
||||
UpdateKey key = UpdateKey.Parse(rawKey);
|
||||
if (seen.Add(key))
|
||||
yield return key;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,12 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
|||
/// <summary>The URL for the mod page.</summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>The license URL, if available.</summary>
|
||||
public string LicenseUrl { get; set; }
|
||||
|
||||
/// <summary>The license name, if available.</summary>
|
||||
public string LicenseName { get; set; }
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
|
@ -86,12 +92,16 @@ namespace StardewModdingAPI.Web.Framework.Caching.Mods
|
|||
this.MainVersion = mod.Version;
|
||||
this.PreviewVersion = mod.PreviewVersion;
|
||||
this.Url = mod.Url;
|
||||
this.LicenseUrl = mod.LicenseUrl;
|
||||
this.LicenseName = mod.LicenseName;
|
||||
}
|
||||
|
||||
/// <summary>Get the API model for the cached data.</summary>
|
||||
public ModInfoModel GetModel()
|
||||
{
|
||||
return new ModInfoModel(name: this.Name, version: this.MainVersion, previewVersion: this.PreviewVersion, url: this.Url).WithError(this.FetchStatus, this.FetchError);
|
||||
return new ModInfoModel(name: this.Name, version: this.MainVersion, url: this.Url, previewVersion: this.PreviewVersion)
|
||||
.SetLicense(this.LicenseUrl, this.LicenseName)
|
||||
.SetError(this.FetchStatus, this.FetchError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,12 +12,6 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
/*********
|
||||
** Fields
|
||||
*********/
|
||||
/// <summary>The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name.</summary>
|
||||
private readonly string StableReleaseUrlFormat;
|
||||
|
||||
/// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the base URL, where {0} is the organisation and project name.</summary>
|
||||
private readonly string AnyReleaseUrlFormat;
|
||||
|
||||
/// <summary>The underlying HTTP client.</summary>
|
||||
private readonly IClient Client;
|
||||
|
||||
|
@ -27,17 +21,12 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
*********/
|
||||
/// <summary>Construct an instance.</summary>
|
||||
/// <param name="baseUrl">The base URL for the GitHub API.</param>
|
||||
/// <param name="stableReleaseUrlFormat">The URL for a GitHub API query for the latest stable release, excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
|
||||
/// <param name="anyReleaseUrlFormat">The URL for a GitHub API query for the latest release (including prerelease), excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
|
||||
/// <param name="userAgent">The user agent for the API client.</param>
|
||||
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
|
||||
/// <param name="username">The username with which to authenticate to the GitHub API.</param>
|
||||
/// <param name="password">The password with which to authenticate to the GitHub API.</param>
|
||||
public GitHubClient(string baseUrl, string stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
|
||||
public GitHubClient(string baseUrl, string userAgent, string acceptHeader, string username, string password)
|
||||
{
|
||||
this.StableReleaseUrlFormat = stableReleaseUrlFormat;
|
||||
this.AnyReleaseUrlFormat = anyReleaseUrlFormat;
|
||||
|
||||
this.Client = new FluentClient(baseUrl)
|
||||
.SetUserAgent(userAgent)
|
||||
.AddDefault(req => req.WithHeader("Accept", acceptHeader));
|
||||
|
@ -45,25 +34,43 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
this.Client = this.Client.SetBasicAuthentication(username, password);
|
||||
}
|
||||
|
||||
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
|
||||
public async Task<GitRepo> GetRepositoryAsync(string repo)
|
||||
{
|
||||
this.AssertKeyFormat(repo);
|
||||
try
|
||||
{
|
||||
return await this.Client
|
||||
.GetAsync($"repos/{repo}")
|
||||
.As<GitRepo>();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Get the latest release for a GitHub repository.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
|
||||
/// <returns>Returns the release if found, else <c>null</c>.</returns>
|
||||
public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
|
||||
{
|
||||
this.AssetKeyFormat(repo);
|
||||
this.AssertKeyFormat(repo);
|
||||
try
|
||||
{
|
||||
if (includePrerelease)
|
||||
{
|
||||
GitRelease[] results = await this.Client
|
||||
.GetAsync(string.Format(this.AnyReleaseUrlFormat, repo))
|
||||
.GetAsync($"repos/{repo}/releases?per_page=2") // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials)
|
||||
.AsArray<GitRelease>();
|
||||
return results.FirstOrDefault(p => !p.IsDraft);
|
||||
}
|
||||
|
||||
return await this.Client
|
||||
.GetAsync(string.Format(this.StableReleaseUrlFormat, repo))
|
||||
.GetAsync($"repos/{repo}/releases/latest")
|
||||
.As<GitRelease>();
|
||||
}
|
||||
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
|
||||
|
@ -85,7 +92,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
/// <summary>Assert that a repository key is formatted correctly.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <exception cref="ArgumentException">The repository key is invalid.</exception>
|
||||
private void AssetKeyFormat(string repo)
|
||||
private void AssertKeyFormat(string repo)
|
||||
{
|
||||
if (repo == null || !repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>The license info for a GitHub project.</summary>
|
||||
internal class GitLicense
|
||||
{
|
||||
/// <summary>The license display name.</summary>
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>The SPDX ID for the license.</summary>
|
||||
[JsonProperty("spdx_id")]
|
||||
public string SpdxId { get; set; }
|
||||
|
||||
/// <summary>The URL for the license info.</summary>
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
using Newtonsoft.Json;
|
||||
|
||||
namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
||||
{
|
||||
/// <summary>Basic metadata about a GitHub project.</summary>
|
||||
internal class GitRepo
|
||||
{
|
||||
/// <summary>The full repository name, including the owner.</summary>
|
||||
[JsonProperty("full_name")]
|
||||
public string FullName { get; set; }
|
||||
|
||||
/// <summary>The URL to the repository web page, if any.</summary>
|
||||
[JsonProperty("html_url")]
|
||||
public string WebUrl { get; set; }
|
||||
|
||||
/// <summary>The code license, if any.</summary>
|
||||
[JsonProperty("license")]
|
||||
public GitLicense License { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,6 +9,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
|
|||
/*********
|
||||
** Methods
|
||||
*********/
|
||||
/// <summary>Get basic metadata for a GitHub repository, if available.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <returns>Returns the repository info if it exists, else <c>null</c>.</returns>
|
||||
Task<GitRepo> GetRepositoryAsync(string repo);
|
||||
|
||||
/// <summary>Get the latest release for a GitHub repository.</summary>
|
||||
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
|
||||
/// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
|
||||
|
|
|
@ -29,12 +29,6 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
|
|||
/// <summary>The base URL for the GitHub API.</summary>
|
||||
public string GitHubBaseUrl { get; set; }
|
||||
|
||||
/// <summary>The URL for a GitHub API query for the latest stable release, excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
|
||||
public string GitHubStableReleaseUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
|
||||
public string GitHubAnyReleaseUrlFormat { get; set; }
|
||||
|
||||
/// <summary>The Accept header value expected by the GitHub API.</summary>
|
||||
public string GitHubAcceptHeader { get; set; }
|
||||
|
||||
|
|
|
@ -32,21 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint realID))
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Chucklefish mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
var mod = await this.Client.GetModAsync(realID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
|
||||
|
||||
// create model
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
|
||||
return mod != null
|
||||
? new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url)
|
||||
: new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Chucklefish mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -30,36 +30,46 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/// <param name="id">The mod ID in this repository.</param>
|
||||
public override async Task<ModInfoModel> GetModInfoAsync(string id)
|
||||
{
|
||||
ModInfoModel result = new ModInfoModel().SetBasicInfo(id, $"https://github.com/{id}/releases");
|
||||
|
||||
// validate ID format
|
||||
if (!id.Contains("/") || id.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != id.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid GitHub mod ID, must be a username and project name like 'Pathoschild/LookupAnything'.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
// fetch repo info
|
||||
GitRepo repository = await this.Client.GetRepositoryAsync(id);
|
||||
if (repository == null)
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub repository for this ID.");
|
||||
result
|
||||
.SetBasicInfo(repository.FullName, $"{repository.WebUrl}/releases")
|
||||
.SetLicense(url: repository.License?.Url, name: repository.License?.Name);
|
||||
|
||||
// get latest release (whether preview or stable)
|
||||
GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
|
||||
if (latest == null)
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID.");
|
||||
return result.SetError(RemoteModStatus.DoesNotExist, "Found no GitHub release for this ID.");
|
||||
|
||||
// split stable/prerelease if applicable
|
||||
GitRelease preview = null;
|
||||
if (latest.IsPrerelease)
|
||||
{
|
||||
GitRelease result = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
|
||||
if (result != null)
|
||||
GitRelease release = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
|
||||
if (release != null)
|
||||
{
|
||||
preview = latest;
|
||||
latest = result;
|
||||
latest = release;
|
||||
}
|
||||
}
|
||||
|
||||
// return data
|
||||
return new ModInfoModel(name: id, version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
|
||||
return result.SetVersions(version: this.NormaliseVersion(latest.Tag), previewVersion: this.NormaliseVersion(preview?.Tag));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
return result.SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,19 +32,19 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
{
|
||||
// validate ID format
|
||||
if (!long.TryParse(id, out long modDropID))
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid ModDrop mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
ModDropMod mod = await this.Client.GetModAsync(modDropID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID.");
|
||||
return new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url);
|
||||
return mod != null
|
||||
? new ModInfoModel(name: mod.Name, version: mod.LatestDefaultVersion?.ToString(), previewVersion: mod.LatestOptionalVersion?.ToString(), url: mod.Url)
|
||||
: new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no ModDrop mod with this ID.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,12 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/// <summary>The mod's web URL.</summary>
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>The license URL, if available.</summary>
|
||||
public string LicenseUrl { get; set; }
|
||||
|
||||
/// <summary>The license name, if available.</summary>
|
||||
public string LicenseName { get; set; }
|
||||
|
||||
/// <summary>The mod availability status on the remote site.</summary>
|
||||
public RemoteModStatus Status { get; set; } = RemoteModStatus.Ok;
|
||||
|
||||
|
@ -37,17 +43,49 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
|
||||
/// <param name="url">The mod's web URL.</param>
|
||||
public ModInfoModel(string name, string version, string url, string previewVersion = null)
|
||||
{
|
||||
this
|
||||
.SetBasicInfo(name, url)
|
||||
.SetVersions(version, previewVersion);
|
||||
}
|
||||
|
||||
/// <summary>Set the basic mod info.</summary>
|
||||
/// <param name="name">The mod name.</param>
|
||||
/// <param name="url">The mod's web URL.</param>
|
||||
public ModInfoModel SetBasicInfo(string name, string url)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Url = url;
|
||||
|
||||
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>
|
||||
public ModInfoModel SetVersions(string version, string previewVersion = null)
|
||||
{
|
||||
this.Version = version;
|
||||
this.PreviewVersion = previewVersion;
|
||||
this.Url = url;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set the license info, if available.</summary>
|
||||
/// <param name="url">The license URL.</param>
|
||||
/// <param name="name">The license name.</param>
|
||||
public ModInfoModel SetLicense(string url, string name)
|
||||
{
|
||||
this.LicenseUrl = url;
|
||||
this.LicenseName = name;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Set a mod error.</summary>
|
||||
/// <param name="status">The mod availability status on the remote site.</param>
|
||||
/// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
|
||||
public ModInfoModel WithError(RemoteModStatus status, string error)
|
||||
public ModInfoModel SetError(RemoteModStatus status, string error)
|
||||
{
|
||||
this.Status = status;
|
||||
this.Error = error;
|
||||
|
|
|
@ -32,27 +32,27 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
|
|||
{
|
||||
// validate ID format
|
||||
if (!uint.TryParse(id, out uint nexusID))
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, $"The value '{id}' isn't a valid Nexus mod ID, must be an integer ID.");
|
||||
|
||||
// fetch info
|
||||
try
|
||||
{
|
||||
NexusMod mod = await this.Client.GetModAsync(nexusID);
|
||||
if (mod == null)
|
||||
return new ModInfoModel().WithError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
|
||||
return new ModInfoModel().SetError(RemoteModStatus.DoesNotExist, "Found no Nexus mod with this ID.");
|
||||
if (mod.Error != null)
|
||||
{
|
||||
RemoteModStatus remoteStatus = mod.Status == NexusModStatus.Hidden || mod.Status == NexusModStatus.NotPublished
|
||||
? RemoteModStatus.DoesNotExist
|
||||
: RemoteModStatus.TemporaryError;
|
||||
return new ModInfoModel().WithError(remoteStatus, mod.Error);
|
||||
return new ModInfoModel().SetError(remoteStatus, mod.Error);
|
||||
}
|
||||
|
||||
return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), previewVersion: mod.LatestFileVersion?.ToString(), url: mod.Url);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new ModInfoModel().WithError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
return new ModInfoModel().SetError(RemoteModStatus.TemporaryError, ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -121,8 +121,6 @@ namespace StardewModdingAPI.Web
|
|||
|
||||
services.AddSingleton<IGitHubClient>(new GitHubClient(
|
||||
baseUrl: api.GitHubBaseUrl,
|
||||
stableReleaseUrlFormat: api.GitHubStableReleaseUrlFormat,
|
||||
anyReleaseUrlFormat: api.GitHubAnyReleaseUrlFormat,
|
||||
userAgent: userAgent,
|
||||
acceptHeader: api.GitHubAcceptHeader,
|
||||
username: api.GitHubUsername,
|
||||
|
|
|
@ -30,8 +30,6 @@
|
|||
"ChucklefishModPageUrlFormat": "resources/{0}",
|
||||
|
||||
"GitHubBaseUrl": "https://api.github.com",
|
||||
"GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest",
|
||||
"GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=2", // allow for draft release (only visible if GitHub repo is owned by same account as the update check credentials)
|
||||
"GitHubAcceptHeader": "application/vnd.github.v3+json",
|
||||
"GitHubUsername": null, // see top note
|
||||
"GitHubPassword": null, // see top note
|
||||
|
|
Loading…
Reference in New Issue