Skip to content

Release 3.20: HEVC closed captions, logging rework, resilient HTTP, custom plugin, default-flag normalization#828

Merged
30 commits merged into
mainfrom
develop
Jul 12, 2026
Merged

Release 3.20: HEVC closed captions, logging rework, resilient HTTP, custom plugin, default-flag normalization#828
30 commits merged into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Promote develop → main for the 3.20 release (30 commits). Full notes in HISTORY.md.

Highlights

⚠️ Promotion conflicts to resolve at merge

main carries dependabot bumps not on develop (forward-only model). Per git merge-tree, exactly two files conflict, and both resolve in develop's favor with nothing lost from main:

  • Directory.Packages.propsmain bumped InsaneGenius.Utilities 3.6.2→3.6.18 and ptr727.LanguageTags 1.5.6→1.5.32; develop removed InsaneGenius.Utilities (replaced by ptr727.Utilities 4.0.7), set LanguageTags 1.5.33, Serilog 4.4.0, and added Serilog.Extensions.Logging. Take develop: 1.5.33 ≥ 1.5.32, and the InsaneGenius bump is moot (package removed).
  • .github/workflows/build-docker-task.ymlmain bumped 4 docker action pins; develop bumped the same pins to the identical SHAs and refactored platform selection (multi-arch only on a main publish, via a PLATFORMS env). Take develop: the pins are identical, so nothing from main is lost.

.github/workflows/validate-task.yml auto-merges cleanly; AGENTS.md and Dockerfile don't conflict (main didn't touch them).

Version: version.json = 3.20 (NBGV tags the release on main).

ptr727 and others added 30 commits June 30, 2026 18:43
* docs(agents): record develop-staleness and issue-closing learnings

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Correct develop-staleness check to use main-only range

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface each faulted file's Information-level remediation detail during `--logwarning` runs via an AsyncLocal per-file session and a single Serilog filter; clean files stay silent. Adds unit tests and release notes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Force the end-of-run summary at information level so it survives `--logwarning`, and handle SIGINT/SIGTERM/SIGQUIT so a stop (docker stop, Ctrl+C) cancels processing gracefully and logs the summary and exit code before exit instead of being force-killed. Replace the Console.CancelKeyPress and custom Ctrl+Q/Ctrl+Z handlers with one PosixSignalRegistration path.
Replace the warning-only (and mis-counting) VerifyTrackFlags with a cleanup step that clears redundant Default flags: a lone track of a type loses its flag, audio keeps the preferred track as the single default, and subtitles keep none. Add MkvPropEdit.ClearDefaultFlags to write flag-default=0, a dedicated ClearedDefaultFlags sidecar state, a post-encode re-run, and a re-detected-after-clearing warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…791)

* Add custom plugin command for bespoke re-processing (IProcessPlugin)

Enhanced verification checks never run on files whose sidecar is already
Verified, and full re-verification is prohibitively expensive. Add a custom
command that loads a user-provided plugin assembly and runs it over the media
files, reusing the existing file iteration and processing API for targeted
re-checks or repairs without recompiling PlexCleaner.

- IProcessPlugin / IPluginHost public interfaces with a deterministic
  PluginApi.Version the plugin checks in Initialize, plus injected logger,
  app/OS/runtime info for compatibility decisions
- PluginLoader / PluginLoadContext (AssemblyLoadContext) sharing the host's
  loaded assemblies so type identity and static state are single-instance
- custom verb (--pluginassembly) wired through CommandLineOptions and
  Program.CustomCommand
- Plugin loading gated behind the PLUGINS constant, compiled only when not
  building for NativeAOT; PublishAot default flipped to false to match every
  shipped build
- MatroskaHeaderCleanup example plugin, PluginLoaderTests, and README custom
  plugins section with CLI and Docker Compose examples

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add Custom Plugins to README table of contents

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix Docker build context and address plugin review feedback

- Dockerfile: copy the Plugins directory into the build context so
  PlexCleanerTests can resolve its project reference (the smoke-build Docker
  job failed with the plugin project "not found")
