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
25 changes: 25 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# C# formatting and style (CSharpier + dotnet format), see task-runner.json
dotnet husky run

# Markdown and spelling lint parity with CI, only when Markdown files are staged. The editor
# extensions only warn, so this hook is what actually gates a commit on lint errors. markdownlint and
# cspell are Node tools that do not run reliably across Windows, WSL, and Linux, so run them through
# Docker (the same way the CLIs are run elsewhere). To keep this hook reliable and never a false
# blocker, only use images that are already present locally (never pull) and skip when Docker or the
# image is unavailable; CI enforces these regardless. Warm the images with the VS Code "Lint" tasks.
# MSYS_NO_PATHCONV keeps the bind mount correct under Git Bash on Windows; it is a no-op on Linux/WSL.
docs_lint() {
image="$1"
shift
if docker image inspect "$image" >/dev/null 2>&1; then
MSYS_NO_PATHCONV=1 docker run --rm -v "$(pwd):/workdir" -w /workdir "$image" "$@" || exit 1
else
echo "husky: $image not present, skipping lint (run the VS Code Lint tasks or rely on CI)"
fi
}

if git diff --cached --name-only --diff-filter=ACMR | grep -q '\.md$'; then
if command -v docker >/dev/null 2>&1; then
docs_lint davidanson/markdownlint-cli2:latest "**/*.md"
docs_lint ghcr.io/streetsidesoftware/cspell:latest --no-progress README.md HISTORY.md
fi
fi
89 changes: 89 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,95 @@
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: CSharpier",
"type": "process",
"command": "dotnet",
"args": [
"csharpier",
"check",
"."
],
"problemMatcher": [
"$msCompile"
],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Style",
"type": "process",
"command": "dotnet",
"args": [
"format",
"style",
"--verify-no-changes",
"--severity=info",
"--verbosity=detailed"
],
"problemMatcher": [
"$msCompile"
],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Markdown",
"type": "shell",
"command": "docker",
"args": [
"run",
"--rm",
"--volume=${workspaceFolder}:/workdir",
"--workdir=/workdir",
"davidanson/markdownlint-cli2:latest",
"**/*.md"
],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: Spelling",
"type": "shell",
"command": "docker",
"args": [
"run",
"--rm",
"--volume=${workspaceFolder}:/workdir",
"--workdir=/workdir",
"ghcr.io/streetsidesoftware/cspell:latest",
"--no-progress",
"README.md",
"HISTORY.md"
],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
},
{
"label": "Lint: All (CI parity)",
"dependsOrder": "sequence",
"dependsOn": [
"Lint: CSharpier",
"Lint: Style",
"Lint: Markdown",
"Lint: Spelling"
],
"problemMatcher": [],
"presentation": {
"showReuseMessage": false,
"clear": false
}
}
]
}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ The conventions for everything under [`.github/workflows/`](./.github/workflows/

- **PlexCleaner** (`PlexCleaner/PlexCleaner.csproj`)
- The CLI application - orchestrates FFmpeg, HandBrake, MkvToolNix, MediaInfo, and 7-Zip to optimize media for Direct Play.
- Target framework: .NET 10.0, AOT compiled (`<PublishAot>true</PublishAot>`). Internals are exposed to the test project via `InternalsVisibleTo`.
- Target framework: .NET 10.0. AOT is opt-in (`<PublishAot>false</PublishAot>` by default, matching the shipped builds); the plugin loader compiles only in non-AOT builds. Internals are exposed to the test project via `InternalsVisibleTo`.
- **PlexCleanerTests** (`PlexCleanerTests/PlexCleanerTests.csproj`)
- xUnit v3 test suite. Assertions via AwesomeAssertions.
- **`Docker/`** - multi-arch Linux container build (`ubuntu:rolling`, `linux/amd64` + `linux/arm64`); runs as a `nonroot` user, mounts media under `/media`.
Expand Down
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ For formatter, EditorConfig, pre-commit hooks, line endings, and charset details
### Code Style

- Target: .NET 10.0 (`<TargetFramework>net10.0</TargetFramework>`)
- AOT compilation enabled: `<PublishAot>true</PublishAot>` in executable projects
- AOT compilation is opt-in: `<PublishAot>false</PublishAot>` by default (matching the shipped builds, which load plugins via reflection); publish with `-p:PublishAot=true` to build an AOT binary, which excludes the plugin loader
- Use C# modern features (records, pattern matching, collection expressions, implicit class extensions)
- Prefer `Debug.Assert()` for internal invariants
- Logging: Serilog with thread IDs (`Log.Information/Warning/Error`)
Expand Down
1 change: 1 addition & 0 deletions Docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ RUN apt install -y --install-suggests dotnet-sdk-10.0
COPY ./Samples/. ./Samples/.
COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
COPY ./PlexCleaner/. ./PlexCleaner/.
COPY ./Plugins/. ./Plugins/.
COPY ./.editorconfig ./
COPY ./Directory.Build.props ./
COPY ./Directory.Packages.props ./
Expand Down
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- Always log the end-of-run summary at information level, even with `--logwarning`, so the modified, error, and verify-failed counts are recorded for every processing run.
- Handle the `SIGINT`, `SIGTERM`, and `SIGQUIT` termination signals (`docker stop`, `Ctrl+C`) so processing is interrupted gracefully and the summary and exit code are logged before exit. The custom `Ctrl+Q`/`Ctrl+Z` exit keys are removed in favor of the standard signals.
- Normalize `Default` track flags instead of only warning about them: clear the flag on a lone track of a type, keep the preferred audio track as the single default when multiple are flagged, and clear all default flags on subtitle tracks.
- Added a `custom` command that loads a user-provided plugin assembly implementing `IProcessPlugin` and runs it over the media files, reusing the file iteration and processing API for bespoke re-processing or repair. Includes the `MatroskaHeaderCleanup` example plugin. Not available in AOT builds.
- Version 3.19:
- Reworked the CI/CD pipeline to a branch-scoped self-publishing model: a weekly scheduled run (and manual dispatch) publishes both `main` (stable, Docker `latest`) and `develop` (prerelease, Docker `develop`) - native executables, the multi-arch Docker image, and the GitHub release - while merges accumulate until the next run. No application changes.
- Added `WORKFLOW.md` (the canonical CI/CD specification) and `repo-config/` (rulesets and repository settings as code).
Expand Down
1 change: 1 addition & 0 deletions PlexCleaner.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@
</Folder>
<Project Path="PlexCleaner/PlexCleaner.csproj" />
<Project Path="PlexCleanerTests/PlexCleanerTests.csproj" />
<Project Path="Plugins/MatroskaHeaderCleanup/MatroskaHeaderCleanup.csproj" />
<Project Path="Sandbox/Sandbox.csproj" />
</Solution>
27 changes: 27 additions & 0 deletions PlexCleaner/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class CommandLineOptions
public string ResultsFile { get; set; } = string.Empty;
public string SchemaFile { get; set; } = string.Empty;
public string SettingsFile { get; set; } = string.Empty;
public string PluginAssembly { get; set; } = string.Empty;
}

