From 57060b18e2b01f6cba5e0b682162a1e76bbf8372 Mon Sep 17 00:00:00 2001 From: BlackSpirits Date: Thu, 10 Sep 2026 21:59:49 +0200 Subject: [PATCH] Allow explicit NTSC for VOB extraction --- src/seconv/Commands/ConvertCommand.cs | 30 ++++++++++++++++++++++++ src/seconv/Core/SubtitleConverter.cs | 11 +++++---- src/seconv/Helpers/HelpDisplay.cs | 2 ++ tests/seconv/Core/VobSubExtractorTest.cs | 27 +++++++++++++++++++++ 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/seconv/Commands/ConvertCommand.cs b/src/seconv/Commands/ConvertCommand.cs index 7067897f0cc..a252a340669 100644 --- a/src/seconv/Commands/ConvertCommand.cs +++ b/src/seconv/Commands/ConvertCommand.cs @@ -17,6 +17,17 @@ internal sealed class ConvertCommand : AsyncCommand /// public static string[] RawArgs { get; set; } = []; + internal static bool ResolveVobIsPal(bool vobPal, bool vobNtsc) + { + if (vobPal && vobNtsc) + { + throw new ArgumentException("--vob-pal and --vob-ntsc are mutually exclusive."); + } + + // Preserve the existing CLI behaviour unless NTSC is explicitly selected. + return !vobNtsc; + } + public sealed class Settings : CommandSettings { [CommandArgument(0, "")] @@ -71,6 +82,14 @@ public sealed class Settings : CommandSettings [Description("Frame rate")] public double? Fps { get; init; } + [CommandOption("--vob-pal")] + [Description("VOB input: treat DVD video as PAL (720x576; default)")] + public bool VobPal { get; init; } + + [CommandOption("--vob-ntsc")] + [Description("VOB input: treat DVD video as NTSC (720x480)")] + public bool VobNtsc { get; init; } + [CommandOption("--input-folder|--inputfolder")] [Description("Input folder name")] public string? InputFolder { get; init; } @@ -687,6 +706,16 @@ protected override async Task ExecuteAsync(CommandContext context, Settings return Fail(settings, $"--change-speed must be greater than 0 (got {settings.ChangeSpeed.Value})."); } + bool vobIsPal; + try + { + vobIsPal = ResolveVobIsPal(settings.VobPal, settings.VobNtsc); + } + catch (ArgumentException ex) + { + return Fail(settings, ex.Message); + } + // Parse offset if supplied TimeSpan? offset = null; if (!string.IsNullOrWhiteSpace(settings.Offset)) @@ -742,6 +771,7 @@ protected override async Task ExecuteAsync(CommandContext context, Settings InputEncodingFallback = settings.InputEncodingFallback, Fps = settings.Fps, TargetFps = settings.TargetFps, + VobIsPal = vobIsPal, Overwrite = settings.Overwrite, KeepTimestamp = settings.KeepTimestamp, Operations = operations, diff --git a/src/seconv/Core/SubtitleConverter.cs b/src/seconv/Core/SubtitleConverter.cs index d5dfa6141da..2a24e86d315 100644 --- a/src/seconv/Core/SubtitleConverter.cs +++ b/src/seconv/Core/SubtitleConverter.cs @@ -262,10 +262,9 @@ private async Task ConvertVobBatchAsync(List vobFiles, try { - // IsPal — there's no single reliable auto-detect from VOB alone (would need - // IFO parsing). Default to PAL to match the GUI's batch converter. Future - // work: add --vob-pal/--vob-ntsc and/or read VIDEO_TS.IFO. - var outputs = VobSubExtractor.Extract(vobFiles, outputBase, isPal: true, overwrite: options.Overwrite); + // There is no reliable PAL/NTSC auto-detect from VOB alone without IFO parsing. + // Preserve PAL as the default, while allowing the CLI to select NTSC explicitly. + var outputs = VobSubExtractor.Extract(vobFiles, outputBase, options.VobIsPal, overwrite: options.Overwrite); result.SuccessfulFiles = vobFiles.Count; // Report the first stream's output path against each input VOB. With multiple // streams there's no clean 1:1 mapping back to inputs, but the OutputFile slot @@ -1115,6 +1114,10 @@ internal record class ConversionOptions public string? InputEncodingFallback { get; init; } public double? Fps { get; init; } public double? TargetFps { get; init; } + + /// DVD VOB extraction video standard. PAL remains the default for backwards compatibility. + public bool VobIsPal { get; init; } = true; + public bool Overwrite { get; init; } /// --keep-timestamp: copy the source file's creation/last-write time onto every output file. diff --git a/src/seconv/Helpers/HelpDisplay.cs b/src/seconv/Helpers/HelpDisplay.cs index 77b8d409204..016c0090a71 100644 --- a/src/seconv/Helpers/HelpDisplay.cs +++ b/src/seconv/Helpers/HelpDisplay.cs @@ -47,6 +47,8 @@ private static void ShowHelp(IAnsiConsole console) ShowParameter(console, "--input-encoding-fallback:", "Assumed input encoding when no BOM and not UTF-8 (skips ANSI guess)"); ShowParameter(console, "--forced-only", "Process forced subtitles only"); ShowParameter(console, "--fps:", "Frame rate for conversion"); + ShowParameter(console, "--vob-pal", "VOB input: treat DVD video as PAL (720x576; default)"); + ShowParameter(console, "--vob-ntsc", "VOB input: treat DVD video as NTSC (720x480)"); ShowParameter(console, "--input-folder:", "Input folder path"); ShowParameter(console, "--offset:hh:mm:ss:ms", "Time offset"); ShowParameter(console, "--output-filename:", "Output file name (for single file only)"); diff --git a/tests/seconv/Core/VobSubExtractorTest.cs b/tests/seconv/Core/VobSubExtractorTest.cs index 06507780c2e..f65d2e9b6a3 100644 --- a/tests/seconv/Core/VobSubExtractorTest.cs +++ b/tests/seconv/Core/VobSubExtractorTest.cs @@ -1,3 +1,4 @@ +using SeConv.Commands; using SeConv.Core; using Xunit; @@ -31,6 +32,32 @@ public void Dispose() } } + [Theory] + [InlineData(false, false, true)] + [InlineData(true, false, true)] + [InlineData(false, true, false)] + public void ResolveVobIsPal_SelectsExpectedStandard(bool vobPal, bool vobNtsc, bool expectedIsPal) + { + Assert.Equal(expectedIsPal, ConvertCommand.ResolveVobIsPal(vobPal, vobNtsc)); + } + + [Fact] + public void ResolveVobIsPal_RejectsConflictingFlags() + { + var ex = Assert.Throws(() => ConvertCommand.ResolveVobIsPal(vobPal: true, vobNtsc: true)); + Assert.Contains("mutually exclusive", ex.Message); + } + + [Fact] + public void ConversionOptions_VobStandardDefaultsToPal_AndAllowsNtsc() + { + var defaultOptions = new ConversionOptions { Patterns = [], Format = "VobSub" }; + var ntscOptions = new ConversionOptions { Patterns = [], Format = "VobSub", VobIsPal = false }; + + Assert.True(defaultOptions.VobIsPal); + Assert.False(ntscOptions.VobIsPal); + } + [Fact] public async Task ConvertAsync_VobInput_NonVobSubTarget_ErrorsWithGuidance() {