mirror of
https://github.com/Lidarr/Lidarr
synced 2025-12-06 08:25:54 +01:00
Improved PluginService with tree support
This commit is contained in:
parent
53132987a1
commit
f46acd61c8
5 changed files with 334 additions and 81 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
|
|
@ -18,21 +17,6 @@ public class PluginResource : RestResource
|
||||||
|
|
||||||
public static class PluginResourceMapper
|
public static class PluginResourceMapper
|
||||||
{
|
{
|
||||||
private static string FormatVersion(Version version)
|
|
||||||
{
|
|
||||||
if (version == null)
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always show 4-part version for UI consistency, handling undefined components
|
|
||||||
var major = version.Major;
|
|
||||||
var minor = version.Minor;
|
|
||||||
var build = version.Build == -1 ? 0 : version.Build;
|
|
||||||
var revision = version.Revision == -1 ? 0 : version.Revision;
|
|
||||||
return $"{major}.{minor}.{build}.{revision}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PluginResource ToResource(this IPlugin plugin)
|
public static PluginResource ToResource(this IPlugin plugin)
|
||||||
{
|
{
|
||||||
return new PluginResource
|
return new PluginResource
|
||||||
|
|
@ -40,8 +24,8 @@ public static PluginResource ToResource(this IPlugin plugin)
|
||||||
Name = plugin.Name,
|
Name = plugin.Name,
|
||||||
Owner = plugin.Owner,
|
Owner = plugin.Owner,
|
||||||
GithubUrl = plugin.GithubUrl,
|
GithubUrl = plugin.GithubUrl,
|
||||||
InstalledVersion = FormatVersion(plugin.InstalledVersion),
|
InstalledVersion = plugin.InstalledVersion.ToFormattedString(),
|
||||||
AvailableVersion = FormatVersion(plugin.AvailableVersion),
|
AvailableVersion = plugin.AvailableVersion.ToFormattedString(),
|
||||||
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
|
@ -39,7 +38,7 @@ public InstallPluginService(IPluginService pluginService,
|
||||||
|
|
||||||
public void Execute(UninstallPluginCommand message)
|
public void Execute(UninstallPluginCommand message)
|
||||||
{
|
{
|
||||||
var (owner, name) = _pluginService.ParseUrl(message.GithubUrl);
|
var (owner, name, tree) = _pluginService.ParseRepositoryInput(message.GithubUrl);
|
||||||
|
|
||||||
// Get installed version before uninstalling
|
// Get installed version before uninstalling
|
||||||
var installedPlugins = _pluginService.GetInstalledPlugins();
|
var installedPlugins = _pluginService.GetInstalledPlugins();
|
||||||
|
|
@ -79,7 +78,7 @@ private void InstallPlugin(RemotePlugin package)
|
||||||
_logger.ProgressInfo($"Plugin [{package.Owner}/{package.Name}] v{package.Version} installed. Please restart Lidarr.");
|
_logger.ProgressInfo($"Plugin [{package.Owner}/{package.Name}] v{package.Version} installed. Please restart Lidarr.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UninstallPlugin(string owner, string name, Version version)
|
private void UninstallPlugin(string owner, string name, PluginVersion version)
|
||||||
{
|
{
|
||||||
_logger.ProgressInfo($"Uninstalling plugin [{owner}/{name}]");
|
_logger.ProgressInfo($"Uninstalling plugin [{owner}/{name}]");
|
||||||
var pluginFolder = Path.Combine(PluginFolder(), owner, name);
|
var pluginFolder = Path.Combine(PluginFolder(), owner, name);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System.Reflection;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Plugins
|
namespace NzbDrone.Core.Plugins
|
||||||
{
|
{
|
||||||
|
|
@ -7,18 +8,41 @@ public interface IPlugin
|
||||||
string Name { get; }
|
string Name { get; }
|
||||||
string Owner { get; }
|
string Owner { get; }
|
||||||
string GithubUrl { get; }
|
string GithubUrl { get; }
|
||||||
Version InstalledVersion { get; }
|
PluginVersion InstalledVersion { get; }
|
||||||
Version AvailableVersion { get; set; }
|
PluginVersion AvailableVersion { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class Plugin : IPlugin
|
public abstract class Plugin : IPlugin
|
||||||
{
|
{
|
||||||
|
private PluginVersion _installedVersion;
|
||||||
|
|
||||||
public virtual string Name { get; }
|
public virtual string Name { get; }
|
||||||
public virtual string Owner { get; }
|
public virtual string Owner { get; }
|
||||||
public virtual string GithubUrl { get; }
|
public virtual string GithubUrl { get; }
|
||||||
|
|
||||||
public Version InstalledVersion => GetType().Assembly.GetName().Version;
|
public PluginVersion InstalledVersion
|
||||||
public Version AvailableVersion { get; set; }
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_installedVersion != null)
|
||||||
|
{
|
||||||
|
return _installedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
var informationalVersion = GetType().Assembly
|
||||||
|
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
|
||||||
|
.InformationalVersion;
|
||||||
|
|
||||||
|
if (informationalVersion.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
_installedVersion = PluginVersion.Parse(informationalVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _installedVersion ??= GetType().Assembly.GetName().Version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginVersion AvailableVersion { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class RemotePlugin
|
public class RemotePlugin
|
||||||
|
|
@ -26,7 +50,8 @@ public class RemotePlugin
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public string Owner { get; set; }
|
public string Owner { get; set; }
|
||||||
public string GithubUrl { get; set; }
|
public string GithubUrl { get; set; }
|
||||||
public Version Version { get; set; }
|
public PluginVersion Version { get; set; }
|
||||||
public string PackageUrl { get; set; }
|
public string PackageUrl { get; set; }
|
||||||
|
public string Tree { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,90 +11,115 @@ namespace NzbDrone.Core.Plugins
|
||||||
{
|
{
|
||||||
public interface IPluginService
|
public interface IPluginService
|
||||||
{
|
{
|
||||||
(string, string) ParseUrl(string repoUrl);
|
(string Owner, string Name, string Tree) ParseRepositoryInput(string input);
|
||||||
RemotePlugin GetRemotePlugin(string repoUrl);
|
RemotePlugin GetRemotePlugin(string input);
|
||||||
List<IPlugin> GetInstalledPlugins();
|
List<IPlugin> GetInstalledPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PluginService : IPluginService
|
public partial class PluginService : IPluginService
|
||||||
{
|
{
|
||||||
private static readonly Regex RepoRegex = new Regex(@"https://github.com/(?<owner>[^/]*)/(?<name>[^/]*)", RegexOptions.Compiled);
|
|
||||||
private static readonly Regex MinVersionRegex = new Regex(@"Minimum Lidarr Version: (?<version>\d+\.\d+\.\d+\.\d+)", RegexOptions.Compiled);
|
|
||||||
|
|
||||||
private readonly IHttpClient _httpClient;
|
private readonly IHttpClient _httpClient;
|
||||||
|
private readonly IPlatformInfo _platformInfo;
|
||||||
private readonly List<IPlugin> _installedPlugins;
|
private readonly List<IPlugin> _installedPlugins;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public PluginService(IHttpClient httpClient,
|
public PluginService(
|
||||||
|
IHttpClient httpClient,
|
||||||
|
IPlatformInfo platformInfo,
|
||||||
IEnumerable<IPlugin> installedPlugins,
|
IEnumerable<IPlugin> installedPlugins,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_installedPlugins = installedPlugins.ToList();
|
_platformInfo = platformInfo;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_installedPlugins = installedPlugins?.ToList() ?? new List<IPlugin>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public (string, string) ParseUrl(string repoUrl)
|
private string Framework => $"net{_platformInfo.Version.Major}.0";
|
||||||
{
|
|
||||||
var match = RepoRegex.Match(repoUrl);
|
|
||||||
|
|
||||||
if (!match.Success)
|
public RemotePlugin GetRemotePlugin(string input)
|
||||||
{
|
|
||||||
_logger.Warn("Invalid plugin repo URL");
|
|
||||||
return (null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
var owner = match.Groups["owner"].Value;
|
|
||||||
var name = match.Groups["name"].Value;
|
|
||||||
|
|
||||||
return (owner, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public RemotePlugin GetRemotePlugin(string repoUrl)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (owner, name) = ParseUrl(repoUrl);
|
var (owner, name, tree) = ParseRepositoryInput(input);
|
||||||
var releaseUrl = $"https://api.github.com/repos/{owner}/{name}/releases";
|
if (string.IsNullOrWhiteSpace(owner))
|
||||||
|
|
||||||
var releases = _httpClient.Get<List<Release>>(new HttpRequest(releaseUrl)).Resource;
|
|
||||||
|
|
||||||
if (!releases?.Any() ?? true)
|
|
||||||
{
|
{
|
||||||
_logger.Warn($"No releases found for {name}");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = releases.OrderByDescending(x => x.PublishedAt).FirstOrDefault(x => IsSupported(x));
|
_logger.Trace($"Fetching releases for {owner}/{name}" + (tree != null ? $" on tree '{tree}'" : ""));
|
||||||
|
|
||||||
if (latest == null)
|
var releasesUrl = $"https://api.github.com/repos/{owner}/{name}/releases";
|
||||||
|
var releases = _httpClient.Get<List<Release>>(new HttpRequest(releasesUrl)).Resource;
|
||||||
|
|
||||||
|
if (releases?.Any() != true)
|
||||||
{
|
{
|
||||||
_logger.Warn($"Plugin {name} requires newer version of Lidarr");
|
_logger.Warn($"No releases found for {owner}/{name}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var version = Version.Parse(latest.TagName.TrimStart('v'));
|
_logger.Trace($"Found {releases.Count} total releases, filtering for framework {Framework}" + (tree != null ? $" and tree '{tree}'" : ""));
|
||||||
var framework = "net8.0";
|
|
||||||
var asset = latest.Assets.FirstOrDefault(x => x.Name.EndsWith($"{framework}.zip"));
|
var compatibleReleases = releases
|
||||||
|
.Where(r => !r.Draft &&
|
||||||
|
(IsMatchingTree(r.TargetCommitish, tree) || IsMatchingTree(r.TagName, tree)) &&
|
||||||
|
HasCompatibleAsset(r, Framework) && MeetsMinimumVersion(r.Body))
|
||||||
|
.OrderByDescending(r => r.PublishedAt)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
_logger.Trace($"Found {compatibleReleases.Count} compatible releases after filtering");
|
||||||
|
|
||||||
|
var release = compatibleReleases.FirstOrDefault();
|
||||||
|
if (release == null)
|
||||||
|
{
|
||||||
|
_logger.Warn($"No compatible release found for {name} with framework {Framework}" +
|
||||||
|
(tree != null ? $" and tree {tree}" : ""));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var releaseUrl = release.HtmlUrl;
|
||||||
|
var urlMatch = RepoUrlExtractorRegex().Match(releaseUrl);
|
||||||
|
if (urlMatch.Success)
|
||||||
|
{
|
||||||
|
owner = urlMatch.Groups["owner"].Value;
|
||||||
|
name = urlMatch.Groups["n"].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetTree = release.TargetCommitish;
|
||||||
|
var isDefaultTree = PluginVersion.IsDefaultTree(targetTree);
|
||||||
|
var tag = release.TagName;
|
||||||
|
|
||||||
|
var version = isDefaultTree
|
||||||
|
? ParseFullVersion(tag)
|
||||||
|
: new PluginVersion(ParseVersionFromTag(tag), targetTree);
|
||||||
|
|
||||||
|
var actualTree = isDefaultTree ? null : targetTree;
|
||||||
|
|
||||||
|
var asset = release.Assets.FirstOrDefault(a =>
|
||||||
|
a.Name.Contains($"{Framework}.zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (asset == null)
|
if (asset == null)
|
||||||
{
|
{
|
||||||
_logger.Warn($"No plugin package found for {framework} for {name}");
|
_logger.Warn($"No asset found matching {Framework}.zip for {name}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var githubUrl = $"https://github.com/{owner}/{name}" + (actualTree != null ? $"/tree/{actualTree}" : "");
|
||||||
|
|
||||||
|
_logger.Info($"Found plugin {owner}/{name} v{version} from tree '{actualTree ?? "default"}' with asset {asset.Name}");
|
||||||
return new RemotePlugin
|
return new RemotePlugin
|
||||||
{
|
{
|
||||||
GithubUrl = repoUrl,
|
GithubUrl = githubUrl,
|
||||||
Name = name,
|
Name = name,
|
||||||
Owner = owner,
|
Owner = owner,
|
||||||
Version = version,
|
Version = version,
|
||||||
PackageUrl = asset.BrowserDownloadUrl
|
PackageUrl = asset.BrowserDownloadUrl,
|
||||||
|
Tree = actualTree
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Warn(ex, "Unable to get remote plugin information for {0}", repoUrl);
|
_logger.Error(ex, $"Failed to get remote plugin for {input}");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,30 +131,128 @@ public List<IPlugin> GetInstalledPlugins()
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var remote = GetRemotePlugin(plugin.GithubUrl);
|
var remote = GetRemotePlugin(plugin.GithubUrl);
|
||||||
if (remote != null)
|
plugin.AvailableVersion = remote?.Version ?? new PluginVersion(new Version(0, 0, 0, 0), "unavailable");
|
||||||
{
|
|
||||||
plugin.AvailableVersion = remote.Version;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Warn(ex, "Unable to check for updates for plugin {0}/{1}", plugin.Owner, plugin.Name);
|
_logger.Warn(ex, $"Unable to check updates for {plugin.Owner}/{plugin.Name}");
|
||||||
|
plugin.AvailableVersion = new PluginVersion(new Version(0, 0, 0, 0), "unavailable");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return _installedPlugins;
|
return _installedPlugins;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsSupported(Release release)
|
public (string Owner, string Name, string Tree) ParseRepositoryInput(string input)
|
||||||
{
|
{
|
||||||
var match = MinVersionRegex.Match(release.Body);
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
if (match.Success)
|
|
||||||
{
|
{
|
||||||
var minVersion = Version.Parse(match.Groups["version"].Value);
|
_logger.Error("Repository input cannot be empty");
|
||||||
return minVersion <= BuildInfo.Version;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input = input.Trim();
|
||||||
|
|
||||||
|
string owner = null;
|
||||||
|
string name = null;
|
||||||
|
string tree = null;
|
||||||
|
|
||||||
|
var urlMatch = GitHubUrlRegex().Match(input);
|
||||||
|
if (urlMatch.Success)
|
||||||
|
{
|
||||||
|
name = urlMatch.Groups["n"].Value;
|
||||||
|
return (
|
||||||
|
urlMatch.Groups["owner"].Value,
|
||||||
|
name.EndsWith(".git", StringComparison.OrdinalIgnoreCase) ? name[..^4] : name,
|
||||||
|
urlMatch.Groups["tree"].Success ? urlMatch.Groups["tree"].Value : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameTokens = new List<string>();
|
||||||
|
|
||||||
|
foreach (var token in input.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
{
|
||||||
|
var parts = TagSplitterRegex().Split(token).Where(p => !string.IsNullOrWhiteSpace(p)).ToArray();
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.Length; i++)
|
||||||
|
{
|
||||||
|
switch (parts[i])
|
||||||
|
{
|
||||||
|
case "@" when i + 1 < parts.Length:
|
||||||
|
owner = parts[++i];
|
||||||
|
break;
|
||||||
|
case "#" when i + 1 < parts.Length:
|
||||||
|
tree = parts[++i];
|
||||||
|
break;
|
||||||
|
case not "@" and not "#":
|
||||||
|
nameTokens.Add(parts[i]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
name = nameTokens.Count > 0 ? string.Concat(nameTokens).Trim() : null;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(owner) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (owner, name, tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsMatchingTree(string targetTree, string requestedTree) =>
|
||||||
|
requestedTree == null
|
||||||
|
? PluginVersion.IsDefaultTree(targetTree)
|
||||||
|
: targetTree.Equals(requestedTree, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
private static bool HasCompatibleAsset(Release release, string framework)
|
||||||
|
=> release.Assets.Any(a => a.Name.Contains($"{framework}.zip", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
private static PluginVersion ParseFullVersion(string tag)
|
||||||
|
{
|
||||||
|
var match = VersionWithPrereleaseRegex().Match(tag.TrimStart('v'));
|
||||||
|
return match.Success
|
||||||
|
? new PluginVersion(
|
||||||
|
Version.Parse(match.Groups["version"].Value),
|
||||||
|
match.Groups["prerelease"].Success ? match.Groups["prerelease"].Value : null)
|
||||||
|
: new PluginVersion(ParseVersionFromTag(tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version ParseVersionFromTag(string tag)
|
||||||
|
{
|
||||||
|
var match = VersionOnlyRegex().Match(tag.TrimStart('v'));
|
||||||
|
return match.Success && Version.TryParse(match.Groups[1].Value, out var version)
|
||||||
|
? version
|
||||||
|
: new Version(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool MeetsMinimumVersion(string body)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(body))
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var match = MinimumVersionRegex().Match(body);
|
||||||
|
return !match.Success || Version.Parse(match.Groups["version"].Value) <= BuildInfo.Version;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^https?://github\.com/(?<owner>[^/]+)/(?<n>[^/\s]+)(?:/tree/(?<tree>[^/\s]+))?(?:\.git)?/?$", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||||
|
private static partial Regex GitHubUrlRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"([@#])", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex TagSplitterRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"github\.com/(?<owner>[^/]+)/(?<n>[^/]+)/", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex RepoUrlExtractorRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"Minimum\s+Lidarr\s+Version:?\s*(?:\*\*)?[\s]*(?<version>\d+\.\d+\.\d+\.\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled)]
|
||||||
|
private partial Regex MinimumVersionRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^(?<version>\d+\.\d+\.\d+(?:\.\d+)?)(?:-(?<prerelease>.+))?$", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex VersionWithPrereleaseRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex(@"^(\d+\.\d+\.\d+(?:\.\d+)?)", RegexOptions.Compiled)]
|
||||||
|
private static partial Regex VersionOnlyRegex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
122
src/NzbDrone.Core/Plugins/PluginVersion.cs
Normal file
122
src/NzbDrone.Core/Plugins/PluginVersion.cs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Plugins
|
||||||
|
{
|
||||||
|
public class PluginVersion : IComparable<PluginVersion>, IEquatable<PluginVersion>
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyCollection<string> DefaultTreeNames = new[] { "master", "main" };
|
||||||
|
|
||||||
|
public Version BaseVersion { get; }
|
||||||
|
public string Suffix { get; }
|
||||||
|
public bool HasSuffix => Suffix != null;
|
||||||
|
|
||||||
|
public PluginVersion(Version version, string suffix = null)
|
||||||
|
{
|
||||||
|
BaseVersion = version ?? throw new ArgumentNullException(nameof(version));
|
||||||
|
Suffix = CleanSuffix(suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsDefaultTree(string treeName) =>
|
||||||
|
DefaultTreeNames.Contains(treeName, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static PluginVersion Parse(string versionString)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(versionString))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var parts = versionString.TrimStart('v').Split('-', 2);
|
||||||
|
return Version.TryParse(parts[0], out var version) ? new PluginVersion(version, parts.Length > 1 ? parts[1] : null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() =>
|
||||||
|
HasSuffix ? $"{BaseVersion}-{Suffix}" : BaseVersion.ToString();
|
||||||
|
|
||||||
|
public string ToFormattedString()
|
||||||
|
{
|
||||||
|
var build = BaseVersion.Build == -1 ? 0 : BaseVersion.Build;
|
||||||
|
var revision = BaseVersion.Revision == -1 ? 0 : BaseVersion.Revision;
|
||||||
|
var baseVersionString = $"{BaseVersion.Major}.{BaseVersion.Minor}.{build}.{revision}";
|
||||||
|
|
||||||
|
return HasSuffix && !IsDefaultTree(Suffix)
|
||||||
|
? $"{baseVersionString} ({Suffix})"
|
||||||
|
: baseVersionString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(PluginVersion other)
|
||||||
|
{
|
||||||
|
if (other is null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseComp = BaseVersion.CompareTo(other.BaseVersion);
|
||||||
|
if (baseComp != 0)
|
||||||
|
{
|
||||||
|
return baseComp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Suffix, other.Suffix) switch
|
||||||
|
{
|
||||||
|
(null, not null) => 1,
|
||||||
|
(not null, null) => -1,
|
||||||
|
_ => string.Compare(Suffix, other.Suffix, StringComparison.OrdinalIgnoreCase)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(PluginVersion other) =>
|
||||||
|
other is not null &&
|
||||||
|
(ReferenceEquals(this, other) ||
|
||||||
|
(BaseVersion.Equals(other.BaseVersion) &&
|
||||||
|
string.Equals(Suffix, other.Suffix, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
|
public override bool Equals(object obj) =>
|
||||||
|
obj is PluginVersion other && Equals(other);
|
||||||
|
|
||||||
|
public override int GetHashCode() =>
|
||||||
|
HashCode.Combine(BaseVersion, Suffix?.ToUpperInvariant());
|
||||||
|
|
||||||
|
public static bool operator ==(PluginVersion left, PluginVersion right) =>
|
||||||
|
left is null ? right is null : left.Equals(right);
|
||||||
|
|
||||||
|
public static bool operator !=(PluginVersion left, PluginVersion right) =>
|
||||||
|
!(left == right);
|
||||||
|
|
||||||
|
public static bool operator <(PluginVersion left, PluginVersion right) =>
|
||||||
|
left is null ? right is not null : left.CompareTo(right) < 0;
|
||||||
|
|
||||||
|
public static bool operator <=(PluginVersion left, PluginVersion right) =>
|
||||||
|
left is null || left.CompareTo(right) <= 0;
|
||||||
|
|
||||||
|
public static bool operator >(PluginVersion left, PluginVersion right) =>
|
||||||
|
left is not null && left.CompareTo(right) > 0;
|
||||||
|
|
||||||
|
public static bool operator >=(PluginVersion left, PluginVersion right) =>
|
||||||
|
left is null ? right is null : left.CompareTo(right) >= 0;
|
||||||
|
|
||||||
|
public static implicit operator PluginVersion(Version version) =>
|
||||||
|
version is null ? null : new PluginVersion(version);
|
||||||
|
|
||||||
|
public static explicit operator Version(PluginVersion pluginVersion)
|
||||||
|
=> pluginVersion?.BaseVersion;
|
||||||
|
|
||||||
|
private static string CleanSuffix(string suffix)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(suffix))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plusIndex = suffix.IndexOf('+', StringComparison.OrdinalIgnoreCase);
|
||||||
|
if (plusIndex >= 0)
|
||||||
|
{
|
||||||
|
suffix = suffix[..plusIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(suffix) ? null : suffix.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue