Skip to content
Open

dev #133

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,20 @@ jobs:
build-and-test:
runs-on: windows-latest

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,20 @@ jobs:
build-and-release:
runs-on: windows-latest

env:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Cache NuGet packages
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }}
restore-keys: |
${{ runner.os }}-nuget-

- name: Extract version from tag
id: version
shell: pwsh
Expand Down
15 changes: 15 additions & 0 deletions CollectionManager.App.Cli/CliConstants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace CollectionManager.App.Cli;

internal static class CliConstants
{
/// <summary>
/// Standard separators for splitting CLI input values.
/// Used for parsing comma or whitespace separated lists.
/// </summary>
public static readonly char[] ValueSeparator = [' ', ',', '\n', '\r', '\t'];

/// <summary>
/// Separators for simple value lists (spaces and commas only).
/// </summary>
public static readonly char[] SimpleValueSeparator = [' ', ','];
}
5 changes: 5 additions & 0 deletions CollectionManager.App.Cli/CollectionManager.App.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<AssemblyTitle>osu! Collection Manager CLI</AssemblyTitle>
<Copyright>Copyright © 2017-present Piotrekol</Copyright>
<PackageId>CollectionManager.App.Cli</PackageId>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand All @@ -13,5 +14,9 @@

<ItemGroup>
<PackageReference Include="CommandLineParser" />
<PackageReference Include="Serilog" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Extensions.Logging" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
</Project>
34 changes: 34 additions & 0 deletions CollectionManager.App.Cli/Commands/ConvertCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace CollectionManager.App.Cli.Commands;

using CollectionManager.App.Cli.Pipeline;
using CollectionManager.Core.Modules.FileIo.FileCollections;
using CommandLine;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

[Verb("convert", HelpText = "Convert collection files between formats (.db/.osdb/.realm)")]
internal sealed partial class ConvertCommand : PipelineOptions, IPipelineCommand
{
private readonly ILogger _logger = Program.Logger;

[Option('i', "input", Required = true, HelpText = "Input collection file (.db/.osdb/.realm)")]
public required string InputFile { get; init; }

[Option('o', "output", Required = true, HelpText = "Output .db/.osdb/.realm file")]
public override string? OutputFile { get; init; }
public override Task<int> RunAsync(CollectionContext context)
{
_ = context.EnsureOsuDatabaseLoaded(this);
LogConvertingCollections();
CollectionLoadResult loaded = context.LoadCollectionsFromFile(InputFile);
LogLoadedCollections(loaded.Collections.Count, InputFile);

return Task.FromResult(0);
}

[LoggerMessage(Level = LogLevel.Information, Message = "Converting collections.")]
private partial void LogConvertingCollections();

[LoggerMessage(Level = LogLevel.Information, Message = "Loaded {Count} collection(s) from {Path}")]
private partial void LogLoadedCollections(int count, string path);
}
116 changes: 116 additions & 0 deletions CollectionManager.App.Cli/Commands/CreateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
namespace CollectionManager.App.Cli.Commands;

using CollectionManager.App.Cli;
using CollectionManager.App.Cli.Logging;
using CollectionManager.App.Cli.Pipeline;
using CollectionManager.Core.Modules.Collection;
using CollectionManager.Core.Types;
using CommandLine;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

[Verb("create", HelpText = "Create collection from beatmap IDs or hashes")]
internal sealed partial class CreateCommand : PipelineOptions
{
private readonly ILogger _logger = Program.Logger;

[Option('i', "ids", Required = false, HelpText = "Comma or whitespace separated beatmap IDs. Can be path to file.")]
public required string BeatmapIds { get; init; }

[Option('h', "hashes", Required = false, HelpText = "Comma or whitespace separated beatmap hashes (MD5). Can be path to file.")]
public required string Hashes { get; init; }

public override Task<int> RunAsync(CollectionContext context)
{
string? input = GetInput();

if (input is null)
{
return Task.FromResult(1);
}

_ = context.EnsureOsuDatabaseLoaded(this);
LogCreatingCollections();
string[] idOrHashArray = input.Split(CliConstants.ValueSeparator, StringSplitOptions.RemoveEmptyEntries);
OsuCollection collection = !string.IsNullOrWhiteSpace(BeatmapIds)
? ProcessBeatmapIds(context, idOrHashArray)
: ProcessHashes(context, idOrHashArray);

CollectionEditArgs args = CollectionEditArgs.AddCollections([collection]);
context.Manager.EditCollection(args);

string collectionIdentifier = CollectionLogger.FormatCollection(collection);
LogCreatedFromEntries(collectionIdentifier, idOrHashArray.Length);

return Task.FromResult(0);
}

private string? GetInput()
{
bool hasBeatmapIds = !string.IsNullOrWhiteSpace(BeatmapIds);
bool hasHashes = !string.IsNullOrWhiteSpace(Hashes);

if (!hasBeatmapIds && !hasHashes)
{
LogBeatmapIdsOrHashesRequired();
return default;
}

if (hasBeatmapIds && hasHashes)
{
LogBeatmapIdsAndHashesMutuallyExclusive();
return default;
}

if (hasBeatmapIds)
{
return BeatmapIds;
}

return Hashes;
}

private static OsuCollection ProcessBeatmapIds(CollectionContext context, string[] beatmapIdArray)
{
OsuCollection collection = new(context.LoadedMaps) { Name = "from mapIds" };

foreach (string beatmapId in beatmapIdArray)
{
if (int.TryParse(beatmapId.Trim(), out int id))
{
collection.AddBeatmapByMapId(id);
}
}

return collection;
}

private static OsuCollection ProcessHashes(CollectionContext context, string[] hashArray)
{
OsuCollection collection = new(context.LoadedMaps) { Name = "from hashes" };

foreach (string hash in hashArray)
{
string trimmedHash = hash.Trim();

if (!string.IsNullOrWhiteSpace(trimmedHash))
{
collection.AddBeatmapByHash(trimmedHash);
}
}

return collection;
}

[LoggerMessage(Level = LogLevel.Error, Message = "Either --ids or --hashes must be provided.")]
private partial void LogBeatmapIdsOrHashesRequired();

[LoggerMessage(Level = LogLevel.Error, Message = "--ids and --hashes cannot be used together.")]
private partial void LogBeatmapIdsAndHashesMutuallyExclusive();

[LoggerMessage(Level = LogLevel.Information, Message = "Creating collections.")]
private partial void LogCreatingCollections();

[LoggerMessage(Level = LogLevel.Information, Message = "Created collection {Collection} from {Count} entries.")]
private partial void LogCreatedFromEntries(string collection, int count);
}
62 changes: 62 additions & 0 deletions CollectionManager.App.Cli/Commands/DifferenceCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
namespace CollectionManager.App.Cli.Commands;

