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
20 changes: 1 addition & 19 deletions src/seconv/Core/SubtitleConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -252,24 +252,6 @@ private async Task<ConversionResult> ConvertVobBatchAsync(List<string> vobFiles,
outputBase = Path.Combine(outputFolder, stem + ".sub");
}

// Overwrite check is best-effort against the base path. Multi-stream DVDs land
// additional outputs at <stem>.<n>.sub which we can't predict without parsing —
// those existing files will be overwritten silently. Acceptable for now since
// multi-stream DVDs are rare and the user opted into the batch by passing all VOBs.
if (!options.Overwrite)
{
var pair = new[] { outputBase, Path.ChangeExtension(outputBase, ".idx") };
foreach (var p in pair)
{
if (File.Exists(p))
{
result.Errors.Add($"Output file already exists: {p}. Pass --overwrite to replace it.");
result.FailedFiles = vobFiles.Count;
return result;
}
}
}

if (!options.Quiet)
{
var label = vobFiles.Count == 1
Expand All @@ -283,7 +265,7 @@ private async Task<ConversionResult> ConvertVobBatchAsync(List<string> vobFiles,
// 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);
var outputs = VobSubExtractor.Extract(vobFiles, outputBase, isPal: true, 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
51 changes: 46 additions & 5 deletions src/seconv/Core/VobSubExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public sealed record StreamOutput(string Path, int StreamId, int Written);
/// <c>movie.0.sub</c>, <c>movie.1.sub</c>, …) and the matching .idx is
/// written alongside each one.
/// </summary>
public static IReadOnlyList<StreamOutput> Extract(IReadOnlyList<string> vobFiles, string subOutputPath, bool isPal)
public static IReadOnlyList<StreamOutput> Extract(IReadOnlyList<string> vobFiles, string subOutputPath, bool isPal, bool overwrite = true)
{
if (vobFiles.Count == 0)
{
Expand Down Expand Up @@ -72,15 +72,15 @@ public static IReadOnlyList<StreamOutput> Extract(IReadOnlyList<string> vobFiles
.OrderBy(g => g.Key)
.ToList();

var outputPaths = BuildOutputPaths(subOutputPath, streams.Count);
EnsureOutputFilesCanBeWritten(outputPaths, overwrite);

var outputs = new List<StreamOutput>(streams.Count);
for (var i = 0; i < streams.Count; i++)
{
var streamId = streams[i].Key;
var streamPacks = streams[i].OrderBy(p => p.StartTime.Ticks).ToList();

var outputPath = streams.Count == 1
? subOutputPath
: InsertStreamIndex(subOutputPath, i);
var outputPath = outputPaths[i];

var written = WriteOneStream(streamPacks, outputPath, isPal, streamId);
outputs.Add(new StreamOutput(outputPath, streamId, written));
Expand All @@ -100,6 +100,47 @@ private static string InsertStreamIndex(string subOutputPath, int index)
return Path.Combine(dir, $"{stem}.{index}.sub");
}

internal static IReadOnlyList<string> BuildOutputPaths(string subOutputPath, int streamCount)
{
if (streamCount <= 0)
{
return [];
}

if (streamCount == 1)
{
return [subOutputPath];
}

var outputPaths = new List<string>(streamCount);
for (var i = 0; i < streamCount; i++)
{
outputPaths.Add(InsertStreamIndex(subOutputPath, i));
}

return outputPaths;
}

internal static void EnsureOutputFilesCanBeWritten(IReadOnlyList<string> outputPaths, bool overwrite)
{
if (overwrite)
{
return;
}

foreach (var outputPath in outputPaths)
{
var idxPath = Path.ChangeExtension(outputPath, ".idx");
foreach (var path in new[] { outputPath, idxPath })
{
if (File.Exists(path))
{
throw new IOException($"Output file already exists: {path}. Pass --overwrite to replace it.");
}
}
}
}

private static int WriteOneStream(IReadOnlyList<VobSubMergedPack> packs, string outputPath, bool isPal, int streamId)
{
var screenWidth = 720;
Expand Down
33 changes: 33 additions & 0 deletions tests/seconv/Core/VobSubExtractorTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,37 @@ public async Task ConvertAsync_VobInput_WithVobSubTarget_AttemptsExtraction()
// No "input file too large" leak.
Assert.DoesNotContain("too large", result.Errors[0]);
}

[Fact]
public void EnsureOutputFilesCanBeWritten_MultiStream_NoOverwrite_ProtectsNumberedOutputs()
{
var outputBase = Path.Combine(_tempRoot, "movie.sub");
var outputPaths = VobSubExtractor.BuildOutputPaths(outputBase, 2);
Assert.Equal(Path.Combine(_tempRoot, "movie.0.sub"), outputPaths[0]);
Assert.Equal(Path.Combine(_tempRoot, "movie.1.sub"), outputPaths[1]);

var existing = Path.ChangeExtension(outputPaths[1], ".idx");
File.WriteAllText(existing, "keep-me");

var ex = Assert.Throws<IOException>(() =>
VobSubExtractor.EnsureOutputFilesCanBeWritten(outputPaths, overwrite: false));

Assert.Contains(existing, ex.Message);
Assert.Equal("keep-me", File.ReadAllText(existing));
Assert.False(File.Exists(outputPaths[0]));
Assert.False(File.Exists(outputPaths[1]));
}

[Fact]
public void EnsureOutputFilesCanBeWritten_MultiStream_DoesNotBlockUnusedBasePath()
{
var outputBase = Path.Combine(_tempRoot, "movie.sub");
File.WriteAllText(outputBase, "unrelated-existing-base");

var outputPaths = VobSubExtractor.BuildOutputPaths(outputBase, 2);
VobSubExtractor.EnsureOutputFilesCanBeWritten(outputPaths, overwrite: false);

Assert.Equal("unrelated-existing-base", File.ReadAllText(outputBase));
}

}
Loading