diff --git a/MSUScripter/Configs/MsuSongMsuPcmInfo.cs b/MSUScripter/Configs/MsuSongMsuPcmInfo.cs index bdfbdd0..dca3ed6 100644 --- a/MSUScripter/Configs/MsuSongMsuPcmInfo.cs +++ b/MSUScripter/Configs/MsuSongMsuPcmInfo.cs @@ -99,7 +99,7 @@ public List GetFiles() return files; } - [YamlIgnore] + [YamlIgnore, JsonSchemaIgnore] public bool HasBothSubTracksAndSubChannels { get @@ -110,7 +110,7 @@ public bool HasBothSubTracksAndSubChannels } } - [YamlIgnore] + [YamlIgnore, JsonSchemaIgnore] public bool HasValidSubChannelCount { get @@ -120,7 +120,7 @@ public bool HasValidSubChannelCount } } - [YamlIgnore] + [YamlIgnore, JsonSchemaIgnore] public bool HasValidChildTypes { get diff --git a/MSUScripter/MSUScripter.csproj b/MSUScripter/MSUScripter.csproj index 991179c..b57abd8 100644 --- a/MSUScripter/MSUScripter.csproj +++ b/MSUScripter/MSUScripter.csproj @@ -8,7 +8,7 @@ true MSUScripterIcon.ico MSUScripterIcon.ico - 5.0.0 + 5.0.1 9.0.0 false 12 diff --git a/MSUScripter/Services/ControlServices/MainWindowService.cs b/MSUScripter/Services/ControlServices/MainWindowService.cs index f7ef9cc..363a7fc 100644 --- a/MSUScripter/Services/ControlServices/MainWindowService.cs +++ b/MSUScripter/Services/ControlServices/MainWindowService.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Http; using System.Runtime.Versioning; using System.Threading.Tasks; using AvaloniaControls.ControlServices; @@ -254,12 +256,10 @@ private bool CleanDirectory(string path, TimeSpan? timeout = null) if (newerGitHubRelease != null) { - if (OperatingSystem.IsLinux()) - { - return (newerGitHubRelease.Url, - newerGitHubRelease.Asset.FirstOrDefault(x => x.Url.ToLower().EndsWith(".appimage"))?.Url); - } - return (newerGitHubRelease.Url, null); + var downloadUrl = OperatingSystem.IsLinux() + ? newerGitHubRelease.Asset.FirstOrDefault(x => x.Url.ToLower().EndsWith(".appimage"))?.Url + : newerGitHubRelease.Asset.FirstOrDefault(x => x.Url.ToLower().EndsWith(".exe"))?.Url; + return (newerGitHubRelease.Url, downloadUrl); } return null; @@ -296,6 +296,72 @@ public void IgnoreFutureUpdates() settingsService.SaveSettings(); } + public async Task InstallWindowsUpdate(string url) + { + var filename = Path.GetFileName(new Uri(url).AbsolutePath); + var localPath = Path.Combine(Path.GetTempPath(), filename); + + logger.LogInformation("Downloading {Url} to {LocalPath}", url, localPath); + + var response = await DownloadFileAsyncAttempt(url, localPath); + + if (!response.Item1) + { + logger.LogInformation("Download failed: {Error}", response.Item2); + return response.Item2; + } + + try + { + logger.LogInformation("Launching setup file"); + + var psi = new ProcessStartInfo + { + FileName = localPath, + UseShellExecute = true, + RedirectStandardOutput = false, + RedirectStandardError = false, + RedirectStandardInput = false, + CreateNoWindow = true + }; + + Process.Start(psi); + return null; + } + catch (Exception e) + { + logger.LogError(e, "Failed to start setup file"); + return "Failed to start setup file"; + } + } + + private async Task<(bool, string?)> DownloadFileAsyncAttempt(string url, string target, int attemptNumber = 0, int totalAttempts = 3) + { + + using var httpClient = new HttpClient(); + + try + { + await using var downloadStream = await httpClient.GetStreamAsync(url); + await using var fileStream = new FileStream(target, FileMode.Create); + await downloadStream.CopyToAsync(fileStream); + return (true, null); + } + catch (Exception ex) + { + logger.LogError(ex, "Download failed"); + if (attemptNumber < totalAttempts) + { + await Task.Delay(TimeSpan.FromSeconds(attemptNumber)); + return await DownloadFileAsyncAttempt(url, target, attemptNumber + 1, totalAttempts); + } + else + { + return (false, $"Download failed: {ex.Message}"); + } + } + } + private async Task CleanUpFolders() { await ITaskService.Run(() => diff --git a/MSUScripter/Services/DependencyInstallerService.cs b/MSUScripter/Services/DependencyInstallerService.cs index c050241..2d22324 100644 --- a/MSUScripter/Services/DependencyInstallerService.cs +++ b/MSUScripter/Services/DependencyInstallerService.cs @@ -32,63 +32,73 @@ public async Task InstallPyApp(Action response, Func + logger.LogInformation("Deleting prior Python installation"); + try { - Directory.Delete(destination, true); - }); - } - catch (TaskCanceledException) - { - // Do Nothing + await ITaskService.Run(() => + { + Directory.Delete(destination, true); + }); + } + catch (TaskCanceledException) + { + // Do Nothing + } } - } - - EnsureFolders(destination); - var tempFile = Path.Combine(Directories.TempFolder, "python.tar.gz"); - var url = OperatingSystem.IsWindows() ? PythonWindowsDownloadUrl : PythonLinuxDownloadUrl; + EnsureFolders(destination); - response.Invoke("Downloading Python"); - if (!await DownloadFileAsync(url, tempFile)) - { - return false; - } + var tempFile = Path.Combine(Directories.TempFolder, "python.tar.gz"); + var url = OperatingSystem.IsWindows() ? PythonWindowsDownloadUrl : PythonLinuxDownloadUrl; - response.Invoke("Extracting Python files"); - if (!await ExtractTarGzFile(tempFile, Directories.Dependencies)) - { - return false; - } + response.Invoke("Downloading Python"); + if (!await DownloadFileAsync(url, tempFile)) + { + return false; + } - var pythonPath = OperatingSystem.IsWindows() - ? Path.Combine(destination, "python.exe") - : Path.Combine(destination, "bin", "python3.13"); + response.Invoke("Extracting Python files"); + if (!await ExtractTarGzFile(tempFile, Directories.Dependencies)) + { + return false; + } - if (OperatingSystem.IsLinux()) - { - File.SetUnixFileMode(pythonPath, - UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); - } + if (OperatingSystem.IsLinux()) + { + File.SetUnixFileMode(pythonPath, + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); + } - response.Invoke("Verifying Python version"); + response.Invoke("Verifying Python version"); - var runPyResult = await runPyFunc(pythonPath, "--version"); - if (!runPyResult.Success || !runPyResult.Result.StartsWith("Python 3")) - { - logger.LogError("Python version response incorrect: {Response} | {Error}", runPyResult.Result, - runPyResult.Error); - return false; + var verifyPythonInstallResult = await runPyFunc(pythonPath, "--version"); + if (!verifyPythonInstallResult.Success || !verifyPythonInstallResult.Result.StartsWith("Python 3.13")) + { + logger.LogError("Python version response incorrect: {Response} | {Error}", verifyPythonInstallResult.Result, + verifyPythonInstallResult.Error); + return false; + } } - + response.Invoke("Installing companion app"); - runPyResult = await runPyFunc(pythonPath, "-m pip install py-msu-scripter-app"); + var runPyResult = await runPyFunc(pythonPath, "-m pip install --upgrade py-msu-scripter-app"); if (!runPyResult.Success && !runPyResult.Error.StartsWith("[notice]")) { logger.LogError("Failed to install Python companion app: {Error}", runPyResult.Error); diff --git a/MSUScripter/Services/PythonCompanionService.cs b/MSUScripter/Services/PythonCompanionService.cs index b65cf45..d688986 100644 --- a/MSUScripter/Services/PythonCompanionService.cs +++ b/MSUScripter/Services/PythonCompanionService.cs @@ -8,13 +8,14 @@ using AvaloniaControls.Services; using Microsoft.Extensions.Logging; using MSUScripter.Models; +using MSUScripter.Tools; namespace MSUScripter.Services; public class PythonCompanionService(ILogger logger, YamlService yamlService, DependencyInstallerService dependencyInstallerService) { private const string BaseCommand = "py_msu_scripter_app"; - private const string MinVersion = "v0.1.5"; + private const string MinVersion = "v0.1.9"; private RunMethod _runMethod = RunMethod.Unknown; private string? _pythonExecutablePath; private string? _ffmpegPath; @@ -34,7 +35,7 @@ public async Task VerifyInstalledAsync() _pythonExecutablePath = null; var response = await RunCommandAsync("--version"); - IsValid = response.Success && response.Result.EndsWith(MinVersion) && + IsValid = response.Success && VerifyVersionNumber(response.Result) && !response.Error.Contains("Couldn't find ffmpeg"); if (IsValid) @@ -49,6 +50,13 @@ public async Task VerifyInstalledAsync() return IsValid; } + private bool VerifyVersionNumber(string versionString) + { + var minVersion = MinVersion.VersionStringToDecimal(); + var currentVersion = versionString.Substring(versionString.IndexOf('v')).VersionStringToDecimal(); + return currentVersion >= minVersion; + } + public async Task VerifyFfMpegAsync() { var ffmpegFolder = Path.Combine(Directories.Dependencies, "ffmpeg", "bin"); @@ -603,7 +611,7 @@ private async Task RunInternalAsync(string command, string argument await process.WaitForExitAsync(cancellationToken ?? CancellationToken.None); var resultText = ""; - var errorText = ""; + string errorText; if (isPipInstall) { diff --git a/MSUScripter/Tools/StringExtensions.cs b/MSUScripter/Tools/StringExtensions.cs index 1b339b7..f114532 100644 --- a/MSUScripter/Tools/StringExtensions.cs +++ b/MSUScripter/Tools/StringExtensions.cs @@ -14,4 +14,33 @@ public static int GetUnicodeLength(this string str) { return new StringInfo(str.CleanString()).LengthInTextElements; } + + public static decimal? VersionStringToDecimal(this string str) + { + if (!str.Contains('.')) + { + return null; + } + + if (str.StartsWith('v')) + { + str = str[1..]; + } + + var parts = str.Split('.'); + if (parts.Length <= 2) + { + return decimal.Parse(str); + } + else if (parts.Length == 3) + { + return decimal.Parse(parts[0]) * 1000 + decimal.Parse(parts[1]) + decimal.Parse(parts[2]) / 1000; + } + else if (parts.Length == 4) + { + return decimal.Parse(parts[0]) * 1000000 + decimal.Parse(parts[1]) * 1000 + decimal.Parse(parts[2]) + decimal.Parse(parts[3]) / 1000; + } + + return null; + } } \ No newline at end of file diff --git a/MSUScripter/Views/MainWindow.axaml.cs b/MSUScripter/Views/MainWindow.axaml.cs index 07dcd41..ebf5ac7 100644 --- a/MSUScripter/Views/MainWindow.axaml.cs +++ b/MSUScripter/Views/MainWindow.axaml.cs @@ -126,6 +126,11 @@ private async Task ShowDesktopFileWindow() private async Task ShowNewReleaseWindow(string releaseUrl, string? downloadUrl) { + if (_service == null) + { + return; + } + downloadUrl ??= ""; var hasDownloadUrl = !string.IsNullOrEmpty(downloadUrl); @@ -151,7 +156,7 @@ private async Task ShowNewReleaseWindow(string releaseUrl, string? downloadUrl) if (messageWindow.DialogResult.CheckedBox) { - _service?.IgnoreFutureUpdates(); + _service.IgnoreFutureUpdates(); } if (messageWindow.DialogResult.PressedAcceptButton) @@ -178,7 +183,15 @@ private async Task ShowNewReleaseWindow(string releaseUrl, string? downloadUrl) } else { - throw new InvalidOperationException("Not supported on Windows"); + var result = await _service.InstallWindowsUpdate(downloadUrl); + if (!string.IsNullOrEmpty(result)) + { + await MessageWindow.ShowErrorDialog(result); + } + else + { + Close();; + } } } } diff --git a/PyMsuScripterApp/py_msu_scripter_app/__main__.py b/PyMsuScripterApp/py_msu_scripter_app/__main__.py index df19eb0..f80cc4e 100644 --- a/PyMsuScripterApp/py_msu_scripter_app/__main__.py +++ b/PyMsuScripterApp/py_msu_scripter_app/__main__.py @@ -38,7 +38,7 @@ def cli(): print("Error: the input YAML file was not found") exit(1) - with open(args.input, "r") as stream: + with open(args.input, "r", encoding="utf-8") as stream: try: yaml_file = yaml.safe_load(stream) except yaml.YAMLError as exc: diff --git a/PyMsuScripterApp/py_msu_scripter_app/video_creator.py b/PyMsuScripterApp/py_msu_scripter_app/video_creator.py index 521a549..d6963a5 100644 --- a/PyMsuScripterApp/py_msu_scripter_app/video_creator.py +++ b/PyMsuScripterApp/py_msu_scripter_app/video_creator.py @@ -53,7 +53,7 @@ def run(self) -> bool: return self.print_yaml(False, f"Track file {track_file} does not exist. Exiting.") stop_event = threading.Event() - t = threading.Thread(target=self.writer_thread, args=(self.progress_file, stop_event)) + t = threading.Thread(target=self.writer_thread, args=(self.progress_file, stop_event), daemon=True) try: t.start() @@ -72,10 +72,10 @@ def run(self) -> bool: logger = MyBarLogger(self) - audio_clip = AudioFileClip(output_wav) - video_clip = ColorClip(size=(720, 576), color=(0, 0, 0), duration=audio_clip.duration) - video_clip = video_clip.with_audio(audio_clip) - video_clip.write_videofile(output_mp4, fps=24, logger=logger) + with AudioFileClip(output_wav) as audio_clip: + with ColorClip(size=(720, 576), color=(0, 0, 0), duration=audio_clip.duration) as video_clip: + video_clip = video_clip.with_audio(audio_clip) + video_clip.write_videofile(output_mp4, fps=24, logger=logger) return self.print_yaml(True, "") except Exception as e: @@ -83,8 +83,12 @@ def run(self) -> bool: return self.print_yaml(False, f"Error creating wav or mp4 file {str(e)}") finally: stop_event.set() - t.join() - + t.join(timeout=2) + try: + audio_clip.close() + video_clip.close() + except Exception: + pass def print_yaml(self, successful: bool, error: str) -> bool: data = dict( @@ -102,11 +106,14 @@ def print_yaml(self, successful: bool, error: str) -> bool: def writer_thread(self, filename, stop_event): while not stop_event.is_set(): - with open(filename, "w") as f: - print(f"writer thread {self.phase} {self.phase_progress}/100") - f.write(f"{self.phase}|{self.phase_progress}\n") - f.flush() # ensure immediate write - time.sleep(0.5) # 500 ms + try: + with open(filename, "w") as f: + print(f"writer thread {self.phase} {self.phase_progress}/100") + f.write(f"{self.phase}|{self.phase_progress}\n") + f.flush() # ensure immediate write + except Exception: + pass + time.sleep(0.5) # 500 ms class MyBarLogger(ProgressBarLogger): diff --git a/PyMsuScripterApp/pyproject.toml b/PyMsuScripterApp/pyproject.toml index 2ef144a..eb96a6d 100644 --- a/PyMsuScripterApp/pyproject.toml +++ b/PyMsuScripterApp/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py-msu-scripter-app" -version = "0.1.5" +version = "0.1.9" description = "" authors = ["MattEqualsCoder "] readme = "README.md" diff --git a/Schemas/MsuSongInfo.json b/Schemas/MsuSongInfo.json index decc9f9..2f07568 100644 --- a/Schemas/MsuSongInfo.json +++ b/Schemas/MsuSongInfo.json @@ -156,6 +156,13 @@ ], "description": "The file to be used as the input for this track/sub-track/sub-channel" }, + "Dither": { + "type": [ + "boolean", + "null" + ], + "description": "Whether or not to apply audio dither to the final output." + }, "SubTracks": { "type": "array", "description": "Files which will be concatenated together to form the input to the parent track", @@ -169,9 +176,6 @@ "items": { "$ref": "#/definitions/MsuSongMsuPcmInfo" } - }, - "HasBothSubTracksAndSubChannels": { - "type": "boolean" } } } diff --git a/Schemas/MsuSongMsuPcmInfo.json b/Schemas/MsuSongMsuPcmInfo.json index 4e344c6..e6b4f43 100644 --- a/Schemas/MsuSongMsuPcmInfo.json +++ b/Schemas/MsuSongMsuPcmInfo.json @@ -99,6 +99,13 @@ ], "description": "The file to be used as the input for this track/sub-track/sub-channel" }, + "Dither": { + "type": [ + "boolean", + "null" + ], + "description": "Whether or not to apply audio dither to the final output." + }, "SubTracks": { "type": "array", "description": "Files which will be concatenated together to form the input to the parent track", @@ -112,9 +119,6 @@ "items": { "$ref": "#" } - }, - "HasBothSubTracksAndSubChannels": { - "type": "boolean" } } } \ No newline at end of file