- PluginLoadContext: override LoadUnmanagedDll so a plugin's bundled native
  dependencies are probed from the plugin directory
- PluginLoader: log the assembly path on load failure since LogAndHandle only
  reports the caller member
- CustomCommand: isolate plugin exceptions to the current file so a faulty
  plugin fails that file instead of aborting the whole PLINQ run

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add application-version pinning example and second-round review fixes

- Example plugin now also pins against the tested PlexCleaner application
  version, not just PluginApi.Version: PluginApi.Version guards only the plugin
  contract, but the public API a plugin calls can change in any release, so the
  example warns when host.ApplicationVersion differs from the tested version
  (a stricter plugin could hard-pin). Documented in the plugin notes.
- PluginLoader: guard null/empty/whitespace assembly path before GetFullPath so
  an empty --pluginassembly does not resolve to the current directory
- README: note the plugin needs a public parameterless constructor and that
  processing helpers are gated by settings (e.g. ProcessOptions.Verify)
- PluginLoaderTests: assign to a non-null local after the NotBeNull assertion

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add parameterless to cspell dictionary

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix docs: parallel default wording and AOT-default consistency

- README: file processing is serial by default; plugin code must be thread-safe
  only when --parallel is enabled (it is opt-in, not the default)
- ARCHITECTURE.md / AGENTS.md: PublishAot now defaults to false to match the
  shipped builds and enable the plugin loader; AOT is opt-in via -p:PublishAot=true

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add local markdown/spell lint parity to hook and VS Code tasks

CI lint failures (cspell, markdownlint) were only caught after push because the
editor extensions are advisory and the pre-commit hook only ran the C# checks.

- pre-commit: when Markdown files are staged, run markdownlint and cspell to
  match CI. These Node tools are unreliable across Windows/WSL/Linux, so run
  them via Docker. To keep the hook reliable and never a false blocker, only use
  images already present locally (never pull) and skip when Docker or the image
  is unavailable; MSYS_NO_PATHCONV keeps the bind mount correct under Git Bash
  on Windows and is a no-op on Linux/WSL. The commit is gated on lint errors
  only when the tooling is present; CI enforces regardless.
- .vscode/tasks.json: add "Lint: CSharpier", "Lint: Style", "Lint: Markdown",
  "Lint: Spelling", and a "Lint: All (CI parity)" compound task. The Docker
  tasks also warm the images the hook reuses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Document custom command in README summary and command list

The custom plugin command was described in its own section but missing from
the 3.20 release-notes summary and the CLI command reference; add both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Resolve plugin path inside try so a malformed path fails cleanly

Path.GetFullPath can throw on invalid path characters; move it and the
File.Exists check inside the try/catch so a bad --pluginassembly value is logged
and returns null instead of crashing the process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Filter non-Matroska files in the example plugin

The custom driver passes every file (mkvFilesOnly=false, matching the flexible
TestMediaInfo precedent so plugins can target any container type). The example
plugin now skips non-Matroska files and returns true rather than reporting them
as errors, and the docs note that a plugin receives all files and should filter
by type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Document plugin sidecar consistency requirement

A plugin that modifies a file must keep the sidecar in sync or the next normal
run invalidates the saved state. Document the example flow (confirm Matroska,
GetMediaProps to initialize the sidecar, modify via a ProcessFile helper that
refreshes it), verified end to end against a real file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Comment sidecar state-flag behavior in the example plugin

RepairMatroskaStructure combines state flags (State |= ReMuxed) and only clears
Verified via SetVerifyFailed if the remux fails to repair the structure, so a
successful repair preserves an existing Verified state and avoids re-verifying.
Add a comment explaining this in the sample.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Require --pluginassembly only in non-AOT builds

In AOT builds the custom command handler emits a clear "requires a non-AOT
build" error, but Required=true made argument parsing fail first. Gate the
Required flag behind PLUGINS so custom can reach CustomCommand in AOT builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Match full assembly identity when sharing host assemblies

