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
6 changes: 4 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Critical performance feature - DO NOT break compatibility:

- Only MkvMerge reports IETF tags, and a missing one is derived from the ISO 639-2B tag
- MkvMerge normalizes to IETF tags when `SetIetfLanguageTags` enabled
- FfMpeg and HandBrake drop the IETF tags when they write a file. Each of their rewrites restores the tags with MkvPropEdit before the output replaces the original, from the track list read before the rewrite. `Convert.RestoreLanguageTags()` first checks that the output keeps each track's number, type, and ISO 639-2B tag. Otherwise the rewrite fails, and the original stays. A HandBrake output holds the video, then the audio, then the subtitle tracks, so `GetHandBrakeTrackOrder()` numbers copies of the tags that way. The FfMpeg remux fallback restores them only for a Matroska input, since another container carries no IETF tags.
- FfMpeg and HandBrake drop the IETF tags when they write a file. Each of their rewrites restores the tags with MkvPropEdit before the output replaces the original, from the original input's track list. `Convert.RestoreLanguageTags()` first checks that the output keeps each track's number, type, and ISO 639-2B tag. Otherwise the rewrite fails, and the original stays. A HandBrake output holds the video, then the audio, then the subtitle tracks, so `GetHandBrakeTrackOrder()` numbers copies of the tags that way. The FfMpeg remux fallback restores them only for a Matroska input, since another container carries no IETF tags.
- FFprobe uses tag metadata which may differ from track metadata
- Track validation: Checks ISO/IETF consistency, sets error states for mismatches

Expand Down Expand Up @@ -442,7 +442,9 @@ flowchart TD
X3 -->|fail| XF
X3 --> X4["ReMux(): MkvMerge pass over the FfMpeg output"]
X4 -->|fail| XF
X4 --> X9["RestoreLanguageTags() when the input is Matroska"]
X4 --> X10["The input's MkvMerge track list, read here when the caller passed none"]
X10 -->|"read cancelled"| XF
X10 --> X9["RestoreLanguageTags() when the input is Matroska"]
X9 -->|fail| XF
X1 --> X5{"The .mkv name is the input file itself?"}
X9 --> X5
Expand Down
15 changes: 15 additions & 0 deletions PlexCleaner/Convert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,21 @@ out string outputName
return false;
}

// A caller that has not read the props yet passes none, so read them only for this fallback
// An input MkvMerge cannot identify keeps the fallback, but a cancel must not skip the restore
if (languageProps == null)
{
if (Tools.MkvMerge.GetMediaProps(inputName, out MediaProps props))
{
languageProps = props;
}
else if (Program.IsCancelledError())
{
File.Delete(tempName);
return false;
}
}

// Only a Matroska input carries IETF language tags to restore
if (
languageProps?.IsContainerMkv() == true
Expand Down
38 changes: 34 additions & 4 deletions PlexCleanerTests/ReplaceOriginalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -538,8 +538,15 @@ bool success
_ = Directory.GetFiles(_directory, "name.tmp*").Should().BeEmpty();
}

[Fact]
public void RemuxByExtension_FfMpegFallbackBeforeMediaProps_RemuxesWithoutRestore()
[Theory]
[InlineData("Matroska", 0, true)]
[InlineData("AVI", 0, false)]
[InlineData("Matroska", 2, false)]
public void RemuxByExtension_FfMpegFallback_RestoresLanguageOnlyForMatroska(
string container,
int identifyExit,
bool restored
)
{
Assert.SkipWhen(OperatingSystem.IsWindows(), "The fake tools are shell scripts");
using IDisposable toolErrorScope = ToolErrorScope.BeginScope();
Expand All @@ -554,10 +561,24 @@ public void RemuxByExtension_FfMpegFallbackBeforeMediaProps_RemuxesWithoutRestor
string propEditLog = WriteFakeTools("*.tmp2|*.tmp4", 0);
FakeTools.WriteSampleToolData(_directory);

// The input identifies as the given container, the FfMpeg output as the sample Matroska tracks
// Only the input's audio track carries en-GB, so the restored tag shows which list was read
string identify = FakeTools.SampleIdentifyCase(_directory);
string identifyJson = Path.Combine(_directory, "MkvMergeInfoData.json");
JsonNode inputInfo = JsonNode.Parse(File.ReadAllText(identifyJson))!;
inputInfo["container"]!["type"] = container;
JsonNode audio = inputInfo["tracks"]!
.AsArray()
.First(track => track!["type"]!.GetValue<string>() == "audio")!;
audio["properties"]!["language_ietf"] = "en-GB";
string inputJson = Path.Combine(_directory, "InputInfoData.json");
File.WriteAllText(inputJson, inputInfo.ToJsonString());

// MkvMerge fails to remux the input, then remuxes the FfMpeg output
FakeTools.WriteScript(
Tools.MkvMerge.GetToolPath(),
FakeTools.SampleIdentifyCase(_directory)
$"case \"$*\" in *--identify*name.avi*) cat \"{inputJson}\"; exit {identifyExit} ;; esac\n"
+ identify
+ "case \"$*\" in *.tmp4*) ;; *) exit 2 ;; esac\n"
+ "for a; do case $a in *.tmp4) echo mkvmerge > \"$a\" ;; esac; done\n"
);
Expand All @@ -570,7 +591,16 @@ public void RemuxByExtension_FfMpegFallbackBeforeMediaProps_RemuxesWithoutRestor
_ = processFile.FileInfo.FullName.Should().Be(mkvFile);
_ = File.ReadAllText(mkvFile).Should().StartWith("mkvmerge");
_ = File.Exists(aviFile).Should().BeFalse();
_ = File.Exists(propEditLog).Should().BeFalse();
_ = File.Exists(propEditLog).Should().Be(restored);
if (restored)
{
string propEditArgs = File.ReadAllText(propEditLog);
_ = propEditArgs.Should().Contain("name.tmp2");
long number = audio["properties"]!["number"]!.GetValue<long>();
_ = $" {propEditArgs.Trim()} "
.Should()
.Contain($" --edit track:@{number} --set language=en-GB ");
}
_ = Directory.GetFiles(_directory, "name.tmp*").Should().BeEmpty();
}

Expand Down
Loading