Repository navigation
Restore IETF Language Tags After Every FFmpeg or HandBrake Rewrite - #1152
Conversation
FFmpeg and HandBrake drop the LanguageIETF element of every track, so a region or script subtag such as the CA in fr-CA could not come back after the lossless timestamp repair, closed caption removal, the re-encode repair tier, or the FFmpeg remux fallback. Convert.RestoreLanguageTags() now restores the tags on every FFmpeg and HandBrake rewrite before the output replaces the original. It edits only when the output keeps each track's number, type, and ISO 639-2B tag, and warns otherwise. The remux fallback restores only for a Matroska input. Closes #1148 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A track mismatch now fails RestoreLanguageTags() at every rewrite site before the move, so the original stays. The VerifyTrackOrder() check that ReEncode and DeInterlace ran after the move is removed with MediaProps.VerifyTrackOrder(), leaving one track comparison. Test the type, language, and dropped-track arms, an unparsable identify, and closed caption removal. Fold the FakeTools sample identify and sidecar reads onto one helper each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HandBrake writes the video, then the audio, then the subtitle tracks, so the re-encode repair tier's HandBrake fallback failed the track check for a file stored in any other order. GetHandBrakeTrackOrder() numbers copies of the tags that way for it and for DeInterlace, which renumbered the original tracks in place through a shallow clone. MediaProps.Clone() has no caller left and is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedYou've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Next included review available in 6 minutes. View limit details
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
|
A recorded local strict-review pass covers head |
PR Summary by QodoRestore IETF language tags before media rewrites replace originals
AI Description
Diagram
High-Level Assessment
Files changed (8)
|
Code Review by Qodo
1.
|
There was a problem hiding this comment.
🟡 Changes recommended
An active moderate issue can cause otherwise repairable files to fail during the HandBrake fallback, and the lossless-repair path lacks regression coverage for its critical pre-replacement edit.
2 open issues
What changed in this PR
This pull request restores IETF language tags after FFmpeg and HandBrake rewrites, validating track identity before rewritten output replaces the original.
Changes:
- Adds
Convert.RestoreLanguageTags()for pre-replacement track validation and language restoration. - Applies restoration across remux, closed-caption removal, re-encode, deinterlace, and lossless timestamp-repair paths.
- Adds HandBrake track ordering, fake-tool coverage, regression tests, and architecture documentation.
- Identifies a HandBrake fallback issue with embedded caption tracks and a missing successful lossless-repair regression test.
This pull request requires changes.
| File | Description |
|---|---|
PlexCleanerTests/VerifyToolErrorTests.cs |
Updates repair fakes for identification and language-edit operations. |
PlexCleanerTests/ReplaceOriginalTests.cs |
Tests restoration, HandBrake ordering, closed-caption handling, remux fallback, and replacement safety. |
PlexCleanerTests/ReEncodeBitrateStateTests.cs |
Verifies language restoration edits the re-encode temporary output. |
PlexCleanerTests/FakeTools.cs |
Consolidates sample identify data and fake-tool setup. |
PlexCleaner/ProcessFile.cs |
Integrates restoration into rewrite and repair workflows and removes obsolete post-move validation. |
PlexCleaner/MediaProps.cs |
Removes obsolete cloning and track-order verification helpers. |
PlexCleaner/Convert.cs |
Adds language restoration, track validation, HandBrake ordering, and updated remux handling. |
ARCHITECTURE.md |
Documents language restoration and updates processing flowcharts. |
Review effort: Lite
HandBrake converts subtitles to ASS, adds embedded closed captions as one more subtitle track, and drops the IETF tags. The repair by re-encode's HandBrake fallback selected every subtitle, so a file with embedded captions failed the track check once a mismatch failed the rewrite. ConvertUsingHandBrake() encodes without subtitles, merges the original subtitle tracks back with MkvMerge, and restores the tags, for DeInterlace and the repair fallback alike. HandBrake.ConvertToMkv() no longer takes an includeSubtitles flag, and AllSubtitles() is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ConvertUsingHandBrake() logs which MkvMerge step runs on the HandBrake output, as DeInterlace did before the helper. The HandBrake repair test asserts that the merge takes only the original's subtitles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemuxByExtension() runs before GetMediaProps(), so it passed a null MkvMergeProps, and the FFmpeg remux fallback threw a NullReferenceException on the IETF restore check. The language props are now optional, and RemuxByExtension() passes none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
A recorded local strict-review pass covers head |
|
Answers to Qodo's findings on 1ddcbb2:
|
* Add 3.23 History Lines for Tag Restore and Reserved .tmp Names #1152 and #1145 changed user-visible behavior with no 3.23 release line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Match the 3.23 Tag and Temporary Name Lines to the Code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Name the Track Number in the 3.23 Track Check Line Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>