using CollectionManager.App.Cli.Logging;
using CollectionManager.App.Cli.Pipeline;
using CollectionManager.Core.Modules.Collection;
using CollectionManager.Core.Types;
using CommandLine;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;

[Verb("difference", HelpText = "Find difference between collections (beatmaps that are present in only one collection).")]
internal sealed partial class DifferenceCommand : PipelineOptions, IPipelineCommand
{
private readonly ILogger _logger = Program.Logger;

[Option('i', "ids", Required = true, HelpText = "Collection Ids to compare (comma or space separated).")]
public required IEnumerable<int> Ids { get; init; }

[Option('n', "name", Required = true, HelpText = "Name for the created collection.")]
public required string NewName { get; init; }

public override Task<int> RunAsync(CollectionContext context)
{
List<int> collectionIds = [.. Ids];

if (collectionIds.Count < 2)
{
LogAtLeastTwoIdsRequired();

return Task.FromResult(1);
}

IEnumerable<IOsuCollection> collections = context.Manager.GetCollectionsById(collectionIds);
HashSet<int> foundIds = [.. collections.Select(c => c.Id)];
List<int> missingIds = [.. collectionIds.Where(id => !foundIds.Contains(id))];

if (missingIds.Count > 0)
{
LogCollectionIdsNotFound(string.Join(", ", missingIds));

return Task.FromResult(1);
}

List<string> names = [.. collections.Select(c => c.Name)];
string newCollectionName = context.Manager.GetValidCollectionName(NewName);
CollectionEditArgs args = CollectionEditArgs.DifferenceCollections(names, newCollectionName);
context.Manager.EditCollection(args);

LogCollectionsDifferenced(names.Count, CollectionLogger.FormatCollection(context.Manager.GetCollectionByName(newCollectionName)));
return Task.FromResult(0);
}

[LoggerMessage(Level = LogLevel.Error, Message = "At least 2 Ids are required for difference.")]
private partial void LogAtLeastTwoIdsRequired();

[LoggerMessage(Level = LogLevel.Error, Message = "Collection Id(s) not found: {MissingIds}")]
private partial void LogCollectionIdsNotFound(string missingIds);

[LoggerMessage(Level = LogLevel.Information, Message = "Found difference of {Count} collection(s) into {DifferenceCollection}")]
private partial void LogCollectionsDifferenced(int count, string differenceCollection);
}
50 changes: 50 additions & 0 deletions CollectionManager.App.Cli/Commands/DuplicateCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace CollectionManager.App.Cli.Commands;

using CollectionManager.App.Cli.Logging;
using CollectionManager.App.Cli.Pipeline;
using CollectionManager.Core.Modules.Collection;
using CollectionManager.Core.Types;
using CommandLine;
using Microsoft.Extensions.Logging;
using System.Linq;
using System.Threading.Tasks;

[Verb("duplicate", HelpText = "Duplicate a collection.")]
internal sealed partial class DuplicateCommand : PipelineOptions, IPipelineCommand
{
private readonly ILogger _logger = Program.Logger;

[Option('i', "id", Required = true, HelpText = "Collection Id to duplicate.")]
public required int Id { get; init; }

[Option('n', "name", Required = true, HelpText = "Name for the duplicated collection.")]
public required string NewName { get; init; }

public override Task<int> RunAsync(CollectionContext context)
{
List<int> collectionIds = [Id];
IEnumerable<IOsuCollection> collections = context.Manager.GetCollectionsById(collectionIds);
HashSet<int> foundIds = [.. collections.Select(c => c.Id)];

if (!foundIds.Contains(Id))
{
LogCollectionIdNotFound(Id);

return Task.FromResult(1);
}

IOsuCollection collection = collections.First();
string newCollectionName = context.Manager.GetValidCollectionName(NewName);
CollectionEditArgs args = CollectionEditArgs.DuplicateCollection(collection.Name, newCollectionName);
context.Manager.EditCollection(args);

LogCollectionDuplicated(CollectionLogger.FormatCollection(collection), CollectionLogger.FormatCollection(context.Manager.GetCollectionByName(newCollectionName)));
return Task.FromResult(0);
}

[LoggerMessage(Level = LogLevel.Error, Message = "Collection Id {Id} not found.")]
private partial void LogCollectionIdNotFound(int id);

[LoggerMessage(Level = LogLevel.Information, Message = "Duplicated {OriginalCollection} to {NewCollection}")]
private partial void LogCollectionDuplicated(string originalCollection, string newCollection);
}
Loading
Loading