PluginLoadContext deferred to the default context by simple assembly name, which
could treat a plugin's private dependency as host-provided when the host loaded a
different version or strong name of the same simple name, preventing the plugin
from loading its own compatible copy. Compare the full identity (name, version,
culture, public key token) so only exact matches are shared; other assemblies
resolve through the dependency resolver in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#792)

* Validate --pluginassembly at parse time with a native FileInfo option

Use Option<FileInfo> with AcceptExistingOnly so a missing plugin path fails at
argument parsing with a clear "File does not exist" message before the command
runs, and simplify PluginLoader.Load to take a FileInfo and drop the empty-path,
GetFullPath, and File.Exists guards (the CLI now guarantees an existing path).

Scope is limited to --pluginassembly, the one path option that is a pure input
whose validation this removes; --mediafiles/--settingsfile/--schemafile keep
their current handling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Resolve plugin FullName inside the try block

FileInfo.FullName resolves the path and can throw on a malformed or too-long
path; move it inside the try so any error is logged via LogAndHandle and returns
null instead of throwing, and log the file name in the catch since it does not
resolve the path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Harden plugin null handling and add custom command parse tests

- PluginLoader.Load: guard against a null FileInfo with ArgumentNullException so
  the catch block never dereferences a null argument
- CustomCommand: log an explicit error before returning when the plugin path is
  missing so the failure is visible even though the option is required
- CommandLineTests: cover the custom command, an existing plugin path binds and
  a missing path fails at parse time via AcceptExistingOnly

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Log the resolved plugin path in the load failure message

Track the assembly path in a single variable that starts as the file name and
upgrades to the resolved full path inside the try, so the catch logs the most
specific location available (full path when resolved, file name if FullName
threw) instead of only the ambiguous file name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Gate --pluginassembly existence check to non-AOT builds

AcceptExistingOnly was applied unconditionally, so under an AOT build a
non-existent plugin path would fail at parse instead of reaching CustomCommand
and its "requires a non-AOT build" message. Gate both Required and
AcceptExistingOnly behind PLUGINS, matching the command handler.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix --logwarning log flood and log detections as warnings

The per-file log elevation was triggered by the SidecarFile.State setter calling
PerFileLogLevel.Elevate() on every non-None state change. Because nearly every
file gets Verified or ClearedDefaultFlags, nearly every file elevated, so
--logwarning showed full information output for all files instead of only files
with a warning or error. Confirmed by instrumenting Elevate() and observing the
state-change trigger fire before the leaked information events.

- Revert SidecarFile.State to a plain auto-property; only warning/error events
  elevate the per-file log level, as intended.
- Log a warning at the point each repair method detects a condition needing
  action (redundant Default flags with per-type counts, invalid language tags,
  interlaced video, tracks needing re-encode, etc.), keeping the cleanup steps at
  information level (elevated by the detection warning). This restores the
  "Multiple Default flagged tracks" style detection warning that regressed to
  information, and makes --logwarning surface every modified file.
- Replace the log-level tests that asserted state-change elevation with a test
  asserting state changes no longer elevate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Count SetFlags tracks directly instead of via GetTrackList

GetTrackList allocates and sorts a combined list; count the SetFlags tracks
directly from the per-type collections in the detection warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Evaluate metadata error conditions once and reuse in the warning