Summary
FFmpeg and HandBrake drop the
LanguageIETFelement of every track when they write an MKV file. A region or script subtag, such as theCAinfr-CA, then cannot come back from the ISO 639-2 code alone. Before this change only theReEncodeandDeInterlacesteps restored the tags. Issue: #1148.Convert.RestoreLanguageTags(), the one restore every FFmpeg and HandBrake rewrite calls before its output replaces the original.mkvmerge -JJSON and checks that each track keeps its number, type, and ISO 639-2 tag. Otherwise it logs an error and the rewrite fails before the move, so the original stays. A rewrite that drops or reorders tracks therefore never replaces the original, and never gets the tags on the wrong tracks.MediaProps, which would log a "Setting IETF tag from ISO639 tag" warning for every track the rewrite stripped.Convert.ReMuxToMkv()FFmpeg fallback. The fallback restores only for a Matroska input, since another container carries no IETF tags.ConvertToMkv()andDeInterlace()call the same helper in place of their ownSetTrackLanguage()call. TheirVerifyTrackOrder()check after the move is removed, along withMediaProps.VerifyTrackOrder(). That check failed the step only after the original was already replaced, and hitDebug.Assert. The helper's check before the move replaces it, so one track comparison remains.ProcessFile.ConvertUsingHandBrake(), the workaround for HandBrake converts all subtitles to ASS vs. passthrough #95 and HandBrake removes IETF language tags #149 thatDeInterlace()already used. HandBrake encodes without subtitles, MkvMerge merges the original subtitle tracks back unchanged, and the tags are restored. The repair by re-encode's HandBrake fallback now uses it as well. It used to select every subtitle, so HandBrake converted them to ASS and added embedded closed captions as one more subtitle track.HandBrake.ConvertToMkv()no longer takes anincludeSubtitlesflag, andHandBrakeBuilder.AllSubtitles()is removed.Convert.GetHandBrakeTrackOrder()numbers copies of the tags in that order for the re-encode repair tier's HandBrake fallback and forDeInterlace().DeInterlace()used to renumberMkvMergeProps' own track objects in place through the shallowMediaProps.Clone(). That method has no caller left and is removed.ReMuxToMkv(string, out string)takes the input's MkvMerge props, ornullwhere they are not read yet.RemuxByExtension()runs beforeGetMediaProps(), so it passes none. The track-selection overload no longer acceptsnull, and its one null caller now picks the overload itself.The branch carries
developwith #1149, the #1146 fix, merged in. The lossless repair then runssetts, the MkvMerge remux #1149 adds, the regression gate, the re-verify, this restore, and the move, in that order.Verification
dotnet test: 473 passed, on the branch merged with currentdevelop. New tests cover:RemuxByExtension()on an.avibefore the media props load, with the FFmpeg fallback taking over. It fails with aNullReferenceExceptionif the null guard is reverted.GetHandBrakeTrackOrder()on a video, subtitle, audio input, leaving the original numbers unchanged--subtitle none, MkvMerge merges the original back only when there is a subtitle, and the restore runs on.tmp12The re-encode repair test also asserts the restore ran on
.tmp12. The fake MkvMerge answers--identifywith the sample track list through oneFakeTools.SampleIdentifyCase()helper.Real media,
ptr727/plexcleaner:developimage tools (FFmpeg 8.1.2, MkvToolNix 100.0). The clip is synthetic: H.264 video taggedenand AAC audio taggedfr-CA, with one audio packet timestamp stepping back. The default settings were used withReEncodeoff, so verify takes the losslesssettsrepair.language_ietfafterprocessfrfr-CAThe baseline also logs "Setting IETF tag from ISO639 tag" for each track and runs an extra "Remux to repair metadata errors" pass. This branch does neither. Rerun on the branch merged with Drop Tags in the Lossless Timestamp Repair Like Every Other Rewrite #1149:
fr-CAstill holds.HandBrake 1.11.2 from the same image, on a synthetic HEVC clip with CC608 caption SEI, AAC audio, and one SRT subtitle:
--all-subtitleswrites SRT plus an extra SubStationAlpha track from the captions.--subtitle none, followed by the MkvMerge merge of the original subtitles, gives exactly the original video, audio, and SRT tracks.CSharpier,
dotnet format style --verify-no-changes --severity=info, markdownlint, and cspell are clean. The prose gate reports onlycomment-added, which thecommentslabel covers.Not covered by a unit test: the lossless repair call site. No test drives that tier to a successful repair today, and #1149 changes the fakes such a test needs. That test is #1151. The real-media run above exercises this call site.
Local Strict Review
The first pass raised these findings:
HasSameTracks()duplicatedMediaProps.VerifyTrackOrder(). The latter is removed, together with its callers after the move.FakeToolshad two copies of the sample identify arm and the sample sidecar read.GetMkvMergeTrackType()from a type pattern to theTrackTypeenum. Both throw on an unknown track, and there is no existing type-to-string mapping to share. TheMkvMergeToolswitch maps strings to new props objects.The second pass raised one finding, now fixed. Once a mismatch fails the rewrite, the re-encode tier's HandBrake fallback failed for any file not already in video, audio, subtitle order.
The third pass raised one finding, now fixed. The HandBrake fallback's
--all-subtitlesadds embedded closed captions as an extra subtitle track, which failed the track check. The fix routes every HandBrake run throughConvertUsingHandBrake(), per the maintainer's direction.Later passes over the HandBrake helper raised a dropped merge log line and a merge test that did not check the track selection. Both are fixed. The passes over the null fix and the #1149 merge raised nothing.
Hosted Review
RemuxByExtension()passed a nullMkvMergePropsto the FFmpeg fallback.Dropped findings
PlexCleanerTests/CleanupStepRepeatTests.cs: a third hand-written copy of the sample identify arm, on a file this change does not touch (consolidation, pre-existing).🤖 Generated with Claude Code