public class CommandLineParser
Expand Down Expand Up @@ -130,6 +131,16 @@ private class CommandHandler(Func<ParseResult, int> action) : SynchronousCommand
HelpName = "boolean",
};

private readonly Option<string> _pluginAssemblyOption = new("--pluginassembly")
{
Description = "Path to a plugin assembly implementing IProcessPlugin",
HelpName = "filepath",
// Not required in AOT builds so custom can reach CustomCommand and emit the non-AOT error
#if PLUGINS
Required = true,
#endif
};

private RootCommand CreateRootCommand()
{
RootCommand rootCommand = new(
Expand Down Expand Up @@ -393,6 +404,21 @@ private RootCommand CreateRootCommand()
Options = { _schemaFileOption },
}
);
rootCommand.Subcommands.Add(
new("custom")
{
Description = "Process media files using a custom plugin assembly",
Action = new CommandHandler(_ => Program.CustomCommand()),
Options =
{
_settingsFileOption,
_mediaFilesOption,
_pluginAssemblyOption,
_parallelOption,
_threadCountOption,
},
}
);

return rootCommand;
}
Expand All @@ -414,6 +440,7 @@ public CommandLineOptions Bind()
ResultsFile = Result.GetValue(_resultsFileOption) ?? string.Empty,
SchemaFile = Result.GetValue(_schemaFileOption) ?? string.Empty,
SettingsFile = Result.GetValue(_settingsFileOption) ?? string.Empty,
PluginAssembly = Result.GetValue(_pluginAssemblyOption) ?? string.Empty,
};

return options;
Expand Down
33 changes: 33 additions & 0 deletions PlexCleaner/IProcessPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace PlexCleaner;

public static class PluginApi
{
// Bumped when the IProcessPlugin or IPluginHost contract changes in a breaking way
public const int Version = 1;
}

public interface IPluginHost
{
// Compare against the PluginApi.Version the plugin was built against
int PluginApiVersion { get; }

// Informational host details for finer compatibility decisions
string ApplicationVersion { get; }
string OperatingSystem { get; }
string Runtime { get; }

// Plugin log events flow to the host sinks and end-of-run summary
Serilog.ILogger Logger { get; }
}

public interface IProcessPlugin
{
// Used in logs and as the processing task name
string Name { get; }

// Called once before processing, return false to abort when incompatible with the host
bool Initialize(IPluginHost host);

// Called once per media file, reuse the public processing API, return false on failure
bool ProcessFile(string fileName);
}
6 changes: 5 additions & 1 deletion PlexCleaner/PlexCleaner.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<OutputType>Exe</OutputType>
<PackageId>ptr727.PlexCleaner</PackageId>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PublishAot>true</PublishAot>
<PublishAot>false</PublishAot>
<PackageProjectUrl>https://github.com/ptr727/PlexCleaner</PackageProjectUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageVersion>1.1.1.0-prerelease</PackageVersion>
Expand All @@ -31,6 +31,10 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<!-- Plugin loading uses AssemblyLoadContext and reflection, unavailable under NativeAOT -->
<PropertyGroup Condition="'$(PublishAot)' != 'true'">
<DefineConstants>$(DefineConstants);PLUGINS</DefineConstants>
</PropertyGroup>
Comment thread
ptr727 marked this conversation as resolved.
<ItemGroup>
<None Include="..\README.md">
<Pack>True</Pack>
Expand Down
Loading