RepairMetadataRemux computed HasMetadataErrors for Remove/ReMux/SetLanguage in
the early-exit and again in the detection warning. Evaluate each once into locals
and reuse them, avoiding the duplicate GetTrackList allocations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The redundant Default flags warning reported only the Default-flagged count per
track type, so it was not clear why a track was flagged. Report the count over
the total track count per type (e.g. Video: 1/1, Audio: 1/1, Subtitle: 0/2) so
the reason is visible: 1/1 is a lone track, 2/4 is multiple defaults.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the actions-deps group with 2 updates: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) and [docker/build-push-action](https://github.com/docker/build-push-action).


Updates `docker/setup-qemu-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](docker/setup-qemu-action@0611638...96fe6ef)

Updates `docker/build-push-action` from 7.2.0 to 7.3.0
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](docker/build-push-action@f9f3042...53b7df9)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: docker/build-push-action
  dependency-version: 7.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the actions-deps group with 2 updates: [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) and [docker/login-action](https://github.com/docker/login-action).


Updates `docker/setup-buildx-action` from 4.1.0 to 4.2.0
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](docker/setup-buildx-action@d7f5e7f...bb05f3f)

Updates `docker/login-action` from 4.2.0 to 4.3.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@650006c...c99871d)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: docker/login-action
  dependency-version: 4.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
The merge-bot held semver-major NuGet bumps for human review via a
version-update:semver-major guard fed by dependabot/fetch-metadata. Per the
every-tier policy already in Utilities and LanguageTags, the required CI checks
are the gate, not the version magnitude.

- Remove the metadata step and the semver-major guard from the merge step so
  every in-repo Dependabot PR auto-merges on open once checks pass.
- Update the workflow contract to match: WORKFLOW.md self-sufficiency prose,
  merge-bot diagram, automation prose, D8.2, D8/D9 summary, S12 trace row, and
  the AGENTS.md Dependabot invariant.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the two AGENTS.md process-learning bullets (develop-staleness check and
issue-closing keywords) with the canonical wording from the sibling repos. The
old staleness bullet steered readers to git log / three-dot diff and claimed the
two-dot `git diff origin/main origin/develop` "will not do" - which is exactly
the check the canonical form now mandates (read its `-` lines; a deletion-only
hunk is the real staleness signal). Also append the template's "In a derived
repo:" upstream-drift reporting paragraph to copilot-instructions.md.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Normalize line endings and final newlines to .editorconfig spec

These files drifted from the .editorconfig line-ending rules (LF where
CRLF is mandated for .yml/.md/.json) or were missing the mandated final
newline. Bring them to spec; no content changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add Codecov coverage reporting to CI

The unit-test job collected no coverage. Emit Cobertura from the existing
dotnet test run (coverlet.collector was already referenced) and upload it
to Codecov for trending only - never gated: the upload sets
fail_ci_if_error false and codecov.yml marks status informational, so a
coverage change or Codecov outage cannot block a merge.

CODECOV_TOKEN is threaded via secrets: inherit and audited in both the
Actions and Dependabot secret stores by configure.sh, since a
Dependabot-triggered push runs the validate job too. Docs (WORKFLOW.md,
repo-config/README.md) list the new secret; AGENTS.md notes CI CLI linters
can run via Docker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add the global editorconfig CRLF default

Set end_of_line = crlf on the [*] block so every file type has a defined ending, matching the fleet template. Per-type CRLF rules and .gitattributes LF pins unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Pieter Viljoen <ptr727@users.noreply.github.com>

* Keep the husky hook LF under the CRLF default

The global [*] end_of_line = crlf also matches the extensionless .husky/pre-commit hook, which must stay LF (a CRLF shebang breaks it when run from the working tree). Add a matching editorconfig LF override, aligned with the existing .gitattributes pin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Pieter Viljoen <ptr727@users.noreply.github.com>

---------

Signed-off-by: Pieter Viljoen <ptr727@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps the actions-deps group with 2 updates: [docker/login-action](https://github.com/docker/login-action) and [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action).


Updates `docker/login-action` from 4.3.0 to 4.4.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](docker/login-action@c99871d...af1e73f)

Updates `DavidAnson/markdownlint-cli2-action` from 23.2.0 to 24.0.0
- [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases)
- [Commits](DavidAnson/markdownlint-cli2-action@ded1f94...8de2aa0)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-deps
- dependency-name: DavidAnson/markdownlint-cli2-action
  dependency-version: 24.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Dependabot and Actions rewrite workflow YAML with LF, so pin
`.github/workflows/*.{yml,yaml}` to LF in `.editorconfig` and convert the
existing files, keeping them consistent instead of mixed on every bump. Add
`.editorconfig-checker.json` and a Docker `editorconfig-checker` step after
actionlint in the lint job so CI enforces the endings. The new checker
surfaced a pre-existing LF `.dockerignore`; normalize it to CRLF like the
`[*]` default, since it is a non-executed pattern file (the parser strips a
trailing CR) rather than execution-sensitive like a Dockerfile. Update
AGENTS.md to record that workflow YAML is LF while other YAML stays CRLF.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Docker build dominates Actions minutes and arm64 is emulated on the amd64
runner, so build linux/amd64,linux/arm64 only on a full main publish; smoke
and develop build linux/amd64 only. A job-level env.PLATFORMS drives buildx
and build-push, and QEMU is set up only when arm64 is in the platform set.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Media logs identified files by base name only, which is ambiguous when
same-named files (e.g. 4K vs 1080p) live in different folders. Log the
full path in ProcessFile, SidecarFile and ProcessDriver media lines,
including the track dumps (via MediaProps construction).

Interlaced detection:
- FindInterlacedTracks is a pure predicate; it outs an FfMpegIdetInfo
  that is non-null only when the idet scan detected interlacing (null
  for a container metadata flag).
- DeInterlace logs a single warning that reports how it was detected
  (Detected by: Idet / Metadata flag) and, for idet, extracts the
  MultiFrame evidence (percentage and field counts) from the out object.

Also enrich "Tags detected" (per-tool) and "Cover Art detected" (format),
and normalise the log placeholder to {FileName} everywhere ({File} and a
stray {fileName} renamed).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

* Encapsulate interlaced decision in a self-describing reason string

Replace IsInterlaced(out double percentage) with IsInterlaced(out string
reason) so the idet types own both the verdict and its human-readable
justification. Callers log the reason string directly instead of reaching
into MultiFrame and re-deriving the percentage.

Drop FfMpegIdetInfo.WriteLine() (and the now-unused Repeated/Frames
WriteLine helpers), which logged RepeatedFields and SingleFrame lines that
never fed the decision.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Keep the boolean interlaced check allocation free and assert the idet invariant

Split the reason string construction out of the decision so the boolean only
IsInterlaced() no longer allocates on the hot idet gating path, and assert the
detected-interlaced invariant where DeInterlace logs the reason.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Assert the reason string dominant percentage in the idet tests

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Match the undetermined-majority reason wording to its >= condition

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bumps InsaneGenius.Utilities from 3.6.2 to 3.6.18
Bumps ptr727.LanguageTags from 1.5.6 to 1.5.32

---
updated-dependencies:
- dependency-name: InsaneGenius.Utilities
  dependency-version: 3.6.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-deps
- dependency-name: ptr727.LanguageTags
  dependency-version: 1.5.32
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget-deps
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
- Error summary prints the failing operation (nameof-tracked) instead of Error: None when a failure left no sidecar state.
- Media tools log their own failures with the process exit code and summarized output from the correct per-tool stream (mkvmerge on stdout), collapsed to a single line; the out error parameter was dropped to remove double-logging.
- Signal interruption reports the Unix 128 + signal exit code (SIGINT 130, SIGQUIT 131, SIGTERM 143) so callers can detect a graceful stop.
- idet parsing uses the last frame-count triple, fixing progressive files with doubled idet output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align the FFmpeg SEI NAL unit lookup with FFprobe's `hevc` codec name so H.265/HEVC closed captions are removed via filter_units, same as H.264 and MPEG-2. HDR10 and HDR10+ HEVC content remains guarded. Adds FfMpegNalUnitTests and updates the closed captions docs.
Reference every PR as a clickable markdown link (never a bare #N), and
present input requests as a numbered list so the user can reply per
number. Mirrors the canonical ProjectTemplate rule (#268).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the standard --loglevel / --logfile / --logclear logging pattern with a static LoggerFactory. Keep the per-file auto up-level feature but make it explicit via --logelevate; --logwarning and --logappend are deprecated with one-time warnings. Log events follow a consistent taxonomy (documented in AGENTS.md): Warning = decision to modify the media (elevation trigger), Information = the app doing its job, Debug = tool/read mechanics, Error = failures. Debug/Verbose are now selectable and low-level chatter is re-leveled accordingly.

Also: corrected the 3.20 interlace/idet notes to describe the parse-resilience fix; bridged library logging (ptr727.Utilities 3.7.5 and ptr727.LanguageTags) through a shared Microsoft.Extensions.Logging factory; bumped Serilog to 4.4.0.
LogFailedResult folded the ' : ' separator into a quoted string value, so the app's {Message} template rendered 'ExitCode: 0" : ...' with the quote after the exit code. Log the error summary as its own {Error} value with the separator in the template. ExitCode 0 on failure is expected: ffmpeg can exit 0 yet report a fatal error on stderr. Adds tests that exercise LogFailedResult and assert the quote placement; tests run serially since they share global static state.
FfProbe's subcc, bitrate, and temp-file paths logged the tool stderr as a second error line unconditionally. A cancelled tool (SIGTERM) produces no stderr, so this rendered an empty "" line. Route those through a LogErrorOutput helper that skips empty or whitespace-only output. Adds tests.
Replace the assembly-wide xunit.runner.json parallelizeTestCollections:false (added in #821, which slowed the whole suite) with a 'Sequential' collection using DisableParallelization, applied only to the two tests that swap the global Serilog Log.Logger. The rest of the suite parallelizes again.
Bump ptr727.Utilities to 4.0.7 and replace the plain HttpClient with the library's resilient Download API (Polly retry + circuit breaker via Microsoft.Extensions.Http.Resilience) for tool downloads and the app version check. The library factory preserves the 120s timeout and an entry-assembly user-agent (PlexCleaner/<version>); resilience events log through the wired LogOptions factory. GetLatestRelease returns false on download or parse failure rather than throwing.
…r, complete Lint tasks (#825)

Adds .editorconfig-checker.json (EOL-only), switches the editorconfig CI step to :latest + renames it, and completes the VS Code Lint group.
Local Lint tasks always pull the current :latest image (ProjectTemplate #284).
Copilot AI review requested due to automatic review settings July 12, 2026 22:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Promotes develop -> main for the 3.20 release, bringing in a broad set of functional updates (logging overhaul, resilient HTTP downloads/version checks, HEVC closed-caption removal, default-flag normalization, custom plugin command, signal handling), plus CI/lint and documentation updates aligned with the repo’s workflow contract.

Changes:

  • Reworked logging and per-file elevation (--loglevel, --logelevate, append-by-default rolling logs, deprecated flags) and propagated logging into ptr727.* libraries.
  • Switched downloads/version checks to ptr727.Utilities resilient HTTP, added custom plugin support (non-AOT), and improved media/tool processing behavior (HEVC CC removal, default flag normalization, idet parsing).
  • Hardened CI/linting (Codecov reporting, EditorConfig EOL enforcement, workflow updates) and updated release/docs/sample config artifacts for 3.20.

Reviewed changes

Copilot reviewed 72 out of 84 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
WORKFLOW.md Updates workflow contract docs (Dependabot every-tier auto-merge, Codecov token notes).
version.json Bumps version floor to 3.20 for the release.
Sandbox/Program.cs Updates sandbox to new ptr727.Utilities logging factory usage.
Samples/PlexCleaner/Sidecar.State.PlexCleaner Adds/updates persisted sidecar sample for schema/state behaviors.
Samples/PlexCleaner/PlexCleaner.v4.json Normalizes sample JSON formatting/newline.
Samples/PlexCleaner/PlexCleaner.v3.json Normalizes sample JSON formatting/newline.
Samples/PlexCleaner/PlexCleaner.v2.json Normalizes sample JSON formatting/newline.
Samples/PlexCleaner/PlexCleaner.v1.json Normalizes sample JSON formatting/newline.
repo-config/README.md Documents required secret names including Codecov token.
repo-config/configure.sh Audits Codecov token in both Actions/Dependabot secret stores.
README.md Updates 3.20 release notes, logging flags, stop-signal guidance, and adds Custom Plugins docs.
Plugins/MatroskaHeaderCleanup/MatroskaHeaderCleanup.csproj Adds example plugin project wired against PlexCleaner host assembly.
Plugins/MatroskaHeaderCleanup/MatroskaCleanupPlugin.cs Implements example IProcessPlugin for Matroska seek-index repair.
PlexCleanerTests/ToolFailureLogFormatTests.cs Adds regression tests for tool-failure log formatting.
PlexCleanerTests/PluginLoaderTests.cs Adds tests for plugin loader example assembly loading.
PlexCleanerTests/PlexCleanerTests.csproj References example plugin project so its DLL is available to tests.
PlexCleanerTests/PlexCleanerFixture.cs Updates test fixture to ptr727.Utilities logging factory and adds sequential collection definition.
PlexCleanerTests/PerFileLogLevelTests.cs Adds comprehensive tests for per-file log elevation filter behavior.
PlexCleanerTests/FfProbeLogTests.cs Adds tests ensuring cancelled tool runs don’t log empty stderr lines.
PlexCleanerTests/FfMpegNalUnitTests.cs Adds tests for HEVC/H.264/MPEG2 NAL unit mapping.
PlexCleanerTests/FfMpegIdetParsingTests.cs Adds tests ensuring idet parser uses the final stats block.
PlexCleanerTests/FfMpegIdetDecisionTests.cs Updates decision tests for new reason-string interlace verdict.
PlexCleanerTests/DefaultTrackFlagsTests.cs Adds tests for redundant/default flag clearing logic.
PlexCleanerTests/CommandLineTests.cs Updates/extends CLI parsing tests for new logging options and custom plugin option validation.
PlexCleaner/VideoProps.cs Adds level-selectable per-track logging and placeholder normalization.
PlexCleaner/TrackProps.cs Adds level-selectable per-track logging; re-levels some logs to Debug.
PlexCleaner/Tools.cs Switches tool download/info calls to ptr727.Utilities.Download and re-levels noisy logs to Debug.
PlexCleaner/SubtitleProps.cs Re-levels closed-caption flag logging to Debug.
PlexCleaner/SidecarFileJsonSchema.cs Switches utilities dependency to ptr727.Utilities.
PlexCleaner/SidecarFile.cs Adds ClearedDefaultFlags state; switches to full-path logging and re-levels sidecar/tool reads to Debug.
PlexCleaner/SevenZipTool.cs Uses resilient GitHub release lookup and centralized failure logging behavior.
PlexCleaner/Properties/launchSettings.json Normalizes formatting/newline.
PlexCleaner/Program.cs Logging factory integration, library logger propagation, deprecation warnings, POSIX-signal handling, custom command wiring.
PlexCleaner/ProcessResultJsonSchema.cs Adds FailedOperation reporting to results.
PlexCleaner/ProcessDriver.cs Re-levels enumeration/tool probing logs to Debug; forces end-of-task summary logs via override context.
PlexCleaner/PluginLoader.cs Adds plugin load context/host and plugin discovery/initialization logic (non-AOT).
PlexCleaner/PlexCleaner.csproj Makes AOT opt-in; adds PLUGINS constant gating; switches to ptr727.Utilities + Serilog MEL bridge package.
PlexCleaner/PerFileLogLevel.cs Adds per-file log-session filter with opt-in elevation behavior.
PlexCleaner/Monitor.cs Removes legacy “press keys to exit” messaging; keeps monitoring banner.
PlexCleaner/MkvPropEditTool.cs Adds default-flag clearing command and centralizes non-zero exit logging.
PlexCleaner/MkvPropEditBuilder.cs Adds ClearFlags helper for mkvpropedit argument construction.
PlexCleaner/MkvMergeTool.cs Switches network reads to resilient HTTP; centralizes tool-failure logging; treats stdout as error stream.
PlexCleaner/MediaTool.cs Adds structured failed-result logging, output sanitization, and debug-level execution logs.
PlexCleaner/MediaProps.cs Adds log level parameter for per-track dumps.
PlexCleaner/MediaInfoTool.cs Uses resilient GitHub release lookup and centralized failure logging.
PlexCleaner/LoggerFactory.cs New static logger factory (levels, rolling file behavior, per-file elevation filter, MEL bridge).
PlexCleaner/IProcessPlugin.cs Adds plugin API contracts (IProcessPlugin, IPluginHost, PluginApi).
PlexCleaner/HandBrakeTool.cs Uses resilient GitHub release lookup and centralized failure logging.
PlexCleaner/GitHubRelease.cs Implements resilient GitHub latest-release fetch with non-throwing parse behavior.
PlexCleaner/FfProbeTool.cs Centralizes stderr logging to avoid empty-error lines; re-levels execution logs.
PlexCleaner/FfMpegTool.cs Fixes HEVC codec tag mapping for CC removal; centralizes failures; adjusts verify/remux APIs.
PlexCleaner/FfMpegIdetInfo.cs Uses last idet stats block and emits self-describing interlace decision reason string.
PlexCleaner/FfMpegBuilder.cs Minor comment cleanup.
PlexCleaner/Convert.cs Adjusts for updated tool APIs and centralized failure logging.
PlexCleaner/CommandLineOptions.cs Adds --loglevel/--logelevate/--logclear and custom --pluginassembly parsing/binding; deprecates legacy flags.
PlexCleaner/Bitrate.cs Re-levels bitrate diagnostic output to Debug; switches utilities dependency.
PlexCleaner.slnx Adds plugin project to solution.
PlexCleaner.schema.json Normalizes formatting/newline.
HISTORY.md Adds detailed 3.20 release history entry.
Docs/LanguageMatching.md Normalizes formatting/indentation.
Docs/CustomOptions.md Normalizes formatting/indentation.
Docker/Dockerfile Copies Plugins into Docker build context for tests/build.
Directory.Packages.props Replaces InsaneGenius.Utilities with ptr727.Utilities, bumps Serilog/LanguageTags, adds Serilog MEL bridge.
cspell.json Adds new CLI/plugin-related words for spellchecking.
codecov.yml Configures Codecov statuses as informational (non-gating).
ARCHITECTURE.md Documents AOT as opt-in and plugin loader exclusion under AOT.
.husky/pre-commit Adds dockerized Markdown/spell linting on staged Markdown (best-effort local parity).
.gitignore Ignores coverage output directory.
.github/workflows/validate-task.yml Adds coverage collection + Codecov upload; adds EditorConfig EOL enforcement; bumps markdownlint action pin.
.github/workflows/test-pull-request.yml Inherits secrets for validate workflow to enable coverage upload.
.github/workflows/publish-release.yml Formatting-only normalization (no semantic change indicated).
.github/workflows/merge-bot-pull-request.yml Removes semver-major NuGet exception; every-tier Dependabot auto-merge.
.github/workflows/build-release-task.yml Inherits secrets for validate workflow to enable coverage upload.
.github/workflows/build-docker-task.yml Makes platforms conditional (multi-arch only on main publish), updates pinned action versions/SHAs.
.github/copilot-instructions.md Adds “report drift upstream” note for derived repos.
.editorconfig-checker.json Adds EOL-only editorconfig-checker config.
.editorconfig Sets global CRLF default; pins workflow YAML to LF; pins Husky hook to LF.
.dockerignore Normalizes formatting/newline.
.config/dotnet-tools.json Normalizes formatting/newline.

Comment thread PlexCleaner/Program.cs
Comment thread PlexCleaner/Program.cs
@ptr727 ptr727 closed this pull request by merging all changes into main in b37f901 Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants