From 244bf3ec5376075954ca66d11e8f519607515628 Mon Sep 17 00:00:00 2001 From: niksedk Date: Wed, 14 Oct 2026 17:26:33 +0200 Subject: [PATCH 1/2] Plugin-Shared: headless batch request dispatch + request type constants PluginBootstrap.Run takes an optional batch handler; a "batch" request (Subtitle Edit Batch convert) is answered without starting Avalonia. Co-Authored-By: Claude Opus 5.5 --- se5/Plugin-Shared/PluginBootstrap.cs | 27 +++++++++++++++++++++++++-- se5/Plugin-Shared/PluginContract.cs | 16 ++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/se5/Plugin-Shared/PluginBootstrap.cs b/se5/Plugin-Shared/PluginBootstrap.cs index 8c83a22b..44ba4676 100644 --- a/se5/Plugin-Shared/PluginBootstrap.cs +++ b/se5/Plugin-Shared/PluginBootstrap.cs @@ -9,7 +9,8 @@ namespace SubtitleEdit.Plugins.Shared; /// /// Entry-point glue for an SE5 plugin: parse the request JSON from the first /// command-line argument, start Avalonia, write the response JSON when the -/// app exits, return 0. +/// app exits, return 0. A "batch" request (Subtitle Edit's Batch convert) is handed to the +/// plugin's batch handler instead, without starting any UI. /// public static class PluginBootstrap { @@ -21,7 +22,12 @@ public static class PluginBootstrap WriteIndented = true, }; - public static int Run(string[] args) where TApp : Application, new() + /// + /// Handles a "batch" request headless: apply saved settings to all lines and return the + /// response. Plugins that pass it should set "batch": true in plugin.json and also + /// handle a "configure" request in their window (settings only, no subtitle in the response). + /// + public static int Run(string[] args, Func? batchHandler = null) where TApp : Application, new() { if (args.Length < 1) { @@ -46,6 +52,23 @@ public static class PluginBootstrap return 1; } + if (request.RequestType == PluginRequestType.Batch) + { + PluginResponse batchResponse; + try + { + batchResponse = batchHandler?.Invoke(request) + ?? new PluginResponse { Status = PluginStatus.Error, Message = "This plugin does not support batch convert." }; + } + catch (Exception exception) + { + batchResponse = new PluginResponse { Status = PluginStatus.Error, Message = exception.Message }; + } + + File.WriteAllText(request.ResponseFilePath, JsonSerializer.Serialize(batchResponse, JsonOpts)); + return 0; + } + PluginApp.PendingRequest = request; BuildAvaloniaApp().StartWithClassicDesktopLifetime(Array.Empty()); diff --git a/se5/Plugin-Shared/PluginContract.cs b/se5/Plugin-Shared/PluginContract.cs index 72b6d045..524a4c8b 100644 --- a/se5/Plugin-Shared/PluginContract.cs +++ b/se5/Plugin-Shared/PluginContract.cs @@ -146,6 +146,22 @@ public sealed class PluginResponse public string? UndoDescription { get; set; } } +/// +/// values. "batch" and "configure" come from Subtitle Edit's +/// Batch convert and are only sent to plugins with "batch": true in plugin.json. +/// +public static class PluginRequestType +{ + /// From the Plugins menu - the plugin may show its window. + public const string Run = "run"; + + /// Once per file in Batch convert - no UI, saved settings, all lines. + public const string Batch = "batch"; + + /// From Batch convert's Configure button - show settings only, return them without a subtitle. + public const string Configure = "configure"; +} + public static class PluginStatus { public const string Ok = "ok"; From 0d3c93a78dfbac54df9a839cdb438a664148e1a0 Mon Sep 17 00:00:00 2001 From: niksedk Date: Wed, 14 Oct 2026 17:27:38 +0200 Subject: [PATCH 2/2] FixEncodingErrors: support Subtitle Edit Batch convert - plugin.json "batch": true - batch request: fix every broken line with the saved code page (auto-detect per file by default), no UI - configure request: code page only, settings returned without subtitle Co-Authored-By: Claude Opus 5.5 --- se5/FixEncodingErrors.Tests/BatchModeTests.cs | 63 +++++++++++++++++++ se5/FixEncodingErrors/MainViewModel.cs | 47 ++++++++++++-- se5/FixEncodingErrors/MainWindow.axaml | 13 +++- se5/FixEncodingErrors/Program.cs | 2 +- se5/FixEncodingErrors/README.md | 7 +++ se5/FixEncodingErrors/plugin.json | 1 + 6 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 se5/FixEncodingErrors.Tests/BatchModeTests.cs diff --git a/se5/FixEncodingErrors.Tests/BatchModeTests.cs b/se5/FixEncodingErrors.Tests/BatchModeTests.cs new file mode 100644 index 00000000..3abf88b5 --- /dev/null +++ b/se5/FixEncodingErrors.Tests/BatchModeTests.cs @@ -0,0 +1,63 @@ +using SubtitleEdit.Plugins.FixEncodingErrors; +using SubtitleEdit.Plugins.Shared; +using System.Text.Json; + +namespace FixEncodingErrors.Tests; + +/// Subtitle Edit Batch convert: "batch" fixes every line headless, "configure" returns the code page only. +public class BatchModeTests +{ + private static PluginRequest Request(string requestType, string? settingsJson, params string[] texts) => new() + { + RequestType = requestType, + Subtitle = new PluginSubtitle + { + Format = "SubRip", + Paragraphs = texts.Select((t, i) => new PluginParagraph { StartMs = i * 1000, EndMs = i * 1000 + 900, Text = t }).ToList(), + }, + Settings = settingsJson == null ? null : JsonDocument.Parse(settingsJson).RootElement.Clone(), + SettingsVersion = settingsJson == null ? null : 1, + }; + + [Fact] + public void Batch_FixesAllBrokenLines() + { + var response = new MainViewModel(Request(PluginRequestType.Batch, null, "café", "Plain", "don’t")).BuildBatchResponse(); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Equal(new[] { "café", "Plain", "don’t" }, response.Subtitle!.Paragraphs!.Select(p => p.Text)); + Assert.Null(response.Settings); + } + + [Fact] + public void Batch_NothingToFix_ReturnsOkWithoutSubtitle() + { + var response = new MainViewModel(Request(PluginRequestType.Batch, null, "Plain", "résumé")).BuildBatchResponse(); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Null(response.Subtitle); + } + + [Fact] + public void Batch_UsesSavedCodePage() + { + var vm = new MainViewModel(Request(PluginRequestType.Batch, "{\"codePage\":1251}", "café")); + + Assert.Equal(1251, vm.SelectedEncoding.CodePage); + } + + [Fact] + public void Configure_ReturnsSettingsWithoutSubtitle() + { + var vm = new MainViewModel(Request(PluginRequestType.Configure, null)); + Assert.True(vm.CanApply); + Assert.False(vm.IsClean); + vm.SelectedEncoding = vm.EncodingOptions.First(e => e.CodePage == 1250); + + var response = vm.BuildResponse(); + + Assert.Equal(PluginStatus.Ok, response.Status); + Assert.Null(response.Subtitle); + Assert.Equal(1250, response.Settings!.Value.GetProperty("codePage").GetInt32()); + } +} diff --git a/se5/FixEncodingErrors/MainViewModel.cs b/se5/FixEncodingErrors/MainViewModel.cs index f8232a4b..e277f55b 100644 --- a/se5/FixEncodingErrors/MainViewModel.cs +++ b/se5/FixEncodingErrors/MainViewModel.cs @@ -16,6 +16,7 @@ public sealed partial class MainViewModel : ObservableObject private readonly List _paragraphs; private readonly long _videoOffsetMs; private readonly SourceEncoding _detected; + private readonly bool _isConfigure; private bool _loading; [ObservableProperty] private SourceEncoding _selectedEncoding; @@ -25,6 +26,7 @@ public sealed partial class MainViewModel : ObservableObject public MainViewModel(PluginRequest request) { _request = request; + _isConfigure = request.RequestType == PluginRequestType.Configure; _videoOffsetMs = request.Rules?.VideoOffsetMs ?? 0; _legacyRequest = request.Subtitle.Paragraphs == null; _paragraphs = _legacyRequest @@ -34,7 +36,7 @@ public MainViewModel(PluginRequest request) SelectedIndices = request.SelectedIndices.Where(i => i >= 0 && i < _paragraphs.Count).Distinct().Order().ToList(); var detected = EncodingFixer.Detect(_paragraphs.Select(p => p.Text ?? string.Empty)); - AutoEncoding = new SourceEncoding(0, "Auto-detect - " + detected.Name); + AutoEncoding = new SourceEncoding(0, _isConfigure ? "Auto-detect (per file)" : "Auto-detect - " + detected.Name); _detected = detected; EncodingOptions = new[] { AutoEncoding }.Concat(EncodingFixer.Encodings).ToList(); _selectedEncoding = AutoEncoding; @@ -57,22 +59,33 @@ public string SubtitleInfo { get { + if (_isConfigure) + { + return "Settings for Batch convert"; + } + var name = string.IsNullOrEmpty(_request.Subtitle.FileName) ? "Untitled" : Path.GetFileName(_request.Subtitle.FileName); var format = string.IsNullOrEmpty(_request.Subtitle.Format) ? string.Empty : $" · {_request.Subtitle.Format}"; return $"{name}{format} · {_paragraphs.Count:N0} lines"; } } + /// Opened from Batch convert's Configure button: only the code page matters, there is no subtitle. + public bool IsConfigure => _isConfigure; + public bool IsNotConfigure => !_isConfigure; + public bool HasSelectedLines => SelectedIndices.Count > 0; public string SelectedLinesText => SelectedIndices.Count == 1 ? "Selected line (1)" : $"Selected lines ({SelectedIndices.Count:N0})"; public bool AllLines { get => !SelectedLinesOnly; set => SelectedLinesOnly = !value; } public int ChangeCount => Changes.Count; public bool HasChanges => Changes.Count > 0; - public bool IsClean => Changes.Count == 0; - public bool CanApply => SelectedCount > 0; + public bool IsClean => Changes.Count == 0 && !_isConfigure; + public bool CanApply => SelectedCount > 0 || _isConfigure; public string CleanText => $"No broken characters found for {EffectiveEncoding.Name}."; - public string StatusText => Changes.Count == 0 + public string StatusText => _isConfigure + ? "Used for every file in Batch convert" + : Changes.Count == 0 ? $"{LinesInScope:N0} lines checked" : $"{SelectedCount:N0} of {Changes.Count:N0} lines selected"; @@ -154,9 +167,35 @@ private void SetSelection(Func selected) } } + /// + /// A "batch" request (Batch convert): fix every line with the saved code page (auto-detect + /// per file by default), without UI. No changes is "ok" without a subtitle. + /// + public PluginResponse BuildBatchResponse() + { + if (Changes.Count == 0) + { + return new PluginResponse { Status = PluginStatus.Ok, Message = "Fix encoding errors: nothing to fix." }; + } + + var response = BuildResponse(); + response.Settings = null; + response.SettingsVersion = null; + return response; + } + + /// A "configure" request: hand back the settings only, for the next Batch convert run. + public PluginResponse BuildConfigureResponse() => + new() { Status = PluginStatus.Ok, Settings = BuildSettings(), SettingsVersion = SettingsVersion }; + /// The response for OK: the selected lines, fixed. public PluginResponse BuildResponse() { + if (_isConfigure) + { + return BuildConfigureResponse(); + } + var selected = Changes.Where(c => c.IsSelected).ToList(); if (selected.Count == 0) { diff --git a/se5/FixEncodingErrors/MainWindow.axaml b/se5/FixEncodingErrors/MainWindow.axaml index acbf304b..618779b6 100644 --- a/se5/FixEncodingErrors/MainWindow.axaml +++ b/se5/FixEncodingErrors/MainWindow.axaml @@ -237,7 +237,7 @@ - + @@ -276,6 +276,17 @@ + + + + + + + + + diff --git a/se5/FixEncodingErrors/Program.cs b/se5/FixEncodingErrors/Program.cs index e7c28d11..184452ec 100644 --- a/se5/FixEncodingErrors/Program.cs +++ b/se5/FixEncodingErrors/Program.cs @@ -11,6 +11,6 @@ public static class Program public static int Main(string[] args) { IconProvider.Current.Register(); - return PluginBootstrap.Run(args); + return PluginBootstrap.Run(args, request => new MainViewModel(request).BuildBatchResponse()); } } diff --git a/se5/FixEncodingErrors/README.md b/se5/FixEncodingErrors/README.md index ae7a7896..b2798f63 100644 --- a/se5/FixEncodingErrors/README.md +++ b/se5/FixEncodingErrors/README.md @@ -66,6 +66,13 @@ Works on the lines exactly as Subtitle Edit holds them, so styles, actors and other fields of formats like ASSA are kept (falls back to SubRip on older SE versions). +## Batch convert + +Fix encoding errors supports Subtitle Edit's **Batch convert** (Tools → Batch convert → Run plugin; needs a Subtitle Edit version with plugin batch support). + +- **Configure...** opens the window without a subtitle to pick the code page. *Auto-detect* (the default) checks each file on its own. +- During the batch every broken line is fixed without showing the window. Files with nothing to fix are left as they are. The same false-fix guards apply as in the window. + ## Build See `.github/workflows/fix-encoding-errors.yml`. diff --git a/se5/FixEncodingErrors/plugin.json b/se5/FixEncodingErrors/plugin.json index 647a3bc5..116efec4 100644 --- a/se5/FixEncodingErrors/plugin.json +++ b/se5/FixEncodingErrors/plugin.json @@ -7,6 +7,7 @@ "url": "https://github.com/SubtitleEdit/plugins/tree/main/se5/FixEncodingErrors", "menu": "Tools", "minSeVersion": "5.0.0", + "batch": true, "runtime": "dotnet", "entry": "FixEncodingErrors.dll" }