Skip to content
Draft
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
30 changes: 30 additions & 0 deletions src/seconv/Commands/ConvertCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ internal sealed class ConvertCommand : AsyncCommand<ConvertCommand.Settings>
/// </summary>
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, "<pattern>")]
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -687,6 +706,16 @@ protected override async Task<int> 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))
Expand Down Expand Up @@ -742,6 +771,7 @@ protected override async Task<int> ExecuteAsync(CommandContext context, Settings
InputEncodingFallback = settings.InputEncodingFallback,
Fps = settings.Fps,
TargetFps = settings.TargetFps,
VobIsPal = vobIsPal,
Overwrite = settings.Overwrite,
KeepTimestamp = settings.KeepTimestamp,
Operations = operations,
Expand Down
11 changes: 7 additions & 4 deletions src/seconv/Core/SubtitleConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,9 @@ private async Task<ConversionResult> ConvertVobBatchAsync(List<string> 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
Expand Down Expand Up @@ -1115,6 +1114,10 @@ internal record class ConversionOptions
public string? InputEncodingFallback { get; init; }
public double? Fps { get; init; }
public double? TargetFps { get; init; }

/// <summary>DVD VOB extraction video standard. PAL remains the default for backwards compatibility.</summary>
public bool VobIsPal { get; init; } = true;

public bool Overwrite { get; init; }

/// <summary>--keep-timestamp: copy the source file's creation/last-write time onto every output file.</summary>
Expand Down
2 changes: 2 additions & 0 deletions src/seconv/Helpers/HelpDisplay.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ private static void ShowHelp(IAnsiConsole console)
ShowParameter(console, "--input-encoding-fallback:<name>", "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>", "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:<folder name>", "Input folder path");
ShowParameter(console, "--offset:hh:mm:ss:ms", "Time offset");
ShowParameter(console, "--output-filename:<file name>", "Output file name (for single file only)");
Expand Down
27 changes: 27 additions & 0 deletions tests/seconv/Core/VobSubExtractorTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using SeConv.Commands;
using SeConv.Core;
using Xunit;

Expand Down Expand Up @@ -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<ArgumentException>(() => 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()
{
Expand Down
Loading