diff --git a/AssM.csproj b/AssM.csproj index 380857d..559ab34 100644 --- a/AssM.csproj +++ b/AssM.csproj @@ -5,26 +5,30 @@ enable true app.manifest - 1.6.1 + 1.7.0 SubZeroPL - https://github.com/SubZeroPL/AssM/ - 1.6.1 - 1.6.1 + https://github.com/SubZeroPL/AssM + 1.7.0 + 1.7.0 AssM + https://github.com/SubZeroPL/AssM/blob/master/LICENSE + https://github.com/SubZeroPL/AssM/ + Assets\assm.ico - - - - - + + + + + - - + + - - + + + @@ -40,6 +44,7 @@ + @@ -53,25 +58,30 @@ + + + + + diff --git a/Assets/assm.ico b/Assets/assm.ico new file mode 100644 index 0000000..3405810 Binary files /dev/null and b/Assets/assm.ico differ diff --git a/Classes/Constants.cs b/Classes/Constants.cs index c80e731..844c764 100644 --- a/Classes/Constants.cs +++ b/Classes/Constants.cs @@ -29,7 +29,7 @@ private static bool IsWindows() => [GeneratedRegex(BinFile)] public static partial Regex BinFileRegex(); - private const string BinHash = @"BIN \(TRACK (\d{2})\) MD5\: (.+)"; + private const string BinHash = @"TRACK (\d{2}) MD5\: (.+)"; [GeneratedRegex(BinHash)] public static partial Regex BinHashRegex(); public const string ReadmeGameTitle = "#gameTitle#"; @@ -53,4 +53,12 @@ private static bool IsWindows() => "Generating README", "Process JSON" ]; + + public static readonly string[] AddFolderSteps = + [ + "", + "Aggregating image files (folder {0} of {1})", + "Adding to the list", + "Loading existing data" + ]; } \ No newline at end of file diff --git a/Classes/Functions.cs b/Classes/Functions.cs index 3e79d3a..64ca5fc 100644 --- a/Classes/Functions.cs +++ b/Classes/Functions.cs @@ -19,7 +19,7 @@ public static class Functions public static string GetChdName(Game game, Configuration configuration) => configuration.GameIdAsChdName ? $"{game.Id.ToUpper()}.chd" - : Path.GetFileName(Path.ChangeExtension(game.CuePath, "chd")); + : Path.GetFileName(Path.ChangeExtension(game.ImagePath, "chd")); public static void LoadChdManInfo(string chdPath, Game game) { @@ -174,7 +174,7 @@ private static void LoadTrackInfoFromReadme(string readmePath, Game game) } var endIndex = Array.FindIndex(lines, line => line.Contains("**Description:**")) - 1; - var hashes = lines.Skip(startIndex).Take(endIndex - startIndex).Where(item => item.StartsWith("BIN")); + var hashes = lines.Skip(startIndex).Take(endIndex - startIndex).Where(item => item.StartsWith("TRACK")); foreach (var hashLine in hashes) { var matches = Constants.BinHashRegex().Matches(hashLine); @@ -199,11 +199,12 @@ private static void LoadTrackInfoFromReadme(string readmePath, Game game) } } - public static List GetCueFilesInDirectory(string directory) + public static List GetCueIsoFilesInDirectory(string directory) { - Logger.Debug($"Getting cue files from {directory}"); - var result = Directory.GetFiles(directory).Where(d => Path.GetExtension(d) == ".cue").ToList(); - Directory.GetDirectories(directory).ToList().ForEach(d => result.AddRange(GetCueFilesInDirectory(d))); + Logger.Debug($"Getting cue/iso files from {directory}"); + var result = Directory.GetFiles(directory) + .Where(d => Path.GetExtension(d) == ".cue" || Path.GetExtension(d) == ".iso").ToList(); + Directory.GetDirectories(directory).ToList().ForEach(d => result.AddRange(GetCueIsoFilesInDirectory(d))); return result; } @@ -216,21 +217,22 @@ public static List GetReadmeFilesInDirectory(string directory) return result; } - public static Game? AddGameToList(string cuePath, Configuration configuration, ObservableCollection gameList) + public static Game? AddGameToList(string imagePath, Configuration configuration, ObservableCollection gameList) { - Logger.Debug($"Adding game to list from {cuePath}"); - var di = DiscInspector.ScanDisc(cuePath); + Logger.Debug($"Adding game to list from {imagePath}"); + var di = DiscInspector.ScanDisc(imagePath); // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - apparently there are games that have no Id in image (like SLPS-00018) if (di.Data.SerialNumber == null) { - Logger.Error($"Failed to add game to list from {cuePath}{Environment.NewLine}Id not present in image"); + Logger.Error($"Failed to add game to list from {imagePath}{Environment.NewLine}Id not present in image"); return null; } - var title = configuration.GetTitleFromCue ? Path.GetFileNameWithoutExtension(cuePath) : di.Data.GameTitle; + + var title = configuration.GetTitleFromCue ? Path.GetFileNameWithoutExtension(imagePath) : di.Data.GameTitle; var game = new Game { Title = title, Id = di.Data.SerialNumber, Platform = di.DetectedDiscType, - CuePath = cuePath + ImagePath = imagePath }; var existingGame = gameList.FirstOrDefault(g => g.Id == game.Id); if (existingGame != null) @@ -238,7 +240,7 @@ public static List GetReadmeFilesInDirectory(string directory) Logger.Debug($"Game already exists: {existingGame.Title}, updating"); existingGame.Title = game.Title; existingGame.Platform = game.Platform; - existingGame.CuePath = game.CuePath; + existingGame.ImagePath = game.ImagePath; existingGame.Id = game.Id; } else @@ -273,4 +275,6 @@ public static void ProcessJson(Configuration configuration, Game game, Action game.ImagePath.EndsWith(".iso", StringComparison.OrdinalIgnoreCase); } \ No newline at end of file diff --git a/Data/Game.cs b/Data/Game.cs index c02e62f..d77c9f1 100644 --- a/Data/Game.cs +++ b/Data/Game.cs @@ -9,7 +9,7 @@ public class Game { public string Title { get; set; } = string.Empty; public string Id { get; set; } = string.Empty; - public string CuePath { get; set; } = string.Empty; + public string ImagePath { get; set; } = string.Empty; public DetectedDiscType Platform { get; set; } = DetectedDiscType.UnknownFormat; public bool ReadmeCreated { get; set; } public bool ChdCreated { get; set; } @@ -29,6 +29,6 @@ public class ChdData public string GetTrackInfo() { return string.Join(Environment.NewLine, - TrackInfo.Select(ti => $"BIN (TRACK {ti.TrackNo,2:D2}) MD5: {ti.TrackMD5}{Environment.NewLine}")).Trim(); + TrackInfo.Select(ti => $"TRACK {ti.TrackNo,2:D2} MD5: {ti.TrackMD5}{Environment.NewLine}")).Trim(); } } \ No newline at end of file diff --git a/README.md b/README.md index 3555395..1177e5f 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ It is a piece of software that assists in creating entries for [Arkadyzja savestates repository](https://github.com/ActionPL/duckstation_openbios_savestates). It allows creating and editing README files for games that will have savestates uploaded to the repository. Those savestates will then be available for [Arkadyzja](https://arkadyzja.honmaru.pl/). ## ⚠️ Requirements -For the application to work [.NET framework 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) is required. +For the application to work, [.NET Framework 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) is required. -Application is developed and tested on Windows 11 OS. In theory should also work on Linux/Mac hovewer it was not tested on those platforms (only a bit on Linux and not at all on Mac) and currently only Win and Linux builds are released (see below). +Application is developed and tested on Windows 11 OS. It should also work on Linux/Mac hovewer it was not tested (extensively) on those platforms (only a bit on Linux and not at all on Mac) and currently only Win and Linux builds are released (see below). ## 📦 Releases Each release starting with 1.4.0 has four variants: @@ -23,12 +23,12 @@ Clicking it will open default browser on the latest release page. ## 🪧 Usage [![Last Release](https://img.shields.io/github/v/release/SubZeroPL/AssM?logo=github&label=latest)](https://github.com/SubZeroPL/AssM/releases) -- get latest version of the application from [Releases](https://github.com/SubZeroPL/AssM/releases) page +- get the latest version of the application from the [Releases](https://github.com/SubZeroPL/AssM/releases) page - unpack to some empty directory - run the `AssM.exe` file (or `AssM` for Linux) ![Application main window](.github/assets/app.png "Application main window") -First you should select Output directory, where the created README files will be stored. The files are created in a specific directory structure: +First, you should select the Output directory, where the created README files will be stored. The files are created in a specific directory structure: `OutputPath\Platform\GameID`.\ The output directory is saved between sessions, so when you start the application next time it will be automatically read and existing entries inserted into the table.\ **You should not modify existing directory structure if you plan to use the output directory again later!** @@ -40,8 +40,8 @@ When you click `Start processing` the application will go through the list and p ## ⚙️ Configuration ![Configuration options](.github/assets/config.png "Configuration options") -- Get game title from CUE file name - by default game name is extracted from cue/bin image, this option uses CUE file name instead -- Use Game ID as name for CHD file - by default game title (as set by previous option) is used for CHD file name, this option uses Game ID instead +- Get game title from CUE file name - by default, game name is extracted from cue/bin image; this option uses CUE file name instead +- Use Game ID as the name for CHD file – by default, game title (as set by a previous option) is used for CHD file name; this option uses Game ID instead Other configuration options should be self-explanatory. diff --git a/Windows/AddFolderProgressWindow.axaml b/Windows/AddFolderProgressWindow.axaml index 0ecd337..42e5e7d 100644 --- a/Windows/AddFolderProgressWindow.axaml +++ b/Windows/AddFolderProgressWindow.axaml @@ -3,16 +3,16 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:gif="clr-namespace:Avalonia.Labs.Gif;assembly=Avalonia.Labs.Gif" - mc:Ignorable="d" d:DesignWidth="300" + mc:Ignorable="d" d:DesignWidth="400" x:Class="AssM.Windows.AddFolderProgressWindow" Closing="Window_OnClosing" - Width="300" WindowStartupLocation="CenterOwner" SizeToContent="Height" ShowInTaskbar="False" CanResize="False" + Width="400" WindowStartupLocation="CenterOwner" SizeToContent="Height" ShowInTaskbar="False" CanResize="False" Title="Adding folder contents"> - - + + diff --git a/Windows/AddFolderProgressWindow.axaml.cs b/Windows/AddFolderProgressWindow.axaml.cs index dbbfa92..d24fb08 100644 --- a/Windows/AddFolderProgressWindow.axaml.cs +++ b/Windows/AddFolderProgressWindow.axaml.cs @@ -9,6 +9,14 @@ namespace AssM.Windows; +internal class AddFolderProgressUpdateObject +{ + public string? Dir { get; init; } + public int? Count { get; init; } + public int? Index { get; init; } + public int Step { get; init; } +} + public partial class AddFolderProgressWindow : Window { private readonly BackgroundWorker _worker; @@ -24,43 +32,64 @@ public AddFolderProgressWindow() private void WorkerOnProgressChanged(object? sender, ProgressChangedEventArgs e) { - if (e.UserState is string dir) - LabelFolderName.Content = dir; + if (e.UserState is not AddFolderProgressUpdateObject updateObject) return; + LabelStep.Content = Constants.AddFolderSteps[updateObject.Step]; + if (updateObject.Step == 1) + { + LabelStep.Content = string.Format(Constants.AddFolderSteps[updateObject.Step], updateObject.Index, + updateObject.Count); + LabelFolderName.Content = updateObject.Dir; + } } - public void Process(List dirs, ObservableCollection gameList, Configuration configuration, Action> finishedCallback) + public void Process(List dirs, ObservableCollection gameList, Configuration configuration, + Action> finishedCallback) { var cueFiles = new List(); var errors = new List(); _worker.DoWork += (_, _) => { + var index = 0; foreach (var dir in dirs) { if (_worker.CancellationPending) return; - _worker.ReportProgress(0, dir); - cueFiles.AddRange(Functions.GetCueFilesInDirectory(dir)); + var progress = index / dirs.Count * 100; + _worker.ReportProgress(progress, + new AddFolderProgressUpdateObject { Dir = dir, Index = index++, Count = dirs.Count, Step = 1 }); + cueFiles.AddRange(Functions.GetCueIsoFilesInDirectory(dir)); } + index = 0; foreach (var cueFile in cueFiles) { if (_worker.CancellationPending) return; + var progress = index / cueFiles.Count * 100; + _worker.ReportProgress(progress, + new AddFolderProgressUpdateObject + { Dir = cueFile, Index = index++, Count = cueFiles.Count, Step = 2 }); var game = Functions.AddGameToList(cueFile, configuration, gameList); if (game == null) { - errors.Add($"Failed to add game to list from {cueFile}{Environment.NewLine}Id not present in image"); + errors.Add( + $"Failed to add game to list from {cueFile}{Environment.NewLine}Id not present in image"); } } + index = 0; if (string.IsNullOrWhiteSpace(configuration.OutputDirectory)) return; foreach (var game in gameList) { if (_worker.CancellationPending) return; + var progress = index / gameList.Count * 100; + _worker.ReportProgress(progress, + new AddFolderProgressUpdateObject + { Dir = game.Title, Index = index++, Count = gameList.Count, Step = 3 }); Functions.LoadExistingData(game, configuration); } }; - + _worker.RunWorkerCompleted += (_, _) => finishedCallback.Invoke(errors); - + _worker.RunWorkerAsync(); } diff --git a/Windows/MainWindow.axaml b/Windows/MainWindow.axaml index c53f6c0..5f65285 100644 --- a/Windows/MainWindow.axaml +++ b/Windows/MainWindow.axaml @@ -39,7 +39,7 @@ Process only modified enties Overwrite existing READMEs - Get game title from CUE file name + Get game title from file name Use Game ID as name for CHD file diff --git a/Windows/MainWindow.axaml.cs b/Windows/MainWindow.axaml.cs index 3eab7c6..d968ff2 100644 --- a/Windows/MainWindow.axaml.cs +++ b/Windows/MainWindow.axaml.cs @@ -20,6 +20,8 @@ using MsBox.Avalonia; using MsBox.Avalonia.Enums; using NLog; +using Octokit; +using ProductHeaderValue = Octokit.ProductHeaderValue; namespace AssM.Windows; @@ -86,30 +88,15 @@ private async Task CheckForNewVersion() { _logger.Debug("Checking for new version"); var currentVer = GetVersion(); - var client = new HttpClient(); - var request = new HttpRequestMessage - { - RequestUri = new Uri(Constants.LatestReleaseLink), - Method = HttpMethod.Get, - Headers = - { - Accept = { new MediaTypeWithQualityHeaderValue("application/vnd.github+json") } - } - }; - request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Assembly.GetExecutingAssembly().GetName().Name!, - Assembly.GetExecutingAssembly().GetName().Version!.ToString())); - var response = await client.SendAsync(request); - if (response.StatusCode != HttpStatusCode.OK) return; - var json = await response.Content.ReadAsStringAsync(); - var jsonDoc = JsonDocument.Parse(json); - if (!jsonDoc.RootElement.TryGetProperty("tag_name", out var tagName)) return; - var tag = tagName.GetString()?.Replace("v", string.Empty); + var ghClient = new GitHubClient(new ProductHeaderValue(Assembly.GetEntryAssembly()?.GetName().Name)); + var latestRelease = await ghClient.Repository.Release.GetLatest("SubZeroPL", "AssM"); + var tag = latestRelease.TagName.Replace("v", string.Empty); var versionPresent = Version.TryParse(tag, out var version); if (!versionPresent || version <= currentVer) return; _logger.Debug($"New version detected: {tag}"); TextBlockUpdate.Text = $"New version: {version!.Major}.{version.Minor}.{version.Build}"; ButtonUpdate.IsVisible = true; - ButtonUpdate.Tag = jsonDoc.RootElement.GetProperty("html_url").GetString(); + ButtonUpdate.Tag = latestRelease.Url; } private void AddButton_OnClick(object? sender, RoutedEventArgs e) @@ -118,7 +105,9 @@ private void AddButton_OnClick(object? sender, RoutedEventArgs e) { AllowMultiple = true, Title = "Select images", - FileTypeFilter = [new FilePickerFileType("CUE file") { Patterns = ["*.cue"] }] + FileTypeFilter = [ new FilePickerFileType("All supported formats (*.cue, *.iso)") { Patterns = ["*.cue", "*.iso"] }, + new FilePickerFileType("CUE file (*.cue)") { Patterns = ["*.cue"] }, + new FilePickerFileType("ISO file (*.iso)") { Patterns = ["*.iso"] }] }; var files = GetTopLevel(this)?.StorageProvider.OpenFilePickerAsync(fpo).GetAwaiter().GetResult() ?? new List(); @@ -130,14 +119,14 @@ private void AddButton_OnClick(object? sender, RoutedEventArgs e) DataGridGameList.CollectionView.Refresh(); } - private void AddGame(string cuePath) + private void AddGame(string imagePath) { - if (string.IsNullOrWhiteSpace(cuePath)) return; - var game = Functions.AddGameToList(cuePath, Configuration, GameList); + if (string.IsNullOrWhiteSpace(imagePath)) return; + var game = Functions.AddGameToList(imagePath, Configuration, GameList); if (game == null) { MessageBoxManager.GetMessageBoxStandard("Error", - $"Failed to add game to list from {cuePath}{Environment.NewLine}Id not present in image", + $"Failed to add game to list from {imagePath}{Environment.NewLine}Id not present in image", ButtonEnum.Ok, MsBox.Avalonia.Enums.Icon.Error, WindowStartupLocation.CenterOwner) .ShowWindowDialogAsync(this); return; @@ -172,7 +161,7 @@ private void AddFolderButton_OnClick(object? sender, RoutedEventArgs e) .ToList() ?? []; var progress = new AddFolderProgressWindow(); _ = progress.ShowDialog(this); - progress.Process(dirs, GameList, Configuration, (errors) => + progress.Process(dirs, GameList, Configuration, errors => { progress.Close(); DataGridGameList.CollectionView.Refresh(); diff --git a/Windows/ProgressWindow.axaml.cs b/Windows/ProgressWindow.axaml.cs index 55298a2..a091f9a 100644 --- a/Windows/ProgressWindow.axaml.cs +++ b/Windows/ProgressWindow.axaml.cs @@ -52,7 +52,8 @@ public void Process(List gameList, Configuration configuration, Action fin { for (var i = 0; i < gameList.Count; i++) { - _worker.ReportProgress(0); + var progress = i / gameList.Count * 100; + _worker.ReportProgress(progress); if (_worker.CancellationPending) break; var game = gameList.ElementAt(i); @@ -64,17 +65,18 @@ public void Process(List gameList, Configuration configuration, Action fin } var step = 1; - _worker.ReportProgress(0, + _worker.ReportProgress(progress, new ProgressUpdateObject - { Title = game.Title, Count = gameList.Count, Step = step, Index = i + 1 }); + { Title = game.Title, Count = gameList.Count, Step = step++, Index = i + 1 }); ConvertSingleChd(game); - _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ }); + _worker.ReportProgress(progress, + new ProgressUpdateObject { Step = step++, BinNo = 1, BinCount = game.ChdData.TrackInfo.Count }); CalculateTracksMd5(game); - _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ }); + _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step++ }); GetChdManInfo(game); - _worker.ReportProgress(0, new ProgressUpdateObject { Step = step++ }); + _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step++ }); GenerateReadme(game); - _worker.ReportProgress(0, new ProgressUpdateObject { Step = step }); + _worker.ReportProgress(progress, new ProgressUpdateObject { Step = step }); Functions.ProcessJson(_configuration, game, d => { _worker.ReportProgress((int)d); }); game.Modified = false; _logger.Debug($"Finished processing game {i + 1}: {game.Title}"); @@ -114,7 +116,7 @@ private void ConvertSingleChd(Game game) var chdPath = Path.Combine(_configuration.OutputDirectory, Functions.OutputPath(game), chdFile); _logger.Debug($"CHD path: {chdPath}"); if (File.Exists(chdPath) && _configuration.ChdProcessing != ChdProcessing.GenerateAll) return; - if (string.IsNullOrWhiteSpace(game.CuePath)) return; + if (string.IsNullOrWhiteSpace(game.ImagePath)) return; Directory.CreateDirectory(Path.GetDirectoryName(chdPath) ?? string.Empty); var chdmanConvert = new Process @@ -122,7 +124,7 @@ private void ConvertSingleChd(Game game) StartInfo = new ProcessStartInfo { FileName = Constants.ChdMan, - Arguments = string.Format(Constants.ChdManConvert, game.CuePath, chdPath), + Arguments = string.Format(Constants.ChdManConvert, game.ImagePath, chdPath), RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, @@ -200,20 +202,30 @@ private void CalculateTracksMd5(Game game) var readmePath = Path.Combine(_configuration.OutputDirectory, Functions.OutputPath(game), Constants.ReadmeFile); _logger.Debug($"Readme path: {readmePath}"); if (File.Exists(readmePath) && !_configuration.OverwriteExistingReadmes) return; - var cuefile = game.CuePath; - _logger.Debug($"Cue path: {cuefile}"); - if (string.IsNullOrWhiteSpace(cuefile)) return; - _logger.Debug("Adding bin files from cue"); - var lines = File.ReadLines(cuefile); + var imagefile = game.ImagePath; List bins = []; - bins.AddRange(from line in lines - where line.StartsWith("FILE") - select Constants.BinFileRegex().Matches(line) - into matches - select matches[0].Groups[1].Value - into bin - select Path.Combine(Path.GetDirectoryName(cuefile)!, bin)); - _logger.Debug($"Bin files from cue: {string.Join(',', bins)}"); + if (Functions.IsIso(game)) + { + _logger.Debug($"Iso path: {imagefile}"); + if (string.IsNullOrWhiteSpace(imagefile)) return; + bins.Add(imagefile); + _logger.Debug($"Iso file: {string.Join(',', bins)}"); + } + else + { + _logger.Debug($"Cue path: {imagefile}"); + if (string.IsNullOrWhiteSpace(imagefile)) return; + _logger.Debug("Adding bin files from cue"); + var lines = File.ReadLines(imagefile); + bins.AddRange(from line in lines + where line.StartsWith("FILE") + select Constants.BinFileRegex().Matches(line) + into matches + select matches[0].Groups[1].Value + into bin + select Path.Combine(Path.GetDirectoryName(imagefile)!, bin)); + _logger.Debug($"Bin files from cue: {string.Join(',', bins)}"); + } if (_worker.CancellationPending) return; if (game.ChdData.TrackInfo.Count != 0) game.ChdData.TrackInfo.Clear(); @@ -225,7 +237,7 @@ into bin using var fs = File.OpenRead(bin); var hash = CalculateMd5HashFromStream(fs); game.ChdData.TrackInfo.Add((i + 1, hash)); - _worker.ReportProgress((int)Math.Round((double)i / bins.Count * 100.0), + _worker.ReportProgress(i / bins.Count * 100, new ProgressUpdateObject { Step = 2, BinNo = i + 1, BinCount = bins.Count }); _logger.Debug("Done"); } @@ -255,7 +267,7 @@ private string CalculateMd5HashFromStream(Stream inputStream) sb.Append(t.ToString("x2")); } - return sb.ToString(); + return sb.ToString().ToUpperInvariant(); } private void GetChdManInfo(Game game)