Skip to content
Merged
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
63 changes: 63 additions & 0 deletions se5/FixEncodingErrors.Tests/BatchModeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using SubtitleEdit.Plugins.FixEncodingErrors;
using SubtitleEdit.Plugins.Shared;
using System.Text.Json;

namespace FixEncodingErrors.Tests;

/// <summary>Subtitle Edit Batch convert: "batch" fixes every line headless, "configure" returns the code page only.</summary>
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());
}
}
47 changes: 43 additions & 4 deletions se5/FixEncodingErrors/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public sealed partial class MainViewModel : ObservableObject
private readonly List<PluginParagraph> _paragraphs;
private readonly long _videoOffsetMs;
private readonly SourceEncoding _detected;
private readonly bool _isConfigure;
private bool _loading;

[ObservableProperty] private SourceEncoding _selectedEncoding;
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -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";
}
}

/// <summary>Opened from Batch convert's Configure button: only the code page matters, there is no subtitle.</summary>
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";

Expand Down Expand Up @@ -154,9 +167,35 @@ private void SetSelection(Func<ChangeItem, bool> selected)
}
}

/// <summary>
/// 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.
/// </summary>
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;
}

/// <summary>A "configure" request: hand back the settings only, for the next Batch convert run.</summary>
public PluginResponse BuildConfigureResponse() =>
new() { Status = PluginStatus.Ok, Settings = BuildSettings(), SettingsVersion = SettingsVersion };

/// <summary>The response for OK: the selected lines, fixed.</summary>
public PluginResponse BuildResponse()
{
if (_isConfigure)
{
return BuildConfigureResponse();
}

var selected = Changes.Where(c => c.IsSelected).ToList();
if (selected.Count == 0)
{
Expand Down
13 changes: 12 additions & 1 deletion se5/FixEncodingErrors/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@
</Border>

<!-- Toolbar -->
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,Auto,*,Auto" Margin="0,0,0,12">
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,Auto,*,Auto" Margin="0,0,0,12" IsVisible="{Binding IsNotConfigure}">
<Border Classes="segmented" ToolTip.Tip="Which lines to fix">
<StackPanel Orientation="Horizontal" Spacing="2">
<RadioButton GroupName="scope" IsChecked="{Binding AllLines}">
Expand Down Expand Up @@ -276,6 +276,17 @@
</StackPanel>
</Grid>

<!-- Configure (Batch convert) -->
<StackPanel DockPanel.Dock="Top" IsVisible="{Binding IsConfigure}" HorizontalAlignment="Center"
Margin="0,50,0,0" Spacing="10" MaxWidth="520">
<Border Width="88" Height="88" CornerRadius="44" Background="{StaticResource FixTint}" HorizontalAlignment="Center">
<i:Icon Value="mdi-cog-outline" FontSize="48" Foreground="{StaticResource FixBrush}" />
</Border>
<TextBlock Text="Batch convert settings" FontSize="18" FontWeight="SemiBold" HorizontalAlignment="Center" />
<TextBlock Classes="muted" TextWrapping="Wrap" TextAlignment="Center"
Text="Pick the code page above and click OK. Batch convert then fixes every broken line in each file without showing this window. Auto-detect checks each file on its own." />
</StackPanel>

<!-- Empty state -->
<StackPanel DockPanel.Dock="Top" IsVisible="{Binding IsClean}" HorizontalAlignment="Center"
Margin="0,70,0,0" Spacing="10">
Expand Down
2 changes: 1 addition & 1 deletion se5/FixEncodingErrors/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ public static class Program
public static int Main(string[] args)
{
IconProvider.Current.Register<MaterialDesignIconProvider>();
return PluginBootstrap.Run<App>(args);
return PluginBootstrap.Run<App>(args, request => new MainViewModel(request).BuildBatchResponse());
}
}
7 changes: 7 additions & 0 deletions se5/FixEncodingErrors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
1 change: 1 addition & 0 deletions se5/FixEncodingErrors/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
27 changes: 25 additions & 2 deletions se5/Plugin-Shared/PluginBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ namespace SubtitleEdit.Plugins.Shared;
/// <summary>
/// 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.
/// </summary>
public static class PluginBootstrap
{
Expand All @@ -21,7 +22,12 @@ public static class PluginBootstrap
WriteIndented = true,
};

public static int Run<TApp>(string[] args) where TApp : Application, new()
/// <param name="batchHandler">
/// Handles a "batch" request headless: apply saved settings to all lines and return the
/// response. Plugins that pass it should set <c>"batch": true</c> in plugin.json and also
/// handle a "configure" request in their window (settings only, no subtitle in the response).
/// </param>
public static int Run<TApp>(string[] args, Func<PluginRequest, PluginResponse>? batchHandler = null) where TApp : Application, new()
{
if (args.Length < 1)
{
Expand All @@ -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<TApp>().StartWithClassicDesktopLifetime(Array.Empty<string>());

Expand Down
16 changes: 16 additions & 0 deletions se5/Plugin-Shared/PluginContract.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ public sealed class PluginResponse
public string? UndoDescription { get; set; }
}

/// <summary>
/// <see cref="PluginRequest.RequestType"/> values. "batch" and "configure" come from Subtitle Edit's
/// Batch convert and are only sent to plugins with <c>"batch": true</c> in plugin.json.
/// </summary>
public static class PluginRequestType
{
/// <summary>From the Plugins menu - the plugin may show its window.</summary>
public const string Run = "run";

/// <summary>Once per file in Batch convert - no UI, saved settings, all lines.</summary>
public const string Batch = "batch";

/// <summary>From Batch convert's Configure button - show settings only, return them without a subtitle.</summary>
public const string Configure = "configure";
}

public static class PluginStatus
{
public const string Ok = "ok";
Expand Down
Loading