Skip to content

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

Merged
ptr727 merged 14 commits into
developfrom
feature/user-plugins
Jul 1, 2026
Merged

Add custom plugin command for bespoke re-processing (IProcessPlugin)#791
ptr727 merged 14 commits into
developfrom
feature/user-plugins

Conversation

@ptr727

@ptr727 ptr727 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Problem

Verification/processing logic is enhanced over time (recently the Matroska seek-index structure check). Files whose sidecar already has StatesType.Verified set are skipped by the conditional verify gate, so newly added checks never run on an already-verified library. The only remedy today is deleting sidecars and full re-verification, which is prohibitively expensive (weeks on a large library).

Solution

A new custom command loads a user-provided plugin assembly and runs it over the media files, reusing the existing file iteration and processing API to perform a targeted re-check/repair without recompiling PlexCleaner and without full re-verification.

  • IProcessPlugin / IPluginHost public interfaces. Initialize(IPluginHost) receives a deterministic PluginApi.Version (bumped on breaking contract changes) the plugin checks for compatibility, an injected Serilog logger (mirroring the existing "caller sets logger" model), and app/OS/runtime info for finer opt-in decisions.
  • PluginLoader / PluginLoadContext (AssemblyLoadContext + AssemblyDependencyResolver) that shares the host's already-loaded assemblies, so IProcessPlugin type identity and the static Program.Config/Tools are single-instance; only the plugin's private dependencies load in isolation. Fails cleanly (missing file / no-or-multiple implementors / load error) with a logged error.
  • custom verb (--pluginassembly, plus the usual --settingsfile/--mediafiles/--parallel/--threadcount) wired through CommandLineOptions and Program.CustomCommand.
  • AOT stays supported. The loader/command body are gated behind a PLUGINS constant compiled only when not building for NativeAOT. The PublishAot default is flipped to false to match every shipped build (CI and Docker already override it). Under an explicit AOT publish the command returns a clear "requires a non-AOT build" error.
  • MatroskaHeaderCleanup example plugin — re-runs RepairMatroskaStructure (not Verified-gated) on already-verified files, the exact motivating case.
  • Docs: README "Custom Plugins" section (interface, trust model, API-stability and thread-safety notes) with both a CLI and a Docker Compose example.

Testing

  • PluginLoaderTests: loads the built example DLL, asserts single type identity and Name, and that a missing assembly returns null.
  • Full suite green (162 passed).
  • End-to-end verified on a real 3.8 GB file with a known seek-index quirk: the plugin initialized, detected Matroska seek index unusable, CuesBeforeTracks, remuxed a copy to repair, and exited 0 (original file untouched).

No version bump (part of unreleased 3.20).

ptr727 and others added 2 commits July 1, 2026 09:17
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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 16:19

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

Adds an opt-in plugin mechanism and a new custom CLI verb so users can run bespoke, targeted re-processing over existing libraries (including already-verified files) without deleting sidecars or recompiling PlexCleaner, while keeping NativeAOT builds supported by compile-time gating.

Changes:

  • Introduces public plugin contracts (IProcessPlugin, IPluginHost, PluginApi.Version) and a runtime loader based on AssemblyLoadContext.
  • Adds the custom command (--pluginassembly) wired through CLI parsing and Program.CustomCommand, with a clear non-AOT error path.
  • Adds an example plugin (MatroskaHeaderCleanup), tests for the loader, and documentation updates (README + HISTORY + cspell).

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
README.md Documents the new custom plugin command, trust/thread-safety notes, and examples (CLI + Docker Compose).
HISTORY.md Records the new custom plugin command and example plugin in release notes.
cspell.json Adds pluginassembly to avoid spelling false-positives.
PlexCleaner/Program.cs Implements Program.CustomCommand with PLUGINS gating and integrates plugin processing into the standard file iteration.
PlexCleaner/PluginLoader.cs Adds plugin assembly load context/host objects and reflection-based plugin discovery/initialization.
PlexCleaner/PlexCleaner.csproj Flips default PublishAot to false and defines PLUGINS constant when not publishing AOT.
PlexCleaner/IProcessPlugin.cs Adds public plugin API/version contract and host/process interfaces.
PlexCleaner/CommandLineOptions.cs Adds --pluginassembly option and registers the custom subcommand.
PlexCleaner.slnx Includes the example plugin project in the solution.
PlexCleanerTests/PluginLoaderTests.cs Adds tests for loading the example plugin and handling missing plugin assemblies.
PlexCleanerTests/PlexCleanerTests.csproj References the example plugin project so its DLL is available for loader tests.
Plugins/MatroskaHeaderCleanup/MatroskaHeaderCleanup.csproj Adds an example plugin project that compiles against PlexCleaner.
Plugins/MatroskaHeaderCleanup/MatroskaCleanupPlugin.cs Implements the example plugin that re-runs Matroska structure repair on already-verified files.

Comment thread PlexCleaner/PluginLoader.cs
Comment thread PlexCleaner/PluginLoader.cs
Comment thread PlexCleaner/Program.cs Outdated
- 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>

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

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment thread PlexCleaner/PluginLoader.cs
Comment thread README.md
Comment thread PlexCleanerTests/PluginLoaderTests.cs
- 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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

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

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

Comment thread README.md Outdated
Comment thread PlexCleaner/PlexCleaner.csproj
Copilot AI review requested due to automatic review settings July 1, 2026 16:43
- 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>

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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Comment thread Plugins/MatroskaHeaderCleanup/MatroskaCleanupPlugin.cs
Comment thread PlexCleanerTests/PluginLoaderTests.cs
Comment thread PlexCleaner/PluginLoader.cs Outdated
Copilot AI review requested due to automatic review settings July 1, 2026 16:48

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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.

Comment thread PlexCleanerTests/PluginLoaderTests.cs
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>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread PlexCleanerTests/PluginLoaderTests.cs
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>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread PlexCleaner/Program.cs
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>
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>
Copilot AI review requested due to automatic review settings July 1, 2026 17:11

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

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>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread PlexCleaner/CommandLineOptions.cs
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>
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>

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

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

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread PlexCleaner/PluginLoader.cs Outdated
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>
@ptr727 ptr727 merged commit c8a1a3f into develop Jul 1, 2026
12 checks passed
@ptr727 ptr727 deleted the feature/user-plugins branch July 1, 2026 17:51
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