diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index 757c0efe..1e06f05b 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -32,7 +32,7 @@ body:
value: |
OS Version: E.g. Windows 11 Pro 22H2.
Docker Version: Run `docker --version`.
- Docker Image: E.g. `latest`, `latest-debian`.
+ Docker Image: E.g. `latest`, `develop`.
`PlexCleaner getversioninfo`: Run `PlexCleaner getversioninfo`.
validations:
required: true
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..5433ae5e
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,442 @@
+# PlexCleaner AI Coding Instructions
+
+## Project Overview
+
+PlexCleaner is a .NET 10.0 CLI utility that optimizes media files for Direct Play in Plex/Emby/Jellyfin by:
+
+- Converting containers to MKV format
+- Re-encoding incompatible video/audio codecs
+- Managing tracks (language tags, duplicates, subtitles)
+- Verifying and repairing media integrity
+- Removing closed captions and unwanted content
+- Monitoring folders for changes and automatically processing new/modified files
+
+The tool orchestrates external media processing tools (FFmpeg, HandBrake, MkvToolNix, MediaInfo, 7-Zip) via CLI wrappers.
+
+## Documentation
+
+User-facing documentation is organized as follows:
+
+- **[README.md](../README.md)**: Main project documentation, quick start, installation, usage, and FAQ.
+- **[Docs/LanguageMatching.md](../Docs/LanguageMatching.md)**: Technical details on IETF/RFC 5646 language tag matching and configuration.
+- **[Docs/CustomOptions.md](../Docs/CustomOptions.md)**: FFmpeg and HandBrake custom encoding parameters, hardware acceleration setup, and encoder options.
+- **[Docs/ClosedCaptions.md](../Docs/ClosedCaptions.md)**: Detailed technical analysis of EIA-608/CTA-708 closed caption detection methods and tools.
+- **[HISTORY.md](../HISTORY.md)**: Release notes and version history.
+
+## Architecture
+
+### Command Structure
+
+PlexCleaner provides multiple commands:
+
+- **process**: Batch process media files in specified folders
+- **monitor**: Watch folders for changes and automatically process modified files
+- **verify**: Verify media files using FFmpeg
+- **remux**: Re-multiplex media files to MKV
+- **reencode**: Re-encode media tracks using HandBrake or FFmpeg
+- **deinterlace**: De-interlace media files
+- **createsidecar**: Create sidecar files for existing media
+- **gettoolinfo**: Display tool version information
+- **gettagmap**: Analyze language tags across media files
+- **getmediainfo**: Extract and display media properties
+- **checkfornewtools**: Check for and download tool updates (Windows only)
+- **defaultsettings**: Create default configuration file
+- **createschema**: Generate JSON schema for configuration validation
+- **removesubtitles**: Remove all subtitle tracks
+- **removeclosedcaptions**: Remove embedded EIA-608/CTA-708 closed captions from video streams
+- **updatesidecar**: Create or update sidecar files to current schema/tool info
+- **getsidecarinfo**: Display sidecar file information
+- **testmediainfo**: Test parsing media tool information for non-Matroska containers
+- **getversioninfo**: Print application and media tool version information
+
+### Fluent Builder Pattern for Media Tools
+
+All media tool command-line construction uses fluent builders (`*Builder.cs`). Never concatenate strings:
+
+```csharp
+// Correct - fluent builder pattern
+var command = new FfMpeg.GlobalOptions(args)
+ .Default()
+ .Add(customOption);
+
+// Wrong - string concatenation
+string args = "-hide_banner " + option;
+```
+
+### Process Execution with CliWrap
+
+All external process execution uses [CliWrap](https://github.com/Tyrrrz/CliWrap) (v3.x):
+
+- Builders create `ArgumentsBuilder` instances
+- Execute via `Cli.Wrap(toolPath).WithArguments(builder)`
+- Use `BufferedCommandResult` for output capture
+- See `MediaTool.cs` for base execution patterns
+- All tool execution supports cancellation via `Program.CancelToken()`
+
+### Sidecar File System
+
+Critical performance feature - DO NOT break compatibility:
+
+- Each `.mkv` gets a `.PlexCleaner` sidecar JSON file
+- Contains: processing state, tool versions, media properties, file hash
+- Hash: First 64KB + last 64KB of file (not timestamp-based)
+- Schema versioned (`SchemaVersion: 5` in `SidecarFileJsonSchema5`, global alias in `GlobalUsing.cs`)
+- Processing skips verified files unless sidecar invalidated
+- State flags are bitwise: `StatesType` enum with `[Flags]` attribute
+- Sidecar operations: `Create()`, `Read()`, `Update()`, `Delete()`
+
+### Media Tool Abstraction
+
+- `MediaTool` base class defines tool lifecycle
+- Each tool family has: Tool class, Builder class, Info schema
+- Tool version info retrieved from CLI output, cached in `Tools.json`
+- Windows supports auto-download via `GitHubRelease.cs`; Linux uses system tools
+- Tool paths: `ToolsOptions.UseSystem` or `RootPath + ToolFamily/SubFolder/ToolName`
+- Tool execution: Base `Execute()` method with cancellation, logging, and error handling
+- Version checking: `GetInstalledVersion()`, `GetLatestVersion()` (Windows only)
+
+### Media Properties and Track Management
+
+**MediaProps hierarchy:**
+
+- `MediaProps`: Container for all media information (video, audio, subtitle tracks)
+- `TrackProps`: Base class for all track types
+ - `VideoProps`: Video track properties (format, resolution, codec, HDR, interlacing)
+ - `AudioProps`: Audio track properties (format, channels, sample rate, codec)
+ - `SubtitleProps`: Subtitle track properties (format, codec, closed captions)
+
+**Track properties:**
+
+- Language tags: ISO 639-2B (`Language`) and RFC 5646/BCP 47 (`LanguageIetf`)
+- Flags: Default, Forced, HearingImpaired, VisualImpaired, Descriptions, Original, Commentary
+- State: Keep, Remove, ReMux, ReEncode, DeInterlace, SetFlags, SetLanguage, Unsupported
+- Title, Format, Codec, Id, Number, Uid
+
+**Track selection (`SelectMediaProps.cs`):**
+
+- Separates tracks into Selected/NotSelected categories
+- Used for language filtering, duplicate removal, codec selection
+- Move operations: `Move(track, toSelected)`, `Move(trackList, toSelected)`
+- State assignment: `SetState(selectedState, notSelectedState)`
+
+### Language Tag Management
+
+**IETF/RFC 5646 Support:**
+
+- Uses external package `ptr727.LanguageTags` for language tag parsing and matching
+- Tag format: `language-extlang-script-region-variant-extension-privateuse`
+- Matching: Left-to-right prefix matching via `LanguageLookup.IsMatch()`
+- Conversion: ISO 639-2B ↔ RFC 5646 via `GetIsoFromIetf()`, `GetIetfFromIso()`
+- Special tags: `und` (undefined), `zxx` (no linguistic content), `en` (English)
+
+**Language processing:**
+
+- MediaInfo reports both ISO 639-2B and IETF tags (if set)
+- MkvMerge normalizes to IETF tags when `SetIetfLanguageTags` enabled
+- FFprobe uses tag metadata which may differ from track metadata
+- Track validation: Checks ISO/IETF consistency, sets error states for mismatches
+
+### Monitor Mode
+
+**File system watching:**
+
+- Uses `FileSystemWatcher` to monitor specified folders
+- Monitors: Size, CreationTime, LastWrite, FileName, DirectoryName
+- Handles: Changed, Created, Deleted, Renamed events
+- Queue-based: Changes added to watch queue with timestamps
+
+**Processing logic:**
+
+- Files must "settle" (no changes for `MonitorWaitTime` seconds) before processing
+- Files must be readable (not being written) before processing
+- Retry logic: `FileRetryCount` attempts with `FileRetryWaitTime` delays
+- Cleanup: Deletes empty folders after file removal
+- Pre-process: Optional initial scan of all monitored folders on startup
+
+**Concurrency:**
+
+- Lock-based queue management (`_watchLock`)
+- Periodic processing (1-second poll interval)
+- Supports parallel processing when `--parallel` enabled
+
+### XML and JSON Parsing
+
+AOT-safe parsers in `MediaInfoXmlParser.cs`:
+
+- **MediaInfoFromXml()**: Parses specific MediaInfo XML elements into `MediaInfoToolXmlSchema.MediaInfo`
+ - Manually parses only known elements needed by PlexCleaner (id, format, language, etc.)
+ - Used by sidecar file system to parse XML output when JSON unavailable
+ - Avoids XmlSerializer (not AOT-compatible)
+- **GenericXmlToJson()**: Converts any XML file to JSON format
+ - Preserves all elements and attributes (unlike MediaInfoFromXml's selective parsing)
+ - Handles attributes: prefix with `@` for elements with children, no prefix for leaf elements
+ - Detects arrays: elements appearing multiple times become JSON arrays
+ - Two-pass algorithm: collect children to detect arrays, then write JSON
+ - Uses `XmlReader` and `Utf8JsonWriter` for streaming efficiency
+ - Special handling for MediaInfo's mixed attribute/text content format (creatingLibrary)
+- **MediaInfoXmlToJson()**: Converts parsed MediaInfo XML to MediaInfo JSON schema
+ - Bridges between XML and JSON schema types
+ - Maps only known MediaInfo track properties
+
+Parser design patterns:
+
+- Forward-only `XmlReader` with depth tracking for streaming
+- Recursive `ElementData` tree for generic XML-to-JSON conversion
+- Namespace filtering (skip `xmlns`, `xsi` attributes)
+- Special handling for MediaInfo's mixed attribute/text content format
+
+### Extensions Pattern
+
+**Modern C# 13 extension syntax:**
+
+- Uses implicit class extensions: `extension(ILogger logger)`
+- Provides context-aware helper methods
+- Examples:
+ - `LogAndPropagate()`: Log exception and return false (propagates error)
+ - `LogAndHandle()`: Log exception and return true (handles error for catch clauses)
+ - `LogOverrideContext()`: Create scoped logger with LogOverride context
+
+## Code Conventions
+
+### Formatting Standards
+
+- **Code formatter**: CSharpier (`.csharpier.json`) - primary formatter
+- **EditorConfig**: `.editorconfig` follows .NET Runtime style guide
+- **Pre-commit hooks**: Husky.Net validates style (`dotnet husky run`)
+- Line endings: CRLF for Windows files (`.cs`, `.json`, `.yml`), LF for shell scripts
+- Charset: UTF-8 without BOM
+
+### Code Style
+
+- Target: .NET 10.0 (`net10.0`)
+- AOT compilation enabled: `true` in executable projects
+- 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`)
+- Exception handling: Currently uses broad `catch(Exception)` - TODO to specialize
+- Global usings: `GlobalUsing.cs` defines project-wide type aliases (`ConfigFileJsonSchema`, `SidecarFileJsonSchema`)
+
+### Naming and Structure
+
+- JSON schemas: Generated via `JsonSchema.Net`, suffixed with version (e.g., `SidecarFileJsonSchema5`)
+- Builder methods: Return `this` for chaining
+- Media props: `*Props.cs` classes (VideoProps, AudioProps, SubtitleProps, TrackProps)
+- Options classes: `*Options.cs` for command categories (ProcessOptions, VerifyOptions, ConvertOptions, ToolsOptions)
+- Partial classes: Tool families use partial class structure (`*Tool.cs`, `*Builder.cs`)
+
+### Async and Concurrency
+
+- Main loop: Uses `WaitForCancel()` polling pattern instead of async/await
+- Tool execution: Synchronous wrappers around CliWrap async operations
+- Parallel processing: PLINQ with `AsParallel()`, `WithDegreeOfParallelism()`
+- Lock-based synchronization: `Lock` instances for collection access
+- Cancellation: Global `CancellationTokenSource` accessed via `Program.CancelToken()`
+
+## Testing
+
+### Test Framework
+
+- xUnit v3.x with `AwesomeAssertions`
+- Test project: `PlexCleanerTests/`
+- Fixture: `PlexCleanerFixture` (assembly-level, sets up defaults and logging)
+- Sample media: `Samples/PlexCleaner/` (relative path `../../../../Samples/PlexCleaner`)
+
+### Test Coverage
+
+- Command-line parsing: `CommandLineTests.cs`
+- Configuration validation: `ConfigFileTests.cs`
+- FFmpeg parsing: `FfMpegIdetParsingTests.cs`
+- Sidecar functionality: `SidecarFileTests.cs`
+- Version parsing: `VersionParsingTests.cs`
+- Wildcards: `WildcardTests.cs`
+- Filename escaping for filters: `FileNameEscapingTests.cs`
+
+### Test Execution
+
+- Task: `"dotnet: .Net Build"` for builds
+- Unit tests: `dotnet test` or VS Code test explorer
+- Docker tests: Download Matroska test files from GitHub
+- CI: Separate workflows for build tests and Docker tests
+
+## Build and Release
+
+### Local Development
+
+```bash
+# Build
+dotnet build
+
+# Format code
+dotnet csharpier .
+
+# Verify formatting
+dotnet format style --verify-no-changes --severity=info --verbosity=detailed
+
+# Run tests
+dotnet test
+
+# Pre-commit validation (automatic via Husky)
+dotnet husky run
+```
+
+### GitHub Actions
+
+- **BuildGitHubRelease.yml**: Multi-runtime matrix build (win, linux, osx × x64/arm/arm64)
+- **BuildDockerPush.yml**: Multi-arch Docker builds (linux/amd64, linux/arm64)
+- **TestBuildPr.yml** / **TestDockerPr.yml**: PR validation
+- Version info: `version.json` with Nerdbank.GitVersioning format
+- Branches: `main` (stable releases), `develop` (pre-releases)
+
+### Docker
+
+- Multi-stage builds in `Docker/Ubuntu.Rolling.Dockerfile`
+- Base image: `ubuntu:rolling` only (no longer publishing Alpine or Debian variants)
+- Supported architectures: `linux/amd64`, `linux/arm64` (no longer supporting `linux/arm/v7`)
+- Tool installation: Ubuntu package manager (apt)
+- Media tool versions match Windows versions for consistent behavior
+- Test script: `Docker/Test.sh` validates all commands
+- Version extraction: `Docker/Version.sh` captures tool versions for README
+- User: Runs as `nonroot` user in containers
+- Volumes: `/media` for media files and configuration
+
+## Common Patterns
+
+### Command-Line Parsing
+
+Uses `System.CommandLine` (v2.x):
+
+- Options defined in `CommandLineOptions.cs`
+- Binding via `CommandLineParser.Bind()`
+- No `System.CommandLine.NamingConventionBinder` (deprecated)
+- Recursive options: Available to all subcommands (`--logfile`, `--logwarning`, `--debug`)
+- Command routing: Each command maps to static method in `Program.cs`
+
+### Parallel Processing
+
+- `--parallel` flag enables concurrent file processing
+- Uses `ProcessDriver.cs` with `AsParallel()` and `WithDegreeOfParallelism()`
+- Default thread count: min(CPU/2, 4), configurable via `--threadcount`
+- Lock-based collection updates in parallel contexts
+- File grouping: Groups by path (excluding extension) to prevent concurrent access to same file
+
+### File Processing States
+
+```csharp
+[Flags]
+enum StatesType {
+ None, SetLanguage, ReMuxed, ReEncoded, DeInterlaced,
+ Repaired, RepairFailed, Verified, VerifyFailed,
+ BitrateExceeded, ClearedTags, FileReNamed, FileDeleted,
+ FileModified, ClearedCaptions, RemovedAttachments,
+ SetFlags, RemovedCoverArt
+}
+```
+
+Check states with `HasFlag()`, combine with `|=`
+
+### Configuration Schema
+
+- Settings: `PlexCleaner.defaults.json` with inline JSONC comments
+- Schema: `PlexCleaner.schema.json` (auto-generated via JsonSchema.Net)
+- Validation: JSON Schema.Net with source-generated context
+- URL schema reference: `https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json`
+- Versioned: ConfigFile schemas numbered (ConfigFileJsonSchema4, etc.)
+- Defaults: `SetDefaults()` method in each options class
+- Verification: `VerifyValues()` method validates configuration
+
+### Keep-Awake Pattern
+
+- Prevents system sleep during long operations
+- Uses `KeepAwake.cs` with Windows API calls
+- Timer-based: Refreshes every 30 seconds
+- Cross-platform: No-op on non-Windows systems
+
+### Cancellation Handling
+
+- Global token source: `Program.s_cancelSource`
+- Console handlers: Ctrl+C, Ctrl+Z, Ctrl+Q
+- Keyboard monitoring: Separate task for key press handling
+- Tool execution: All CliWrap calls use `Program.CancelToken()`
+- Graceful cleanup: Logs cancellation messages, disables file watchers
+
+## Critical Details
+
+### DO NOT
+
+- Break sidecar file compatibility (versioned schema migrations only)
+- Use string concatenation for command-line arguments (use builders)
+- Modify file timestamps unless `RestoreFileTimestamp` enabled
+- Execute media tools without CliWrap abstractions
+- Add synchronous operations in parallel processing paths
+- Use `XmlSerializer` for AOT compilation (not compatible)
+- Break language tag matching logic (IETF/ISO conversion)
+
+### DO
+
+- Add tests for media tool parsing changes (see `FfMpegIdetParsingTests.cs`)
+- Update `HISTORY.md` for notable changes
+- Use `Program.CancelToken()` for cancellation support
+- Log with context: filenames, state transitions, tool versions
+- Handle cross-platform paths (`Path.Combine`, forward slashes in Docker)
+- Use modern C# features (collection expressions, pattern matching, extensions)
+- Version schemas when making breaking changes
+- Update global using aliases in `GlobalUsing.cs` when changing schema versions
+
+### Performance Considerations
+
+- Sidecar files enable fast re-processing (skip verified files)
+- `--parallel` most effective with I/O-bound operations (re-mux)
+- `--quickscan` limits scan to 3 minutes (trades accuracy for speed)
+- `--testsnippets` creates 30s clips for testing
+- Docker logging can grow large - configure rotation externally
+- Monitor mode: Settle time prevents excessive re-processing
+
+### Special Cases
+
+**Closed Captions:**
+
+- EIA-608/EIA-708 tracks handled specially in `SubtitleProps.HandleClosedCaptions()`
+- Parsed as subtitle tracks but removed during processing
+- Track IDs formatted as `{VideoId}-CC{Number}` (e.g., `256-CC1`)
+
+**VOBSUB Subtitles:**
+
+- Require `MuxingMode` to be set for Plex compatibility
+- Missing `MuxingMode` triggers error and removal recommendation
+
+**Duplicate Tracks:**
+
+- Language-based grouping with flag preservation
+- Preferred audio codec selection via `FindPreferredAudio()`
+- Keeps one flagged track per flag type, one non-flagged track
+
+**Language Mismatches:**
+
+- ISO 639-2B vs IETF tag validation in `TrackProps.SetLanguage()`
+- Tag metadata vs track metadata differences (FFprobe specific)
+- Automatic fallback: At least one track kept even if language doesn't match
+
+## Key Files Reference
+
+- **Program.cs**: Entry point, command routing, global state, cancellation handling
+- **ProcessDriver.cs**: File enumeration, parallel processing orchestration
+- **ProcessFile.cs**: Single-file processing logic, track selection algorithms
+- **Process.cs**: High-level processing workflow, empty folder deletion
+- **SidecarFile.cs**: Sidecar creation, validation, state management, hashing
+- **MediaTool.cs**: Base class for tool abstractions, execution patterns
+- **MediaProps.cs**: Media container, track aggregation
+- **TrackProps.cs**: Base track properties, language handling, flag management
+- **VideoProps.cs / AudioProps.cs / SubtitleProps.cs**: Track-specific properties
+- **MediaInfoXmlParser.cs**: AOT-safe XML/JSON parsing (MediaInfo output)
+- **Monitor.cs**: File system watching, change queue management
+- **Convert.cs**: Re-encoding and re-muxing orchestration
+- **MkvProcess.cs**: MKV-specific operations (attachment removal, flag setting)
+- **Tools.cs**: Tool instances, version verification, update checking
+- **Language.cs**: IETF tag matching, language list extraction
+- **SelectMediaProps.cs**: Track filtering and selection logic
+- **CommandLineOptions.cs**: CLI parsing, option definitions
+- **Extensions.cs**: Logger extensions, implicit class extensions
+- **GlobalUsing.cs**: Global type aliases for schema versions
+- **KeepAwake.cs**: System sleep prevention
+- **PlexCleaner.defaults.json**: Canonical configuration reference
+- **.editorconfig** / **.csharpier.json**: Code style definitions
diff --git a/.github/workflows/BuildDockerPush.yml b/.github/workflows/BuildDockerPush.yml
index 267bfe3f..8718a4b8 100644
--- a/.github/workflows/BuildDockerPush.yml
+++ b/.github/workflows/BuildDockerPush.yml
@@ -27,12 +27,8 @@ jobs:
strategy:
matrix:
include:
- - tag: ${{ endsWith(github.ref, 'refs/heads/main') && 'debian' || 'debian-develop' }}
- file: debian.ver
- - tag: ${{ endsWith(github.ref, 'refs/heads/main') && 'alpine' || 'alpine-develop' }}
- file: alpine.ver
- - tag: ${{ endsWith(github.ref, 'refs/heads/main') && 'ubuntu' || 'ubuntu-develop' }}
- file: ubuntu.ver
+ - tag: ${{ endsWith(github.ref, 'refs/heads/main') && 'latest' || 'develop' }}
+ file: latest.ver
steps:
diff --git a/.github/workflows/BuildDockerTask.yml b/.github/workflows/BuildDockerTask.yml
index 1650b99e..010515ef 100644
--- a/.github/workflows/BuildDockerTask.yml
+++ b/.github/workflows/BuildDockerTask.yml
@@ -23,21 +23,9 @@ jobs:
strategy:
matrix:
include:
- - file: ./Docker/Debian.Stable.Dockerfile
- platforms: linux/amd64,linux/arm64,linux/arm/v7
- cache-scope: debian
- tags: |
- docker.io/ptr727/plexcleaner:${{ endsWith(github.ref, 'refs/heads/main') && 'debian' || 'debian-develop' }}
- - file: ./Docker/Alpine.Latest.Dockerfile
- platforms: linux/amd64,linux/arm64
- cache-scope: alpine
- tags: |
- docker.io/ptr727/plexcleaner:${{ endsWith(github.ref, 'refs/heads/main') && 'alpine' || 'alpine-develop' }}
- file: ./Docker/Ubuntu.Rolling.Dockerfile
platforms: linux/amd64,linux/arm64
- cache-scope: ubuntu
tags: |
- docker.io/ptr727/plexcleaner:${{ endsWith(github.ref, 'refs/heads/main') && 'ubuntu' || 'ubuntu-develop' }}
docker.io/ptr727/plexcleaner:${{ endsWith(github.ref, 'refs/heads/main') && 'latest' || 'develop' }}
docker.io/ptr727/plexcleaner:${{ needs.get-version.outputs.SemVer2 }}
@@ -49,12 +37,12 @@ jobs:
- name: Setup QEMU
uses: docker/setup-qemu-action@v3
with:
- platforms: linux/amd64,linux/arm64,linux/arm/v7
+ platforms: linux/amd64,linux/arm64
- name: Setup Buildx
uses: docker/setup-buildx-action@v3
with:
- platforms: linux/amd64,linux/arm64,linux/arm/v7
+ platforms: linux/amd64,linux/arm64
- name: Login to Docker Hub
uses: docker/login-action@v3
@@ -67,8 +55,8 @@ jobs:
uses: docker/build-push-action@v6
with:
context: .
- file: ${{ matrix.file }}
push: ${{ inputs.push }}
+ file: ${{ matrix.file }}
tags: ${{ matrix.tags }}
platforms: ${{ matrix.platforms }}
build-args: |
diff --git a/.github/workflows/BuildGitHubRelease.yml b/.github/workflows/BuildGitHubRelease.yml
index d00ea1b2..32b5cdbb 100644
--- a/.github/workflows/BuildGitHubRelease.yml
+++ b/.github/workflows/BuildGitHubRelease.yml
@@ -29,21 +29,14 @@ jobs:
needs: [test-build, get-version]
strategy:
matrix:
- include:
- - runtime: win-x64
- - runtime: linux-x64
- - runtime: linux-musl-x64
- - runtime: linux-arm
- - runtime: linux-arm64
- - runtime: osx-x64
- - runtime: osx-arm64
+ runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ]
steps:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9"
+ dotnet-version: "10"
- name: Checkout code
uses: actions/checkout@v6
@@ -52,9 +45,9 @@ jobs:
run: >-
dotnet publish ./PlexCleaner/PlexCleaner.csproj
--runtime ${{ matrix.runtime }}
- --self-contained false
--output ${{ runner.temp }}/publish/${{ matrix.runtime }}
--configuration ${{ endsWith(github.ref, 'refs/heads/main') && 'Release' || 'Debug' }}
+ -property:PublishAot=false
-property:Version=${{ needs.get-version.outputs.AssemblyVersion }}
-property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }}
-property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }}
diff --git a/.github/workflows/GetVersionTask.yml b/.github/workflows/GetVersionTask.yml
index d749bb93..55b4ffdd 100644
--- a/.github/workflows/GetVersionTask.yml
+++ b/.github/workflows/GetVersionTask.yml
@@ -29,7 +29,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9"
+ dotnet-version: "10"
- name: Checkout
uses: actions/checkout@v6
diff --git a/.github/workflows/TestBuildTask.yml b/.github/workflows/TestBuildTask.yml
index 26f7c816..fec28633 100644
--- a/.github/workflows/TestBuildTask.yml
+++ b/.github/workflows/TestBuildTask.yml
@@ -15,7 +15,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
- dotnet-version: "9"
+ dotnet-version: "10"
- name: Checkout code
uses: actions/checkout@v6
diff --git a/.gitignore b/.gitignore
index d83e0a86..8ae884af 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,5 +5,6 @@
.idea
.vs
+.DS_Store
*.user
diff --git a/.husky/task-runner.json b/.husky/task-runner.json
index eb389c75..009e6b3a 100644
--- a/.husky/task-runner.json
+++ b/.husky/task-runner.json
@@ -22,8 +22,7 @@
"style",
"--verify-no-changes",
"--severity=info",
- "--verbosity=detailed",
- "--exclude-diagnostics=IDE0055"
+ "--verbosity=detailed"
],
"include": [
"**/*.cs"
diff --git a/.vscode/launch.json b/.vscode/launch.json
index a4c4f6e6..079badb8 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -11,11 +11,11 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"--help",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -24,12 +24,12 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"process",
"--help",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -38,11 +38,11 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"--version",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -51,12 +51,12 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"getversioninfo",
"--settingsfile=PlexCleaner.json",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -65,12 +65,12 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"defaultsettings",
- "--settingsfile=PlexCleaner.json",
+ "--settingsfile=PlexCleaner.defaults.json",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"console": "internalConsole",
"stopAtEntry": false
},
@@ -79,16 +79,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"process",
"--logfile=PlexCleaner_Single.log",
"--settingsfile=PlexCleaner.json",
"--resultsfile=Results_Single.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"stopAtEntry": false,
"enableStepFiltering": false,
"justMyCode": false
@@ -98,17 +97,16 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"process",
"--logfile=PlexCleaner_Single_Quickscan.log",
"--settingsfile=PlexCleaner.json",
"--resultsfile=Results_Single_Quickscan.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -119,7 +117,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"process",
"--logfile=PlexCleaner_Single_Quickscan.log",
@@ -127,10 +125,9 @@
"--resultsfile=Results_Single_Quickscan.json",
"--testsnippets",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -141,7 +138,7 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"process",
"--logfile=PlexCleaner_Parallel_TestSnippets_Quickscan.log",
@@ -151,10 +148,9 @@
"--parallel",
"--testsnippets",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -165,16 +161,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"monitor",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--preprocess",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -185,16 +180,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"remux",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -205,16 +199,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"reencode",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -225,16 +218,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"deinterlace",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -245,16 +237,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"removeclosedcaptions",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -265,15 +256,14 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"removesubtitles",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -284,16 +274,15 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"verify",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
"--quickscan",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -304,14 +293,13 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"gettagmap",
"--settingsfile=PlexCleaner.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -322,14 +310,13 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"getmediainfo",
"--settingsfile=PlexCleaner.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -340,15 +327,14 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"testmediainfo",
"--logfile=PlexCleaner.log",
"--settingsfile=PlexCleaner.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -359,14 +345,13 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"gettoolinfo",
"--settingsfile=PlexCleaner.json",
- "--mediafiles",
- "D:\\Test"
+ "--mediafiles=D:\\Test"
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -377,12 +362,79 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0/PlexCleaner.dll",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
"args": [
"checkfornewtools",
"--settingsfile=PlexCleaner.json",
],
- "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
+ "externalConsole": false,
+ "stopAtEntry": false,
+ "enableStepFiltering": false,
+ "justMyCode": false
+ },
+ {
+ "name": "Create Sidecar",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": ".Net Build",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
+ "args": [
+ "createsidecar",
+ "--settingsfile=PlexCleaner.json",
+ "--mediafiles=D:\\Test"
+ ],
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
+ "externalConsole": false,
+ "stopAtEntry": false,
+ "enableStepFiltering": false,
+ "justMyCode": false
+ },
+ {
+ "name": "Update Sidecar",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": ".Net Build",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
+ "args": [
+ "updatesidecar",
+ "--settingsfile=PlexCleaner.json",
+ "--mediafiles=D:\\Test"
+ ],
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
+ "externalConsole": false,
+ "stopAtEntry": false,
+ "enableStepFiltering": false,
+ "justMyCode": false
+ },
+ {
+ "name": "Get Sidecar Info",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": ".Net Build",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
+ "args": [
+ "getsidecarinfo",
+ "--settingsfile=PlexCleaner.json",
+ "--mediafiles=D:\\Test"
+ ],
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
+ "externalConsole": false,
+ "stopAtEntry": false,
+ "enableStepFiltering": false,
+ "justMyCode": false
+ },
+ {
+ "name": "Create Schema",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": ".Net Build",
+ "program": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0/PlexCleaner.dll",
+ "args": [
+ "createschema",
+ "--schemafile=PlexCleaner.schema.json"
+ ],
+ "cwd": "${workspaceFolder}/PlexCleaner/bin/Debug/net10.0",
"externalConsole": false,
"stopAtEntry": false,
"enableStepFiltering": false,
@@ -393,9 +445,9 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": ".Net Build",
- "program": "${workspaceFolder}/Sandbox/bin/Debug/net9.0/Sandbox.dll",
+ "program": "${workspaceFolder}/Sandbox/bin/Debug/net10.0/Sandbox.dll",
"args": [],
- "cwd": "${workspaceFolder}/Sandbox/bin/Debug/net9.0",
+ "cwd": "${workspaceFolder}/Sandbox/bin/Debug/net10.0",
"stopAtEntry": false,
"enableStepFiltering": false,
"justMyCode": false
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 58661e7a..957454dc 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -3,8 +3,13 @@
"tasks": [
{
"label": ".Net Build",
- "type": "dotnet",
- "task": "build",
+ "type": "process",
+ "command": "dotnet",
+ "args": [
+ "build",
+ "${workspaceFolder}",
+ "--verbosity=diagnostic"
+ ],
"group": "build",
"problemMatcher": [
"$msCompile"
@@ -23,8 +28,7 @@
"style",
"--verify-no-changes",
"--severity=info",
- "--verbosity=detailed",
- "--exclude-diagnostics=IDE0055"
+ "--verbosity=detailed"
],
"problemMatcher": [
"$msCompile"
@@ -34,6 +38,7 @@
"clear": false
},
"dependsOn": [
+ "CSharpier Format",
".Net Build"
]
},
@@ -126,92 +131,11 @@
"clear": false
}
},
- {
- "label": "Build Alpine.Edge Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--platform=linux/amd64,linux/arm64",
- "--file=./Docker/Alpine.Edge.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Build Alpine.Latest Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--platform=linux/amd64,linux/arm64",
- "--file=./Docker/Alpine.Latest.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Build Debian.Stable Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--platform=linux/amd64,linux/arm64,linux/arm/v7",
- "--file=./Docker/Debian.Stable.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Build Debian.Testing Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--platform=linux/amd64,linux/arm64,linux/arm/v7",
- "--file=./Docker/Debian.Testing.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
{
"label": "Build all Dockerfiles",
"dependsOrder": "parallel",
"dependsOn": [
"Build Ubuntu.Rolling Dockerfile",
- "Build Ubuntu.Devel Dockerfile",
- "Build Alpine.Edge Dockerfile",
- "Build Alpine.Latest Dockerfile",
- "Build Debian.Stable Dockerfile",
- "Build Debian.Testing Dockerfile"
],
"problemMatcher": [],
"presentation": {
@@ -261,100 +185,11 @@
"clear": false
}
},
- {
- "label": "Load Alpine.Edge Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--load",
- "--platform=linux/amd64",
- "--tag=plexcleaner:alpine-edge",
- "--file=./Docker/Alpine.Edge.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Load Alpine.Latest Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--load",
- "--platform=linux/amd64",
- "--tag=plexcleaner:alpine",
- "--file=./Docker/Alpine.Latest.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Load Debian.Stable Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--load",
- "--platform=linux/amd64",
- "--tag=plexcleaner:debian",
- "--file=./Docker/Debian.Stable.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Load Debian.Testing Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "buildx",
- "build",
- "--load",
- "--platform=linux/amd64",
- "--tag=plexcleaner:debian-testing",
- "--file=./Docker/Debian.Testing.Dockerfile",
- "${workspaceFolder}"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
{
"label": "Load all Dockerfiles",
"dependsOrder": "parallel",
"dependsOn": [
"Load Ubuntu.Rolling Dockerfile",
- "Load Ubuntu.Devel Dockerfile",
- "Load Alpine.Edge Dockerfile",
- "Load Alpine.Latest Dockerfile",
- "Load Debian.Stable Dockerfile",
- "Load Debian.Testing Dockerfile"
],
"problemMatcher": [],
"presentation": {
@@ -410,112 +245,11 @@
"clear": false
}
},
- {
- "label": "Test Alpine.Edge Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "run",
- "-it",
- "--rm",
- "--name=PlexCleaner-Test-Alpine.Edge",
- "plexcleaner:alpine-edge",
- "/Test/Test.sh"
- ],
- "dependsOrder": "sequence",
- "dependsOn": [
- "Load Alpine.Edge Dockerfile"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Test Alpine.Latest Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "run",
- "-it",
- "--rm",
- "--name=PlexCleaner-Test-Alpine.Latest",
- "plexcleaner:alpine",
- "/Test/Test.sh"
- ],
- "dependsOrder": "sequence",
- "dependsOn": [
- "Load Alpine.Latest Dockerfile"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Test Debian.Stable Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "run",
- "-it",
- "--rm",
- "--name=PlexCleaner-Test-Debian.Stable",
- "plexcleaner:debian",
- "/Test/Test.sh"
- ],
- "dependsOrder": "sequence",
- "dependsOn": [
- "Load Debian.Stable Dockerfile"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
- {
- "label": "Test Debian.Testing Dockerfile",
- "type": "shell",
- "command": "docker",
- "args": [
- "run",
- "-it",
- "--rm",
- "--name=PlexCleaner-Test-Debian.Testing",
- "plexcleaner:debian-testing",
- "/Test/Test.sh"
- ],
- "dependsOrder": "sequence",
- "dependsOn": [
- "Load Debian.Testing Dockerfile"
- ],
- "problemMatcher": [
- "$msCompile"
- ],
- "presentation": {
- "showReuseMessage": false,
- "clear": false
- }
- },
{
"label": "Test all Dockerfiles",
"dependsOrder": "parallel",
"dependsOn": [
"Test Ubuntu.Rolling Dockerfile",
- "Test Ubuntu.Devel Dockerfile",
- "Test Alpine.Edge Dockerfile",
- "Test Alpine.Latest Dockerfile",
- "Test Debian.Stable Dockerfile",
- "Test Debian.Testing Dockerfile"
],
"problemMatcher": [],
"presentation": {
diff --git a/Docker/Alpine.Edge.Dockerfile b/Docker/Alpine.Edge.Dockerfile
deleted file mode 100644
index f3ab970c..00000000
--- a/Docker/Alpine.Edge.Dockerfile
+++ /dev/null
@@ -1,134 +0,0 @@
-# Description: Alpine development release
-# Based on: alpine:edge
-# .NET install: Alpine repository
-# Platforms: linux/amd64, linux/arm64
-# Tag: ptr727/plexcleaner:alpine-edge
-
-# Docker build debugging:
-# --progress=plain
-# --no-cache
-
-# Test image in shell:
-# docker run -it --rm --pull always --name Testing alpine:edge /bin/sh
-# docker run -it --rm --pull always --name Testing ptr727/plexcleaner:alpine-edge /bin/sh
-
-# Build Dockerfile
-# docker buildx create --name plexcleaner --use
-# docker buildx build --platform linux/amd64,linux/arm64 --file ./Docker/Alpine.Edge.Dockerfile .
-
-# Test linux/amd64 target
-# docker buildx build --load --platform linux/amd64 --tag plexcleaner:alpine-edge --file ./Docker/Alpine.Edge.Dockerfile .
-# docker run -it --rm --name PlexCleaner-Test plexcleaner:alpine-edge /bin/sh
-
-
-# Builder layer
-FROM --platform=$BUILDPLATFORM alpine:edge AS builder
-
-# Layer workdir
-WORKDIR /Builder
-
-# Build platform args
-ARG TARGETPLATFORM \
- TARGETARCH \
- BUILDPLATFORM
-
-# PlexCleaner build attribute configuration
-ARG BUILD_CONFIGURATION="Debug" \
- BUILD_VERSION="1.0.0.0" \
- BUILD_FILE_VERSION="1.0.0.0" \
- BUILD_ASSEMBLY_VERSION="1.0.0.0" \
- BUILD_INFORMATION_VERSION="1.0.0.0" \
- BUILD_PACKAGE_VERSION="1.0.0.0"
-
-# Upgrade
-RUN apk update \
- && apk upgrade
-
-# Install .NET SDK
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/dotnet9-sdk
-RUN apk add --no-cache dotnet9-sdk
-
-# Copy source and unit tests
-COPY ./Samples/. ./Samples/.
-COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
-COPY ./PlexCleaner/. ./PlexCleaner/.
-
-# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
-RUN ./UnitTest.sh
-
-# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
-RUN ./Build.sh
-
-
-# Final layer
-FROM alpine:edge AS final
-
-# Image label
-ARG LABEL_VERSION="1.0.0.0"
-LABEL name="PlexCleaner" \
- version=${LABEL_VERSION} \
- description="Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc." \
- maintainer="Pieter Viljoen "
-
-# Enable .NET globalization, set default locale to en_US.UTF-8, and timezone to UTC
-# https://github.com/dotnet/dotnet-docker/blob/main/samples/dotnetapp/Dockerfile.alpine-icu
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
- LANG=en_US.UTF-8 \
- LANGUAGE=en_US:en \
- LC_ALL=en_US.UTF-8 \
- TZ=Etc/UTC
-
-# Upgrade
-RUN apk update \
- && apk upgrade
-
-# Install dependencies
-RUN apk add --no-cache \
- icu-data-full \
- icu-libs \
- p7zip \
- tzdata \
- wget
-
-# Install .NET Runtime
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/dotnet9-runtime
-RUN apk add --no-cache dotnet9-runtime
-
-# Install media tools
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/ffmpeg
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/mediainfo
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/mkvtoolnix
-# https://pkgs.alpinelinux.org/package/edge/community/x86_64/handbrake
-RUN apk add --no-cache \
- ffmpeg\
- handbrake \
- mediainfo \
- mkvtoolnix
-
-# Copy PlexCleaner from builder layer
-COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
-
-# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
-
-# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
- && rm -rf ./InstallDebugTools.sh
-
-# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
-
-# Print version information
-ARG TARGETPLATFORM \
- BUILDPLATFORM
-RUN if [ "$BUILDPLATFORM" = "$TARGETPLATFORM" ]; then \
- /PlexCleaner/Version.sh; \
- fi
diff --git a/Docker/Alpine.Latest.Dockerfile b/Docker/Alpine.Latest.Dockerfile
deleted file mode 100644
index 6156a53c..00000000
--- a/Docker/Alpine.Latest.Dockerfile
+++ /dev/null
@@ -1,134 +0,0 @@
-# Description: Alpine latest release
-# Based on: alpine:latest
-# .NET install: Alpine repository
-# Platforms: linux/amd64, linux/arm64
-# Tag: ptr727/plexcleaner:alpine
-
-# Docker build debugging:
-# --progress=plain
-# --no-cache
-
-# Test image in shell:
-# docker run -it --rm --pull always --name Testing alpine:latest /bin/sh
-# docker run -it --rm --pull always --name Testing ptr727/plexcleaner:alpine /bin/sh
-
-# Build Dockerfile
-# docker buildx create --name plexcleaner --use
-# docker buildx build --platform linux/amd64,linux/arm64 --file ./Docker/Alpine.Latest.Dockerfile .
-
-# Test linux/amd64 target
-# docker buildx build --load --platform linux/amd64 --tag plexcleaner:alpine --file ./Docker/Alpine.Latest.Dockerfile .
-# docker run -it --rm --name PlexCleaner-Test plexcleaner:alpine /bin/sh
-
-
-# Builder layer
-FROM --platform=$BUILDPLATFORM alpine:latest AS builder
-
-# Layer workdir
-WORKDIR /Builder
-
-# Build platform args
-ARG TARGETPLATFORM \
- TARGETARCH \
- BUILDPLATFORM
-
-# PlexCleaner build attribute configuration
-ARG BUILD_CONFIGURATION="Debug" \
- BUILD_VERSION="1.0.0.0" \
- BUILD_FILE_VERSION="1.0.0.0" \
- BUILD_ASSEMBLY_VERSION="1.0.0.0" \
- BUILD_INFORMATION_VERSION="1.0.0.0" \
- BUILD_PACKAGE_VERSION="1.0.0.0"
-
-# Upgrade
-RUN apk update \
- && apk upgrade
-
-# Install .NET SDK
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/dotnet9-sdk
-RUN apk add --no-cache dotnet9-sdk
-
-# Copy source and unit tests
-COPY ./Samples/. ./Samples/.
-COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
-COPY ./PlexCleaner/. ./PlexCleaner/.
-
-# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
-RUN ./UnitTest.sh
-
-# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
-RUN ./Build.sh
-
-
-# Final layer
-FROM alpine:latest AS final
-
-# Image label
-ARG LABEL_VERSION="1.0.0.0"
-LABEL name="PlexCleaner" \
- version=${LABEL_VERSION} \
- description="Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc." \
- maintainer="Pieter Viljoen "
-
-# Enable .NET globalization, set default locale to en_US.UTF-8, and timezone to UTC
-# https://github.com/dotnet/dotnet-docker/blob/main/samples/dotnetapp/Dockerfile.alpine-icu
-ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false \
- LANG=en_US.UTF-8 \
- LANGUAGE=en_US:en \
- LC_ALL=en_US.UTF-8 \
- TZ=Etc/UTC
-
-# Upgrade
-RUN apk update \
- && apk upgrade
-
-# Install dependencies
-RUN apk add --no-cache \
- icu-data-full \
- icu-libs \
- p7zip \
- tzdata \
- wget
-
-# Install .NET Runtime
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/dotnet9-runtime
-RUN apk add --no-cache dotnet9-runtime
-
-# Install media tools
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/ffmpeg
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/mediainfo
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/mkvtoolnix
-# https://pkgs.alpinelinux.org/package/v3.21/community/x86_64/handbrake
-RUN apk add --no-cache \
- ffmpeg\
- handbrake \
- mediainfo \
- mkvtoolnix
-
-# Copy PlexCleaner from builder layer
-COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
-
-# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
-
-# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
- && rm -rf ./InstallDebugTools.sh
-
-# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
-
-# Print version information
-ARG TARGETPLATFORM \
- BUILDPLATFORM
-RUN if [ "$BUILDPLATFORM" = "$TARGETPLATFORM" ]; then \
- /PlexCleaner/Version.sh; \
- fi
diff --git a/Docker/Build.sh b/Docker/Build.sh
index 0f631697..1d43903e 100644
--- a/Docker/Build.sh
+++ b/Docker/Build.sh
@@ -9,9 +9,9 @@ set -e
# Build release and debug builds
dotnet publish ./PlexCleaner/PlexCleaner.csproj \
--arch $TARGETARCH \
- --self-contained false \
--output ./Build/Release \
--configuration release \
+ -property:PublishAot=false \
-property:Version=$BUILD_VERSION \
-property:FileVersion=$BUILD_FILE_VERSION \
-property:AssemblyVersion=$BUILD_ASSEMBLY_VERSION \
@@ -20,9 +20,9 @@ dotnet publish ./PlexCleaner/PlexCleaner.csproj \
dotnet publish ./PlexCleaner/PlexCleaner.csproj \
--arch $TARGETARCH \
- --self-contained false \
--output ./Build/Debug \
--configuration debug \
+ -property:PublishAot=false \
-property:Version=$BUILD_VERSION \
-property:FileVersion=$BUILD_FILE_VERSION \
-property:AssemblyVersion=$BUILD_ASSEMBLY_VERSION \
diff --git a/Docker/Debian.Stable.Dockerfile b/Docker/Debian.Stable.Dockerfile
deleted file mode 100644
index 7b053abb..00000000
--- a/Docker/Debian.Stable.Dockerfile
+++ /dev/null
@@ -1,192 +0,0 @@
-# Description: Debian latest release
-# Based on: debian:stable-slim
-# .NET install: Install script
-# Platforms: linux/amd64, linux/arm64, linux/arm/v7
-# Tag: ptr727/plexcleaner:debian
-
-# Docker build debugging:
-# --progress=plain
-# --no-cache
-
-# Test image in shell:
-# docker run -it --rm --pull always --name Testing debian:stable-slim /bin/bash
-# docker run -it --rm --pull always --name Testing ptr727/plexcleaner:debian /bin/bash
-# export DEBIAN_FRONTEND=noninteractive
-
-# Build Dockerfile
-# docker buildx create --name "plexcleaner" --use
-# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --file ./Docker/Debian.Stable.Dockerfile .
-
-# Test linux/amd64 target
-# docker buildx build --load --platform linux/amd64 --tag plexcleaner:debian --file ./Docker/Debian.Stable.Dockerfile .
-# docker run -it --rm --name PlexCleaner-Test plexcleaner:debian /bin/bash
-
-
-# Builder layer
-FROM --platform=$BUILDPLATFORM debian:stable-slim AS builder
-
-# Layer workdir
-WORKDIR /Builder
-
-# Build platform args
-ARG TARGETPLATFORM \
- TARGETARCH \
- BUILDPLATFORM
-
-# PlexCleaner build attribute configuration
-ARG BUILD_CONFIGURATION="Debug" \
- BUILD_VERSION="1.0.0.0" \
- BUILD_FILE_VERSION="1.0.0.0" \
- BUILD_ASSEMBLY_VERSION="1.0.0.0" \
- BUILD_INFORMATION_VERSION="1.0.0.0" \
- BUILD_PACKAGE_VERSION="1.0.0.0"
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install dependencies
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- lsb-release \
- wget
-
-# Install .NET SDK
-# https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual
-# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script
-# https://github.com/dotnet/core/blob/main/release-notes/9.0/os-packages.md
-# https://github.com/dotnet/dotnet-docker/blob/main/src/sdk/9.0/bookworm-slim/amd64/Dockerfile
-# https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/9.0/bookworm-slim/amd64/Dockerfile
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- curl \
- libc6 \
- libgcc-s1 \
- libicu76 \
- libssl3t64 \
- libstdc++6 \
- tzdata \
- wget \
- && wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
- && chmod ug=rwx,o=rx dotnet-install.sh \
- && ./dotnet-install.sh --install-dir /usr/local/bin/dotnet --channel 9.0 \
- && rm dotnet-install.sh
-ENV DOTNET_ROOT=/usr/local/bin/dotnet \
- PATH=$PATH:/usr/local/bin/dotnet:/usr/local/bin/dotnet/tools \
- DOTNET_USE_POLLING_FILE_WATCHER=true \
- DOTNET_RUNNING_IN_CONTAINER=true
-
-# Copy source and unit tests
-COPY ./Samples/. ./Samples/.
-COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
-COPY ./PlexCleaner/. ./PlexCleaner/.
-
-# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
-RUN ./UnitTest.sh
-
-# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
-RUN ./Build.sh
-
-
-# Final layer
-FROM debian:stable-slim AS final
-
-# Image label
-ARG LABEL_VERSION="1.0.0.0"
-LABEL name="PlexCleaner" \
- version=${LABEL_VERSION} \
- description="Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc." \
- maintainer="Pieter Viljoen "
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install dependencies
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- locales \
- locales-all \
- lsb-release \
- p7zip-full \
- tzdata \
- wget \
- && locale-gen --no-purge en_US en_US.UTF-8
-
-# Set locale to UTF-8 after running locale-gen
-# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md
-ENV TZ=Etc/UTC \
- LANG=en_US.UTF-8 \
- LANGUAGE=en_US:en \
- LC_ALL=en_US.UTF-8
-
-# Install .NET runtime
-# Keep dependencies in sync with SDK install step
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- curl \
- libc6 \
- libgcc-s1 \
- libicu76 \
- libssl3t64 \
- libstdc++6 \
- tzdata \
- wget \
-&& wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
- && chmod ug=rwx,o=rx dotnet-install.sh \
- && ./dotnet-install.sh --install-dir /usr/local/bin/dotnet --runtime dotnet --channel 9.0 \
- && rm dotnet-install.sh
-ENV DOTNET_ROOT=/usr/local/bin/dotnet \
- PATH=$PATH:/usr/local/bin/dotnet:/usr/local/bin/dotnet/tools \
- DOTNET_USE_POLLING_FILE_WATCHER=true \
- DOTNET_RUNNING_IN_CONTAINER=true
-
-# Install media tools
-# https://tracker.debian.org/pkg/ffmpeg
-# https://tracker.debian.org/pkg/handbrake
-# https://tracker.debian.org/pkg/mediainfo
-# https://tracker.debian.org/pkg/mkvtoolnix
-RUN apt install -y --no-install-recommends \
- ffmpeg \
- handbrake-cli \
- mediainfo \
- mkvtoolnix
-
-# Cleanup
-RUN apt autoremove -y \
- && apt clean \
- && rm -rf /var/lib/apt/lists/*
-
-# Copy PlexCleaner from builder layer
-COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
-
-# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
-
-# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
- && rm -rf ./InstallDebugTools.sh
-
-# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
-
-# Print version information
-ARG TARGETPLATFORM \
- BUILDPLATFORM
-RUN if [ "$BUILDPLATFORM" = "$TARGETPLATFORM" ]; then \
- /PlexCleaner/Version.sh; \
- fi
diff --git a/Docker/Debian.Testing.Dockerfile b/Docker/Debian.Testing.Dockerfile
deleted file mode 100644
index c430cda8..00000000
--- a/Docker/Debian.Testing.Dockerfile
+++ /dev/null
@@ -1,192 +0,0 @@
-# Description: Debian development release
-# Based on: debian:testing-slim
-# .NET install: Install script
-# Platforms: linux/amd64, linux/arm64, linux/arm/v7
-# Tag: ptr727/plexcleaner:debian-testing
-
-# Docker build debugging:
-# --progress=plain
-# --no-cache
-
-# Test image in shell:
-# docker run -it --rm --pull always --name Testing debian:testing-slim /bin/bash
-# docker run -it --rm --pull always --name Testing ptr727/plexcleaner:debian-testing /bin/bash
-# export DEBIAN_FRONTEND=noninteractive
-
-# Build Dockerfile
-# docker buildx create --name "plexcleaner" --use
-# docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --file ./Docker/Debian.Testing.Dockerfile .
-
-# Test linux/amd64 target
-# docker buildx build --load --platform linux/amd64 --tag plexcleaner:debian-testing --file ./Docker/Debian.Testing.Dockerfile .
-# docker run -it --rm --name PlexCleaner-Test plexcleaner:debian-testing /bin/bash
-
-
-# Builder layer
-FROM --platform=$BUILDPLATFORM debian:testing-slim AS builder
-
-# Layer workdir
-WORKDIR /Builder
-
-# Build platform args
-ARG TARGETPLATFORM \
- TARGETARCH \
- BUILDPLATFORM
-
-# PlexCleaner build attribute configuration
-ARG BUILD_CONFIGURATION="Debug" \
- BUILD_VERSION="1.0.0.0" \
- BUILD_FILE_VERSION="1.0.0.0" \
- BUILD_ASSEMBLY_VERSION="1.0.0.0" \
- BUILD_INFORMATION_VERSION="1.0.0.0" \
- BUILD_PACKAGE_VERSION="1.0.0.0"
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install dependencies
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- lsb-release \
- wget
-
-# Install .NET SDK
-# https://learn.microsoft.com/en-us/dotnet/core/install/linux-scripted-manual
-# https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-install-script
-# https://github.com/dotnet/core/blob/main/release-notes/9.0/os-packages.md
-# https://github.com/dotnet/dotnet-docker/blob/main/src/sdk/10.0/trixie-slim/amd64/Dockerfile
-# https://github.com/dotnet/dotnet-docker/blob/main/src/runtime-deps/10.0/trixie-slim/amd64/Dockerfile
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- curl \
- libc6 \
- libgcc-s1 \
- libicu76 \
- libssl3t64 \
- libstdc++6 \
- tzdata \
- wget \
- && wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
- && chmod ug=rwx,o=rx dotnet-install.sh \
- && ./dotnet-install.sh --install-dir /usr/local/bin/dotnet --channel 9.0 \
- && rm dotnet-install.sh
-ENV DOTNET_ROOT=/usr/local/bin/dotnet \
- PATH=$PATH:/usr/local/bin/dotnet:/usr/local/bin/dotnet/tools \
- DOTNET_USE_POLLING_FILE_WATCHER=true \
- DOTNET_RUNNING_IN_CONTAINER=true
-
-# Copy source and unit tests
-COPY ./Samples/. ./Samples/.
-COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
-COPY ./PlexCleaner/. ./PlexCleaner/.
-
-# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
-RUN ./UnitTest.sh
-
-# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
-RUN ./Build.sh
-
-
-# Final layer
-FROM debian:testing-slim AS final
-
-# Image label
-ARG LABEL_VERSION="1.0.0.0"
-LABEL name="PlexCleaner" \
- version=${LABEL_VERSION} \
- description="Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc." \
- maintainer="Pieter Viljoen "
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install dependencies
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- locales \
- locales-all \
- lsb-release \
- p7zip-full \
- tzdata \
- wget \
- && locale-gen --no-purge en_US en_US.UTF-8
-
-# Set locale to UTF-8 after running locale-gen
-# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md
-ENV TZ=Etc/UTC \
- LANG=en_US.UTF-8 \
- LANGUAGE=en_US:en \
- LC_ALL=en_US.UTF-8
-
-# Install .NET runtime
-# Keep dependencies in sync with SDK install step
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- curl \
- libc6 \
- libgcc-s1 \
- libicu76 \
- libssl3t64 \
- libstdc++6 \
- tzdata \
- wget \
- && wget https://dot.net/v1/dotnet-install.sh -O dotnet-install.sh \
- && chmod ug=rwx,o=rx dotnet-install.sh \
- && ./dotnet-install.sh --install-dir /usr/local/bin/dotnet --runtime dotnet --channel 9.0 \
- && rm dotnet-install.sh
-ENV DOTNET_ROOT=/usr/local/bin/dotnet \
- PATH=$PATH:/usr/local/bin/dotnet:/usr/local/bin/dotnet/tools \
- DOTNET_USE_POLLING_FILE_WATCHER=true \
- DOTNET_RUNNING_IN_CONTAINER=true
-
-# Install media tools
-# https://tracker.debian.org/pkg/ffmpeg
-# https://tracker.debian.org/pkg/handbrake
-# https://tracker.debian.org/pkg/mediainfo
-# https://tracker.debian.org/pkg/mkvtoolnix
-RUN apt install -y --no-install-recommends \
- ffmpeg \
- handbrake-cli \
- mediainfo \
- mkvtoolnix
-
-# Cleanup
-RUN apt autoremove -y \
- && apt clean \
- && rm -rf /var/lib/apt/lists/*
-
-# Copy PlexCleaner from builder layer
-COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
-
-# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
-
-# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
- && rm -rf ./InstallDebugTools.sh
-
-# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
-
-# Print version information
-ARG TARGETPLATFORM \
- BUILDPLATFORM
-RUN if [ "$BUILDPLATFORM" = "$TARGETPLATFORM" ]; then \
- /PlexCleaner/Version.sh; \
- fi
diff --git a/Docker/README.m4 b/Docker/README.m4
index 923ddf3c..4956ea1f 100644
--- a/Docker/README.m4
+++ b/Docker/README.m4
@@ -15,42 +15,21 @@ Binary releases are published on [GitHub Releases](https://github.com/ptr727/Ple
Docker images are published on [Docker Hub](https://hub.docker.com/r/ptr727/plexcleaner).\
Images are updated weekly with the latest upstream updates.
-## Docker Builds and Tags
+## Usage
-- `latest`: Alias for `ubuntu`.
-- `develop`: Alias for `ubuntu-develop`.
-- `ubuntu`: Based on [Ubuntu Rolling](https://releases.ubuntu.com/) `ubuntu:rolling` latest stable release base image.
- - Multi-architecture image supporting `linux/amd64` and `linux/arm64` builds.
-- `alpine`: Based on [Alpine Latest](https://alpinelinux.org/releases/) `alpine:latest` latest stable release base image.
- - Multi-architecture image supporting `linux/amd64` and `linux/arm64` builds.
-- `debian`: Based on [Debian Stable](https://www.debian.org/releases/) `debian:stable-slim` latest stable release base image.
- - Multi-architecture image supporting `linux/amd64`, `linux/arm64`, and `linux/arm/v7` builds.
-- `*-develop` : Builds from the pre-release [develop branch](https://github.com/ptr727/PlexCleaner/tree/develop).
-
-## Platform Support
-
-| Tag | `linux/amd64` | `linux/arm64` | `linux/arm/v7` | Size |
-| --- | --- | --- | --- | --- |
-| `ubuntu` | ☑ | ☑ | ☐ | ~350MB |
-| `alpine` | ☑ | ☑ | ☐ | ~156MB |
-| `debian` | ☑ | ☑ | ☑ | ~330MB |
-
-## Media Tool Versions
-
-### `ptr727/plexcleaner:ubuntu`
+Refer to the [project](https://github.com/ptr727/PlexCleaner) page.
-```text
-include({{ubuntu.ver}})
-```
-
-### `ptr727/plexcleaner:debian`
+## Docker Tags
-```text
-include({{debian.ver}})
-```
+- `latest`:
+ - Based on [Ubuntu Rolling](https://releases.ubuntu.com/) `ubuntu:rolling` latest stable release base image.
+ - Multi-architecture image supporting `linux/amd64` and `linux/arm64` builds.
+ - Builds from the release [main branch](https://github.com/ptr727/PlexCleaner/tree/main).
+- `develop`:
+ - Builds from the pre-release [develop branch](https://github.com/ptr727/PlexCleaner/tree/develop).
-### `ptr727/plexcleaner:alpine`
+## Image Information
```text
-include({{alpine.ver}})
+include({{latest.ver}})
```
diff --git a/Docker/Test.sh b/Docker/Test.sh
index 4a1c885a..e7ee2bd6 100644
--- a/Docker/Test.sh
+++ b/Docker/Test.sh
@@ -66,7 +66,9 @@ $PlexCleanerApp getversioninfo --settingsfile $SettingsFile
# Run process command first
$PlexCleanerApp process --settingsfile $SettingsFile --logfile $TestPath/PlexCleaner.log --logwarning --mediafiles $MediaPath --testsnippets --resultsfile $TestPath/Results.json
-# Info commands
+# Info/Testing commands
+$PlexCleanerApp verify --settingsfile $SettingsFile --mediafiles $MediaPath --quickscan
+$PlexCleanerApp testmediainfo --settingsfile $SettingsFile --mediafiles $MediaPath
$PlexCleanerApp updatesidecar --settingsfile $SettingsFile --mediafiles $MediaPath
$PlexCleanerApp getsidecarinfo --settingsfile $SettingsFile --mediafiles $MediaPath
$PlexCleanerApp gettagmap --settingsfile $SettingsFile --mediafiles $MediaPath
@@ -77,10 +79,11 @@ $PlexCleanerApp gettoolinfo --settingsfile $SettingsFile --mediafiles $MediaPath
$PlexCleanerApp createsidecar --settingsfile $SettingsFile --mediafiles $MediaPath
# Processing commands
-$PlexCleanerApp remux --settingsfile $SettingsFile --mediafiles $MediaPath --testsnippets
-$PlexCleanerApp reencode --settingsfile $SettingsFile --mediafiles $MediaPath --testsnippets
-$PlexCleanerApp deinterlace --settingsfile $SettingsFile --mediafiles $MediaPath --testsnippets
+$PlexCleanerApp remux --settingsfile $SettingsFile --mediafiles $MediaPath
+$PlexCleanerApp reencode --settingsfile $SettingsFile --mediafiles $MediaPath
+$PlexCleanerApp deinterlace --settingsfile $SettingsFile --mediafiles $MediaPath
$PlexCleanerApp removesubtitles --settingsfile $SettingsFile --mediafiles $MediaPath
+$PlexCleanerApp removeclosedcaptions --settingsfile $SettingsFile --mediafiles $MediaPath --quickscan
# Not readily testable
# $PlexCleanerApp monitor --settingsfile $SettingsFile --mediafiles $MediaPath
diff --git a/Docker/Ubuntu.Devel.Dockerfile b/Docker/Ubuntu.Devel.Dockerfile
deleted file mode 100644
index 84f64162..00000000
--- a/Docker/Ubuntu.Devel.Dockerfile
+++ /dev/null
@@ -1,147 +0,0 @@
-# Description: Ubuntu development release
-# Based on: ubuntu:devel
-# .NET install: Ubuntu repository
-# Platforms: linux/amd64, linux/arm64
-# Tag: ptr727/plexcleaner:ubuntu-devel
-
-# Docker build debugging:
-# --progress=plain
-# --no-cache
-
-# Test image in shell:
-# docker run -it --rm --pull always --name Testing ubuntu:devel /bin/bash
-# docker run -it --rm --pull always --name Testing ptr727/plexcleaner:ubuntu-devel /bin/bash
-# export DEBIAN_FRONTEND=noninteractive
-
-# Build Dockerfile
-# docker buildx create --name "plexcleaner" --use
-# docker buildx build --platform linux/amd64,linux/arm64 --file ./Docker/Ubuntu.Devel.Dockerfile .
-
-# Test linux/amd64 target
-# docker buildx build --load --platform linux/amd64 --tag plexcleaner:ubuntu-devel --file ./Docker/Ubuntu.Devel.Dockerfile .
-# docker run -it --rm --name PlexCleaner-Test plexcleaner:ubuntu-devel /bin/bash
-
-
-# Builder layer
-FROM --platform=$BUILDPLATFORM ubuntu:devel AS builder
-
-# Layer workdir
-WORKDIR /Builder
-
-# Build platform args
-ARG TARGETPLATFORM \
- TARGETARCH \
- BUILDPLATFORM
-
-# PlexCleaner build attribute configuration
-ARG BUILD_CONFIGURATION="Debug" \
- BUILD_VERSION="1.0.0.0" \
- BUILD_FILE_VERSION="1.0.0.0" \
- BUILD_ASSEMBLY_VERSION="1.0.0.0" \
- BUILD_INFORMATION_VERSION="1.0.0.0" \
- BUILD_PACKAGE_VERSION="1.0.0.0"
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install .NET SDK
-# https://packages.ubuntu.com/plucky/dotnet-sdk-9.0
-RUN apt install -y --no-install-recommends dotnet-sdk-9.0
-
-# Copy source and unit tests
-COPY ./Samples/. ./Samples/.
-COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
-COPY ./PlexCleaner/. ./PlexCleaner/.
-
-# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
-RUN ./UnitTest.sh
-
-# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
-RUN ./Build.sh
-
-
-# Final layer
-FROM ubuntu:devel AS final
-
-# Image label
-ARG LABEL_VERSION="1.0.0.0"
-LABEL name="PlexCleaner" \
- version=${LABEL_VERSION} \
- description="Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc." \
- maintainer="Pieter Viljoen "
-
-# Prevent EULA and confirmation prompts in installers
-ENV DEBIAN_FRONTEND=noninteractive
-
-# Upgrade
-RUN apt update \
- && apt upgrade -y
-
-# Install dependencies
-RUN apt install -y --no-install-recommends \
- ca-certificates \
- locales \
- locales-all \
- p7zip-full \
- tzdata \
- wget \
- && locale-gen --no-purge en_US en_US.UTF-8
-
-# Set locale to UTF-8 after running locale-gen
-# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md
-ENV TZ=Etc/UTC \
- LANG=en_US.UTF-8 \
- LANGUAGE=en_US:en \
- LC_ALL=en_US.UTF-8
-
-# Install .NET Runtime
-# https://packages.ubuntu.com/plucky/dotnet-runtime-9.0
-RUN apt install -y --no-install-recommends dotnet-runtime-9.0
-
-# Install media tools
-# https://packages.ubuntu.com/plucky/ffmpeg
-# https://packages.ubuntu.com/plucky/handbrake-cli
-# https://packages.ubuntu.com/plucky/mediainfo
-# https://packages.ubuntu.com/plucky/mkvtoolnix
-RUN apt install -y --no-install-recommends \
- ffmpeg \
- handbrake-cli \
- mediainfo \
- mkvtoolnix
-
-# Cleanup
-RUN apt autoremove -y \
- && apt clean \
- && rm -rf /var/lib/apt/lists/*
-
-# Copy PlexCleaner from builder layer
-COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
-
-# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
-
-# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
- && rm -rf ./InstallDebugTools.sh
-
-# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
-
-# Print version information
-ARG TARGETPLATFORM \
- BUILDPLATFORM
-RUN if [ "$BUILDPLATFORM" = "$TARGETPLATFORM" ]; then \
- /PlexCleaner/Version.sh; \
- fi
diff --git a/Docker/Ubuntu.Rolling.Dockerfile b/Docker/Ubuntu.Rolling.Dockerfile
index 1b882927..ebfb559a 100644
--- a/Docker/Ubuntu.Rolling.Dockerfile
+++ b/Docker/Ubuntu.Rolling.Dockerfile
@@ -49,8 +49,9 @@ RUN apt update \
&& apt upgrade -y
# Install .NET SDK
-# https://packages.ubuntu.com/oracular/dotnet-sdk-9.0
-RUN apt install -y --no-install-recommends dotnet-sdk-9.0
+# https://documentation.ubuntu.com/ubuntu-for-developers/howto/dotnet-setup
+# https://learn.microsoft.com/en-us/dotnet/core/install/linux-ubuntu-install
+RUN apt install -y --install-suggests dotnet-sdk-10.0
# Copy source and unit tests
COPY ./Samples/. ./Samples/.
@@ -58,13 +59,11 @@ COPY ./PlexCleanerTests/. ./PlexCleanerTests/.
COPY ./PlexCleaner/. ./PlexCleaner/.
# Unit Test
-COPY ./Docker/UnitTest.sh ./
-RUN chmod ug=rwx,o=rx ./UnitTest.sh
+COPY --chmod=ug=rwx,o=rx ./Docker/UnitTest.sh ./
RUN ./UnitTest.sh
# Build
-COPY ./Docker/Build.sh ./
-RUN chmod ug=rwx,o=rx ./Build.sh
+COPY --chmod=ug=rwx,o=rx ./Docker/Build.sh ./
RUN ./Build.sh
@@ -103,14 +102,13 @@ ENV TZ=Etc/UTC \
LC_ALL=en_US.UTF-8
# Install .NET Runtime
-# https://packages.ubuntu.com/oracular/dotnet-runtime-9.0
-RUN apt install -y --no-install-recommends dotnet-runtime-9.0
+RUN apt install -y --install-suggests dotnet-runtime-10.0
# Install media tools
-# https://packages.ubuntu.com/oracular/ffmpeg
-# https://packages.ubuntu.com/oracular/handbrake-cli
-# https://packages.ubuntu.com/oracular/mediainfo
-# https://packages.ubuntu.com/oracular/mkvtoolnix
+# https://packages.ubuntu.com/questing/ffmpeg
+# https://packages.ubuntu.com/questing/handbrake-cli
+# https://packages.ubuntu.com/questing/mediainfo
+# https://packages.ubuntu.com/questing/mkvtoolnix
RUN apt install -y --no-install-recommends \
ffmpeg \
handbrake-cli \
@@ -126,18 +124,15 @@ RUN apt autoremove -y \
COPY --from=builder /Builder/Publish/PlexCleaner/. /PlexCleaner
# Copy test script
-COPY /Docker/Test.sh /Test/
-RUN chmod -R ug=rwx,o=rx /Test
+COPY --chmod=ug=rwx,o=rx ./Docker/Test.sh /Test/
# Install debug tools
-COPY ./Docker/InstallDebugTools.sh ./
-RUN chmod ug=rwx,o=rx ./InstallDebugTools.sh \
- && ./InstallDebugTools.sh \
+COPY --chmod=ug=rwx,o=rx ./Docker/InstallDebugTools.sh ./
+RUN ./InstallDebugTools.sh \
&& rm -rf ./InstallDebugTools.sh
# Copy version script
-COPY /Docker/Version.sh /PlexCleaner/
-RUN chmod ug=rwx,o=rx /PlexCleaner/Version.sh
+COPY --chmod=ug=rwx,o=rx ./Docker/Version.sh /PlexCleaner/
# Print version information
ARG TARGETPLATFORM \
diff --git a/Docs/ClosedCaptions.md b/Docs/ClosedCaptions.md
new file mode 100644
index 00000000..3fa9bf17
--- /dev/null
+++ b/Docs/ClosedCaptions.md
@@ -0,0 +1,186 @@
+# EIA-608 and CTA-708 Closed Captions
+
+[EIA-608](https://en.wikipedia.org/wiki/EIA-608) and [CTA-708](https://en.wikipedia.org/wiki/CTA-708) subtitles, commonly referred to as Closed Captions (CC), are typically used for broadcast television.
+
+> **ℹ️ TL;DR**: Closed captions (CC) are subtitles embedded in the video stream (not separate tracks). They can cause issues with some players that always display them or cannot disable them. PlexCleaner detects and removes them using the `RemoveClosedCaptions` option with the FFprobe `subcc` filter (most reliable method). Detection requires scanning the entire file (~10-30% of playback time, faster with `--quickscan`). Removal uses FFmpeg's `filter_units` filter without re-encoding.
+
+## Understanding Closed Captions
+
+Media containers typically contain separate discrete subtitle tracks, but closed captions can be encoded into the primary video stream.
+
+Removal of closed captions may be desirable for various reasons, including undesirable content, or players that always burn in closed captions during playback.
+
+Unlike normal subtitle tracks, detection and removal of closed captions are non-trivial.
+
+## Technical Details
+
+> **ℹ️ Note**: I have no expertise in video engineering; the following information was gathered by research and experimentation.
+
+The currently implemented method of closed caption detection uses [FFprobe and the `subcc` filter](#ffprobe-subcc) to detect closed caption frames in the video stream.
+
+> **ℹ️ Note**: The `subcc` filter does not support partial file analysis. When the `quickscan` option is enabled, a small file snippet is first created and used for analysis, reducing processing times.
+
+The [FFmpeg `filter_units` filter](#ffmpeg-filter_units) is used for closed caption removal.
+
+## Closed Caption Detection
+
+### FFprobe
+
+FFprobe used to identify closed caption presence in normal console output, but [does not support](https://github.com/ptr727/PlexCleaner/issues/94) closed caption reporting when using `-print_format json`, and recently [removed reporting](https://github.com/ptr727/PlexCleaner/issues/497) of closed caption presence completely, prompting research into alternatives.
+
+E.g. `ffprobe filename`
+
+```text
+Stream #0:0(eng): Video: h264 (High), yuv420p(tv, bt709, progressive), 1920x1080, Closed Captions, SAR 1:1 DAR 16:9, 29.97 fps, 29.97 tbr, 1k tbn (default)
+```
+
+### MediaInfo
+
+MediaInfo supports closed caption detection, but only for [some container types](https://github.com/MediaArea/MediaInfoLib/issues/2264) (e.g. TS and DV), and [only scans](https://github.com/MediaArea/MediaInfoLib/issues/1881) the first 30s of the video looking for video frames containing closed captions.
+
+E.g. `mediainfo --Output=JSON filename`
+
+MediaInfo does [not support](https://github.com/MediaArea/MediaInfoLib/issues/1881#issuecomment-2816754336) general input piping (e.g. MKV -> FFmpeg -> TS -> MediaInfo), and requires a temporary TS file to be created on disk and used as standard input.
+
+In my testing I found that remuxing 30s of video from MKV to TS did produce reliable results.
+
+E.g.
+
+```json
+{
+ "@type": "Text",
+ "ID": "256-1",
+ "Format": "EIA-708",
+ "MuxingMode": "A/53 / DTVCC Transport",
+},
+```
+
+### CCExtractor
+
+[CCExtractor](https://ccextractor.org/) supports closed caption detection using `-out=report`.
+
+E.g. `ccextractor -12 -out=report filename`
+
+In my testing I found using MKV containers directly as input produced unreliable results, either no output generated or false negatives.
+
+CCExtractor does support input piping, but I found it to be unreliable with broken pipes, and requires a temporary TS file to be created on disk and used as standard input.
+
+Even in TS format on disk, it is very sensitive to stream anomalies, e.g. `Error: Broken AVC stream - forbidden_zero_bit not zero ...`, making it unreliable.
+
+E.g.
+
+```text
+EIA-608: Yes
+CEA-708: Yes
+```
+
+## FFprobe `readeia608`
+
+FFmpeg [`readeia608` filter](https://ffmpeg.org/ffmpeg-filters.html#readeia608) can be used in FFprobe to report EIA-608 frame information.
+
+E.g.
+
+```shell
+ffprobe -loglevel error -f lavfi -i "movie=filename,readeia608" -show_entries frame=best_effort_timestamp_time,duration_time:frame_tags=lavfi.readeia608.0.line,lavfi.readeia608.0.cc,lavfi.readeia608.1.line,lavfi.readeia608.1.cc -print_format json
+```
+
+The `movie=filename[out0+subcc]` convention requires [special escaping](https://superuser.com/questions/1893137/how-to-quote-a-file-name-containing-single-quotes-in-ffmpeg-ffprobe-movie-filena) of the filename to not interfere with commandline or filter graph parsing.
+
+In my testing I found only one [IMX sample](https://archive.org/details/vitc_eia608_sample) that produced the expected results, making it unreliable.
+
+E.g.
+
+```json
+{
+ "best_effort_timestamp_time": "0.000000",
+ "duration_time": "0.033367",
+ "tags": {
+ "lavfi.readeia608.1.cc": "0x8504",
+ "lavfi.readeia608.0.cc": "0x8080",
+ "lavfi.readeia608.0.line": "28",
+ "lavfi.readeia608.1.line": "29"
+ },
+}
+```
+
+### FFprobe `subcc`
+
+FFmpeg [`subcc` filter](https://www.ffmpeg.org/ffmpeg-devices.html#Options-10) can be used in FFprobe to create subtitle streams from the closed captions embedded in video streams.
+
+E.g.
+
+```shell
+ffprobe -loglevel error -select_streams s:0 -f lavfi -i "movie=filename[out0+subcc]" -show_packets -print_format json
+```
+
+E.g.
+
+```shell
+ffmpeg -abort_on empty_output -y -f lavfi -i "movie=filename[out0+subcc]" -map 0:s -c:s srt outfilename
+```
+
+The `ffmpeg -t` and `ffprobe -read_intervals` options limiting scan time does [not work](https://superuser.com/questions/1893673/how-to-time-limit-the-input-stream-duration-when-using-movie-filenameout0subcc) on the input stream when using the `subcc` filter, and scanning the entire file can take a very long time.
+
+In my testing I found the results to be reliable across a wide variety of files.
+
+E.g.
+
+```json
+{
+ "codec_type": "subtitle",
+ "stream_index": 1,
+ "pts_time": "0.000000",
+ "dts_time": "0.000000",
+ "size": "60",
+ "pos": "5690",
+ "flags": "K__"
+},
+```
+
+```text
+9
+00:00:35,568 --> 00:00:38,004
+{\an7}No going back now.
+```
+
+### FFprobe `analyze_frames`
+
+FFprobe [recently added](https://github.com/FFmpeg/FFmpeg/commit/90af8e07b02e690a9fe60aab02a8bccd2cbf3f01) the `analyze_frames` [option](https://ffmpeg.org/ffprobe.html#toc-Main-options) that reports on the presence of closed captions in video streams.
+
+As of writing this functionality has not yet been released, but is only in nightly builds.
+
+E.g.
+
+```shell
+ffprobe -loglevel error -show_streams -analyze_frames -read_intervals %180 filename -print_format json
+```
+
+```json
+{
+ "index": 0,
+ "codec_name": "h264",
+ "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
+ "coded_width": 1920,
+ "coded_height": 1088,
+ "closed_captions": 1,
+ "film_grain": 0,
+}
+```
+
+The FFprobe `analyze_frames` method of detection will be implemented when broadly supported.
+
+## Closed Caption Removal
+
+### FFmpeg `filter_units`
+
+FFmpeg [`filter_units` filter](https://ffmpeg.org/ffmpeg-bitstream-filters.html#filter_005funits) can be used to [remove closed captions](https://stackoverflow.com/questions/48177694/removing-eia-608-closed-captions-from-h-264-without-reencode) from video streams.
+
+E.g.
+
+```shell
+ffmpeg -loglevel error -i [in-filename] -c copy -map 0 -bsf:v filter_units=remove_types=6 [out-filename]
+```
+
+Closed captions SEI unit for H264 is `6`, `39` for H265, and `178` for MPEG2.
+
+> **ℹ️ Note**: [Wiki](https://trac.ffmpeg.org/wiki/HowToExtractAndRemoveClosedCaptions) and [issue](https://trac.ffmpeg.org/ticket/5283); as of writing HDR10+ metadata may be lost when removing closed captions from H265 content.
diff --git a/Docs/CustomOptions.md b/Docs/CustomOptions.md
new file mode 100644
index 00000000..274d1900
--- /dev/null
+++ b/Docs/CustomOptions.md
@@ -0,0 +1,106 @@
+# Custom FFmpeg and HandBrake CLI Parameters
+
+Custom encoding settings for FFmpeg and Handbrake.
+
+## Understanding Custom Settings
+
+The `ConvertOptions:FfMpegOptions` and `ConvertOptions:HandBrakeOptions` settings allow custom CLI parameters for media processing. This is useful for:
+
+- Hardware-accelerated encoding (GPU encoding via NVENC, QuickSync, etc.).
+- Custom quality/speed tradeoffs (CRF values, presets).
+- Alternative codecs (AV1, VP9, etc.).
+
+> **ℹ️ Note**: Hardware encoding options are operating system, hardware, and tool version specific.\
+Refer to the Jellyfin hardware acceleration [docs](https://jellyfin.org/docs/general/administration/hardware-acceleration/) for hints on usage.
+The example configurations are from documentation and minimal testing with Intel QuickSync on Windows only, please discuss and post working configurations in [GitHub Discussions](https://github.com/ptr727/PlexCleaner/discussions).
+
+## FFmpeg Options
+
+See the [FFmpeg documentation](https://ffmpeg.org/ffmpeg.html) for complete commandline option details.
+
+The typical FFmpeg commandline is:
+
+```text
+ffmpeg [global_options] {[input_file_options] -i input_url} ... {[output_file_options] output_url}
+```
+
+E.g.:
+
+```shell
+ffmpeg -analyzeduration 2147483647 -probesize 2147483647 -i /media/foo.mkv -max_muxing_queue_size 1024 -abort_on empty_output -hide_banner -nostats -map 0 -c:v libx265 -crf 26 -preset medium -c:a ac3 -c:s copy -f matroska /media/bar.mkv
+```
+
+Settings allows for custom configuration of:
+
+- `FfMpegOptions:Global`: Custom hardware global options, e.g. `-hwaccel cuda -hwaccel_output_format cuda`
+- `FfMpegOptions:Video`: Video encoder options following the `-c:v` parameter, e.g. `libx264 -crf 22 -preset medium`
+- `FfMpegOptions:Audio`: Audio encoder options following the `-c:a` parameter, e.g. `ac3`
+
+Get encoder options:
+
+- List hardware acceleration methods: `ffmpeg -hwaccels`
+- List supported encoders: `ffmpeg -encoders`
+- List options supported by an encoder: `ffmpeg -h encoder=libsvtav1`
+
+Example video encoder options:
+
+- [H.264](https://trac.ffmpeg.org/wiki/Encode/H.264): `libx264 -crf 22 -preset medium`
+- [H.265](https://trac.ffmpeg.org/wiki/Encode/H.265): `libx265 -crf 26 -preset medium`
+- [AV1](https://trac.ffmpeg.org/wiki/Encode/AV1): `libsvtav1 -crf 30 -preset 5`
+
+Example hardware assisted video encoding options:
+
+- NVidia NVENC:
+ - See [FFmpeg NVENC](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) documentation.
+ - View NVENC encoder options: `ffmpeg -h encoder=h264_nvenc`
+ - `FfMpegOptions:Global`: `-hwaccel cuda -hwaccel_output_format cuda`
+ - `FfMpegOptions:Video`: `h264_nvenc -preset medium`
+- Intel QuickSync:
+ - See [FFmpeg QuickSync](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) documentation.
+ - View QuickSync encoder options: `ffmpeg -h encoder=h264_qsv`
+ - `FfMpegOptions:Global`: `-hwaccel qsv -hwaccel_output_format qsv`
+ - `FfMpegOptions:Video`: `h264_qsv -preset medium`
+
+## HandBrake Options
+
+See the [HandBrake documentation](https://handbrake.fr/docs/en/latest/cli/command-line-reference.html) for complete commandline option details.
+
+The typical HandBrake commandline is:
+
+```text
+HandBrakeCLI [options] -i -o
+```
+
+E.g.
+
+```shell
+HandBrakeCLI --input /media/foo.mkv --output /media/bar.mkv --format av_mkv --encoder x265 --quality 26 --encoder-preset medium --comb-detect --decomb --all-audio --aencoder copy --audio-fallback ac3
+```
+
+Settings allows for custom configuration of:
+
+- `HandBrakeOptions:Video`: Video encoder options following the `--encode` parameter, e.g. `x264 --quality 22 --encoder-preset medium`
+- `HandBrakeOptions:Audio`: Audio encoder options following the `--aencode` parameter, e.g. `copy --audio-fallback ac3`
+
+Get encoder options:
+
+- List all supported encoders: `HandBrakeCLI --help`
+- List presets supported by an encoder: `HandBrakeCLI --encoder-preset-list svt_av1`
+
+Example video encoder options:
+
+- H.264: `x264 --quality 22 --encoder-preset medium`
+- H.265: `x265 --quality 26 --encoder-preset medium`
+- AV1: `svt_av1 --quality 30 --encoder-preset 5`
+
+Example hardware assisted video encoding options:
+
+- NVidia NVENC:
+ - See [HandBrake NVENC](https://handbrake.fr/docs/en/latest/technical/video-nvenc.html) documentation.
+ - `HandBrakeOptions:Video`: `nvenc_h264 --encoder-preset medium`
+- Intel QuickSync:
+ - See [HandBrake QuickSync](https://handbrake.fr/docs/en/latest/technical/video-qsv.html) documentation.
+ - `HandBrakeOptions:Video`: `qsv_h264 --encoder-preset balanced`
+
+> **ℹ️ Note**: HandBrake is primarily used for video deinterlacing, and only as backup encoder when FFmpeg fails.\
+The default `HandBrakeOptions:Audio` configuration is set to `copy --audio-fallback ac3` that will copy all supported audio tracks as is, and only encode to `ac3` if the audio codec is not natively supported.
diff --git a/Docs/LanguageMatching.md b/Docs/LanguageMatching.md
new file mode 100644
index 00000000..c13e4607
--- /dev/null
+++ b/Docs/LanguageMatching.md
@@ -0,0 +1,27 @@
+# IETF Language Matching
+
+Language tag matching supports [IETF / RFC 5646 / BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) tag formats as implemented by [MkvMerge](https://codeberg.org/mbunkus/mkvtoolnix/wiki/Languages-in-Matroska-and-MKVToolNix).
+
+## Understanding Language Matching
+
+Tags are in the form of `language-extlang-script-region-variant-extension-privateuse`, and matching happens left to right (most specific to least specific).
+
+Examples:
+
+- `pt` matches: `pt` Portuguese, `pt-BR` Brazilian Portuguese, `pt-PT` European Portuguese.
+- `pt-BR` matches: only `pt-BR` Brazilian Portuguese.
+- `zh` matches: `zh` Chinese, `zh-Hans` simplified Chinese, `zh-Hant` traditional Chinese, and other variants.
+- `zh-Hans` matches: only `zh-Hans` simplified Chinese.
+
+## Technical Details
+
+During processing the absence of IETF language tags will be treated as a track warning, and an RFC 5646 IETF language will be temporarily assigned based on the ISO639-2B tag.\
+If `ProcessOptions.SetIetfLanguageTags` is enabled MkvMerge will be used to remux the file using the `--normalize-language-ietf extlang` option, see the [MkvMerge documentation](https://mkvtoolnix.download/doc/mkvmerge.html) for more details.
+
+Normalized tags will be expanded for matching.\
+E.g. `cmn-Hant` will be expanded to `zh-cmn-Hant` allowing matching with `zh`.
+
+## References
+
+- See the [W3C Language tags in HTML and XML](https://www.w3.org/International/articles/language-tags/) and [BCP47 language subtag lookup](https://r12a.github.io/app-subtags/) for technical details.
+- Language tag matching is implemented using the [LanguageTags](https://github.com/ptr727/LanguageTags) library.
diff --git a/HISTORY.md b/HISTORY.md
index c191bf50..560ffb91 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -4,18 +4,49 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
## Release History
+- Version 3.15:
+ - This is primarily a code refactoring release.
+ - Updated from .NET 9 to .NET 10.
+ - Added [Nullable types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types) support.
+ - Added [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot) support.
+ - Replaced `JsonSchemaBuilder.FromType()` with `GetJsonSchemaAsNode()` as `FromType()` is [not AOT compatible](https://github.com/json-everything/json-everything/issues/975).
+ - Replaced `JsonSerializer.Deserialize()` with `JsonSerializer.Deserialize(JsonSerializerContext)` for generating [AOT compatible](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonserializercontext) JSON serialization code.
+ - Replaced `MethodBase.GetCurrentMethod()?.Name` with `[System.Runtime.CompilerServices.CallerMemberName]` to generate the caller function name during compilation.
+ - AOT cross compilation is [not supported](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/cross-compile) by the CI/CD pipeline and single file native AOT binaries can be [manually built](./README.md#aot) if needed.
+ - Changed MediaInfo output from `--Output=XML` using XML to `--Output=JSON` using JSON.
+ - Attempts to use `Microsoft.XmlSerializer.Generator` and generate AOT compatible XML parsing was [unsuccessful](https://stackoverflow.com/questions/79858800/statically-generated-xml-parsing-code-using-microsoft-xmlserializer-generator), while JSON `JsonSerializerContext` is AOT compatible.
+ - Parsing the existing XML schema is done with custom AOT compatible XML parser created for the MediaInfo XML content.
+ - SidecarFile schema changed from v4 to v5 to account for XML to JSON content change.
+ - Schema will automatically be upgraded and convert XML to JSON equivalent on reading.
+ - Using [`ArrayPool.Shared.Rent()`](https://learn.microsoft.com/en-us/dotnet/api/system.buffers.arraypool-1) vs. `new byte[]` to improve memory pressure during sidecar hash calculations.
+ - Removed `MonitorOptions` from the config file schema, default values do not need to be changed.
+ - ⚠️ Standardized on only using the Ubuntu [rolling](https://releases.ubuntu.com/) docker base image.
+ - No longer publishing Debian or Alpine based docker images, or images supporting `linux/arm/v7`.
+ - The media tool versions published with the rolling release are typically current, and matches the versions available on Windows, offering a consistent experience, and requires less testing due to changes in behavior between versions.
+- Version 3.14:
+ - Switch to using [CliWrap][cliwrap-link] for commandline tool process execution.
+ - Remove dependency on [deprecated](https://github.com/dotnet/command-line-api/issues/2576) `System.CommandLine.NamingConventionBinder` by directly using commandline options binding.
+ - Converted media tool commandline creation to using fluent builder pattern.
+ - Converted FFprobe JSON packet parsing to using streaming per-packet processing using [Utf8JsonAsyncStreamReader][utf8jsonasync-link] vs. read everything into memory and then process.
+ - Switched editorconfig `charset` from `utf-8-bom` to `utf-8` as some tools and PR merge in GitHub always write files without the BOM.
+ - Improved closed caption detection in MediaInfo, e.g. discrete detection of separate `SCTE 128` tracks vs. `A/53` embedded video tracks.
+ - Improved media tool parsing resiliency when parsing non-Matroska containers, i.e. added `testmediainfo` command to attempt parsing media files.
+ - Add [Husky.Net](https://alirezanet.github.io/Husky.Net) for pre-commit hook code style validation.
+ - General refactoring.
+- Version 3.13:
+ - Escape additional filename characters for use with `ffprobe movie=filename[out0+subcc]` command. Fixes [#524](https://github.com/ptr727/PlexCleaner/issues/524).
- Version 3:12:
- Update to .NET 9.0.
- - Dropping Ubuntu docker `arm/v7` support as .NET for ARM32 is no longer published in the Ubuntu repository.
+ - ⚠️ Dropping Ubuntu docker `arm/v7` support as .NET for ARM32 is no longer published in the Ubuntu repository.
- Switching Debian docker builds to install .NET using install script as the Microsoft repository now only supports x64 builds. (Ubuntu and Alpine still installing .NET using the distribution repository.)
- Updated code style [`.editorconfig`](./.editorconfig) to closely follow the Visual Studio and .NET Runtime defaults.
- Set [CSharpier](https://csharpier.com/) as default C# code formatter.
- - Removed docker [`UbuntuDevel.Dockerfile`](./Docker/Ubuntu.Devel.Dockerfile), [`AlpineEdge.Dockerfile`](./Docker/Alpine.Edge.Dockerfile), and [`DebianTesting.Dockerfile`](./Docker/Debian.Testing.Dockerfile) builds from CI as theses OS pre-release / Beta builds were prone to intermittent build failures. If "bleeding edge" media tools are required local builds can be done using the Dockerfile.
+ - ⚠️ Removed docker [`UbuntuDevel.Dockerfile`](./Docker/Ubuntu.Devel.Dockerfile), [`AlpineEdge.Dockerfile`](./Docker/Alpine.Edge.Dockerfile), and [`DebianTesting.Dockerfile`](./Docker/Debian.Testing.Dockerfile) builds from CI as theses OS pre-release / Beta builds were prone to intermittent build failures. If "bleeding edge" media tools are required local builds can be done using the Dockerfile.
- Updated 7-Zip version number parsing to account for newly [observed](./PlexCleanerTests/VersionParsingTests.cs) variants.
- EIA-608 and CTA-708 closed caption detection was reworked due to FFmpeg [removing](https://code.ffmpeg.org/FFmpeg/FFmpeg/commit/19c95ecbff84eebca254d200c941ce07868ee707) easy detection using FFprobe.
- See the [EIA-608 and CTA-708 Closed Captions](./README.md#eia-608-and-cta-708-closed-captions) section for details.
- Refactored the logic used to determine if a video stream should be considered to contain closed captions.
- - Note that detection may have been broken since the release of FFmpeg v7, it is possible that media files may be in the `Verified` state with closed captions being undetected, run the `removeclosedcaptions` command to re-detect and remove closed captions.
+ - Detection may have been broken since the release of FFmpeg v7, it is possible that media files may be in the `Verified` state with closed captions being undetected, run the `removeclosedcaptions` command to re-detect and remove closed captions.
- Interlace and Telecine detection is complicated and this implementation using track flags and `idet` is naive and may not be reliable, changed `DeInterlace` to default to `false`.
- Re-added `parallel` and `threadcount` option to `monitor` command, fixes [#498](https://github.com/ptr727/PlexCleaner/issues/498).
- Added conditional checks for `ReMux` to warn when disabled and media must be modified for processing logic to work as intended, e.g. removing extra video streams, removing cover art, etc.
@@ -40,7 +71,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- No longer pre-installing VS Debug Tools in docker builds, replaced with [`DebugTools.sh`](./Docker//DebugTools.sh) script that can be used to install [VS Debug Tools](https://learn.microsoft.com/en-us/visualstudio/debugger/remote-debugging-dotnet-core-linux-with-ssh) and [.NET Diagnostic Tools](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/tools-overview) if required.
- Version 3.8:
- Added Alpine Stable and Edge, Debian Stable and Testing, and Ubuntu Rolling and Devel docker builds.
- - Removed ArchLinux docker build, only supported x64 and media tool versions were often lagging.
+ - ⚠️ Removed ArchLinux docker build, only supported x64 and media tool versions were often lagging.
- No longer using MCR base images with .NET pre-installed, support for new linux distribution versions were often lagging.
- Alpine Stable builds are still [disabled](https://github.com/ptr727/PlexCleaner/issues/344), waiting for Alpine 3.20 to be released, ETA 1 June 2024.
- Rob Savoury [announced][savoury-link] that due to a lack of funding Ubuntu Noble 24.04 LTS will not get PPA support.
@@ -60,7 +91,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- Changed JSON serialization from `Newtonsoft.Json` [to](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/migrate-from-newtonsoft) .NET native `Text.Json`.
- Changed JSON schema generation from `Newtonsoft.Json.Schema` [to][jsonschema-link] `JsonSchema.Net.Generation`.
- Fixed issue with old settings schemas not upgrading as expected, and updated associated unit tests to help catch this next time.
- - Disabling Alpine Edge builds, Handbrake is [failing](https://gitlab.alpinelinux.org/alpine/aports/-/issues/15979) to install, again.
+ - ⚠️ Disabling Alpine Edge builds, Handbrake is [failing](https://gitlab.alpinelinux.org/alpine/aports/-/issues/15979) to install, again.
- Will re-enable Alpine builds if Alpine 3.20 and Handbrake is stable.
- Version 3.6:
- Disabling Alpine 3.19 release builds and switching to Alpine Edge.
@@ -78,7 +109,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- Updating the tool itself is still a manual process.
- Alternatively subscribe to GitHub [Release Notifications][github-release-notification].
- Added `verify` command option to verify media streams in files.
- - Note that only media stream validation is performed, track-, bitrate-, and HDR verification is only performed as part of the `process` command.
+ - Only media stream validation is performed, track-, bitrate-, and HDR verification is only performed as part of the `process` command.
- The `verify` command is useful when testing or selecting from multiple available media sources.
- Version 3.3:
- Download Windows FfMpeg builds from [GyanD FfMpeg GitHub mirror](https://github.com/GyanD/codexffmpeg), may help with [#214](https://github.com/ptr727/PlexCleaner/issues/214).
@@ -104,7 +135,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- On v3 schema upgrade old `ConvertOptions` settings will be upgrade to equivalent settings.
- Added support for [IETF / RFC 5646 / BCP 47](https://en.wikipedia.org/wiki/IETF_language_tag) language tag formats.
- See the [Language Matching](./README.md#language-matching) section usage for details.
- - IETF language tags allows for greater flexibility in Matroska player [language matching](https://gitlab.com/mbunkus/mkvtoolnix/-/wikis/Languages-in-Matroska-and-MKVToolNix).
+ - IETF language tags allows for greater flexibility in Matroska player [language matching](https://codeberg.org/mbunkus/mkvtoolnix/wiki/Languages-in-Matroska-and-MKVToolNix).
- E.g. `pt-BR` for Brazilian Portuguese vs. `por` for Portuguese.
- E.g. `zh-Hans` for simplified Chinese vs. `chi` for Chinese.
- Update `ProcessOptions:DefaultLanguage` and `ProcessOptions:KeepLanguages` from ISO 639-2B to RFC 5646 format, e.g. `eng` to `en`.
@@ -141,8 +172,8 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- Changed the process exit code to return `1` vs. `-1` in case of error, more conformant with standard exit codes, `0` remains success.
- Settings JSON schema updated from v2 to v3 to account for new and modified settings.
- Older settings schemas will automatically be upgraded with compatible settings to v3 on first run.
- - *Breaking Change* Removed the `reprocess` commandline option, logic was very complex with limited value, use `reverify` instead.
- - *Breaking Change* Refactored commandline arguments to only add relevant options to commands that use them vs. adding global options to all commands.
+ - ⚠️ Removed the `reprocess` commandline option, logic was very complex with limited value, use `reverify` instead.
+ - ⚠️ Refactored commandline arguments to only add relevant options to commands that use them vs. adding global options to all commands.
- Maintaining commandline backwards compatibility was [complicated](https://github.com/dotnet/command-line-api/issues/2023), and the change is unfortunately a breaking change.
- The following global options have been removed and added to their respective commands:
- `--settingsfile` used by several commands.
@@ -281,11 +312,11 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- Support for H.265 encoding added.
- All file metadata, titles, tags, and track names are now deleted during media file cleanup.
- Windows systems will be kept awake during processing.
- - Schema version numbers were added to JSON config files, breaking backwards compatibility.
+ - ⚠️ Schema version numbers were added to JSON config files, breaking backwards compatibility.
- Sidecar JSON will be invalid and recreated, including re-verifying that can be very time consuming.
- Tools JSON will be invalid and `checkfortools` should be used to update tools.
- Tool version numbers are now using the short version number, allowing for Sidecar compatibility between Windows and Linux.
- - Processing of the same media can be mixed between Windows, Linux, and Docker, note that the paths in the `FileIgnoreList` setting are platform specific.
+ - Processing of the same media can be mixed between Windows, Linux, and Docker, but the paths in the `FileIgnoreList` setting are platform specific.
- New options were added to the JSON config file.
- `ConvertOptions:EnableH265Encoder`: Enable H.265 encoding vs. H.264.
- `ToolsOptions:UseSystem`: Use tools from the system path vs. from the Tools folder, this is the default on Linux.
@@ -294,7 +325,9 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- File logging and console output is now done using structured Serilog logging.
- Basic console and file logging options are used, configuration from JSON is not currently supported.
+[cliwrap-link]: https://github.com/Tyrrrz/CliWrap
[docker-link]: https://hub.docker.com/r/ptr727/plexcleaner
-[savoury-link]: https://launchpad.net/~savoury1
[github-release-notification]: https://docs.github.com/en/account-and-profile/managing-subscriptions-and-notifications-on-github/managing-subscriptions-for-activity-on-github/viewing-your-subscriptions
[jsonschema-link]: https://json-everything.net/json-schema/
+[savoury-link]: https://launchpad.net/~savoury1
+[utf8jsonasync-link]: https://github.com/gragra33/Utf8JsonAsyncStreamReader
diff --git a/PlexCleaner.code-workspace b/PlexCleaner.code-workspace
index 50ef1733..8eb52a44 100644
--- a/PlexCleaner.code-workspace
+++ b/PlexCleaner.code-workspace
@@ -159,6 +159,7 @@
"Pieter",
"Plex",
"plexcleaner",
+ "PLINQ",
"preprocess",
"printmediainfo",
"printsidecar",
@@ -236,7 +237,7 @@
"yaml.schemas": {
"https://json.schemastore.org/github-issue-config.json": "file:/.github/ISSUE_TEMPLATE/config.yml"
},
- "dotnet.defaultSolution": "PlexCleaner.sln",
+ "dotnet.defaultSolution": "PlexCleaner.slnx",
"files.trimTrailingWhitespace": true,
"[markdown]": {
"files.trimTrailingWhitespace": false
@@ -256,7 +257,8 @@
"dotnet.formatting.organizeImportsOnFormat": true,
"csharp.debug.symbolOptions.searchNuGetOrgSymbolServer": true,
"csharp.debug.symbolOptions.searchMicrosoftSymbolServer": true,
- "files.encoding": "utf8"
+ "files.encoding": "utf8",
+ "markdown.extension.toc.levels": "2..3"
},
"extensions": {
"recommendations": [
diff --git a/PlexCleaner.defaults.json b/PlexCleaner.defaults.json
index b130c16b..8971fc2a 100644
--- a/PlexCleaner.defaults.json
+++ b/PlexCleaner.defaults.json
@@ -18,6 +18,7 @@
"*.jpg",
"*.nfo",
"*.partial~",
+ "*.png",
"*.sample.*",
"*.sample",
"*.smi",
@@ -40,6 +41,8 @@
".m2ts",
".m4v",
".mp4",
+ ".mpg",
+ ".mov",
".ts",
".vob",
".wmv"
@@ -175,13 +178,5 @@
"RegisterInvalidFiles": false,
// Warn when bitrate in bits per second is exceeded
"MaximumBitrate": 100000000
- },
- "MonitorOptions": {
- // Time to wait in seconds after detecting a file change
- "MonitorWaitTime": 60,
- // Time to wait in seconds between file retry operations
- "FileRetryWaitTime": 5,
- // Maximum number of file retry operations
- "FileRetryCount": 2
}
}
diff --git a/PlexCleaner.schema.json b/PlexCleaner.schema.json
index a101cfc2..6d0af249 100644
--- a/PlexCleaner.schema.json
+++ b/PlexCleaner.schema.json
@@ -1,95 +1,99 @@
{
- "type": "object",
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "ConvertOptions": {
- "type": "object",
- "properties": {
- "FfMpegOptions": {
- "type": "object",
- "properties": {
- "Audio": {
- "type": "string"
- },
- "Global": {
- "type": "string"
- },
- "Video": {
- "type": "string"
- }
- }
- },
- "HandBrakeOptions": {
- "type": "object",
- "properties": {
- "Audio": {
- "type": "string"
- },
- "Video": {
- "type": "string"
- }
- }
- }
- }
+ "$schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "SchemaVersion": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
},
- "MonitorOptions": {
+ "ToolsOptions": {
"type": "object",
"properties": {
- "FileRetryCount": {
- "type": "integer"
+ "UseSystem": {
+ "type": "boolean"
+ },
+ "RootPath": {
+ "type": "string"
},
- "FileRetryWaitTime": {
- "type": "integer"
+ "RootRelative": {
+ "type": "boolean"
},
- "MonitorWaitTime": {
- "type": "integer"
+ "AutoUpdate": {
+ "type": "boolean"
}
- }
+ },
+ "required": [
+ "UseSystem",
+ "RootPath",
+ "RootRelative",
+ "AutoUpdate"
+ ]
},
"ProcessOptions": {
"type": "object",
"properties": {
- "DefaultLanguage": {
- "type": "string"
+ "FileIgnoreMasks": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
- "DeInterlace": {
+ "KeepOriginalLanguage": {
"type": "boolean"
},
- "DeleteEmptyFolders": {
+ "RemoveClosedCaptions": {
"type": "boolean"
},
- "DeleteUnwantedExtensions": {
+ "SetIetfLanguageTags": {
"type": "boolean"
},
- "FileIgnoreList": {
- "$ref": "#/$defs/hashSetOfString"
- },
- "FileIgnoreMasks": {
- "$ref": "#/$defs/hashSetOfString"
- },
- "KeepLanguages": {
- "$ref": "#/$defs/hashSetOfString"
- },
- "KeepOriginalLanguage": {
+ "SetTrackFlags": {
"type": "boolean"
},
- "PreferredAudioFormats": {
- "$ref": "#/$defs/hashSetOfString"
- },
- "ReEncode": {
- "type": "boolean"
+ "KeepExtensions": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
- "ReEncodeAudioFormats": {
- "$ref": "#/$defs/hashSetOfString"
+ "ReMuxExtensions": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
"ReEncodeVideo": {
"type": "array",
"items": {
- "type": "object",
+ "type": [
+ "object",
+ "null"
+ ],
"properties": {
- "Codec": {
+ "Format": {
"type": "string"
},
- "Format": {
+ "Codec": {
"type": "string"
},
"Profile": {
@@ -98,70 +102,180 @@
}
}
},
- "RemoveClosedCaptions": {
- "type": "boolean"
+ "ReEncodeAudioFormats": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
- "RemoveDuplicateTracks": {
- "type": "boolean"
+ "KeepLanguages": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
},
- "RemoveTags": {
+ "PreferredAudioFormats": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "ReEncodeVideoFormats": {
+ "type": "string"
+ },
+ "ReEncodeVideoCodecs": {
+ "type": "string"
+ },
+ "ReEncodeVideoProfiles": {
+ "type": "string"
+ },
+ "DeleteEmptyFolders": {
"type": "boolean"
},
- "RemoveUnwantedLanguageTracks": {
+ "DeleteUnwantedExtensions": {
"type": "boolean"
},
"ReMux": {
"type": "boolean"
},
- "ReMuxExtensions": {
- "$ref": "#/$defs/hashSetOfString"
+ "DeInterlace": {
+ "type": "boolean"
},
- "RestoreFileTimestamp": {
+ "ReEncode": {
"type": "boolean"
},
- "SetIetfLanguageTags": {
+ "SetUnknownLanguage": {
"type": "boolean"
},
- "SetTrackFlags": {
+ "DefaultLanguage": {
+ "type": "string"
+ },
+ "RemoveUnwantedLanguageTracks": {
"type": "boolean"
},
- "SetUnknownLanguage": {
+ "RemoveDuplicateTracks": {
"type": "boolean"
},
- "SidecarUpdateOnToolChange": {
+ "RemoveTags": {
"type": "boolean"
},
"UseSidecarFiles": {
"type": "boolean"
},
+ "SidecarUpdateOnToolChange": {
+ "type": "boolean"
+ },
"Verify": {
"type": "boolean"
+ },
+ "RestoreFileTimestamp": {
+ "type": "boolean"
+ },
+ "FileIgnoreList": {
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
- }
- },
- "$schema": {
- "type": "string",
- "readOnly": true
- },
- "SchemaVersion": {
- "type": "integer"
+ },
+ "required": [
+ "FileIgnoreMasks",
+ "KeepOriginalLanguage",
+ "RemoveClosedCaptions",
+ "SetIetfLanguageTags",
+ "SetTrackFlags",
+ "ReMuxExtensions",
+ "ReEncodeVideo",
+ "ReEncodeAudioFormats",
+ "KeepLanguages",
+ "PreferredAudioFormats",
+ "DeleteEmptyFolders",
+ "DeleteUnwantedExtensions",
+ "ReMux",
+ "DeInterlace",
+ "ReEncode",
+ "SetUnknownLanguage",
+ "DefaultLanguage",
+ "RemoveUnwantedLanguageTracks",
+ "RemoveDuplicateTracks",
+ "RemoveTags",
+ "UseSidecarFiles",
+ "SidecarUpdateOnToolChange",
+ "Verify",
+ "RestoreFileTimestamp",
+ "FileIgnoreList"
+ ]
},
- "ToolsOptions": {
+ "ConvertOptions": {
"type": "object",
"properties": {
- "AutoUpdate": {
- "type": "boolean"
+ "FfMpegOptions": {
+ "type": "object",
+ "properties": {
+ "Video": {
+ "type": "string"
+ },
+ "Audio": {
+ "type": "string"
+ },
+ "Global": {
+ "type": "string"
+ },
+ "Output": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "Video",
+ "Audio",
+ "Global"
+ ]
},
- "RootPath": {
- "type": "string"
+ "HandBrakeOptions": {
+ "type": "object",
+ "properties": {
+ "Video": {
+ "type": "string"
+ },
+ "Audio": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "Video",
+ "Audio"
+ ]
},
- "RootRelative": {
+ "EnableH265Encoder": {
"type": "boolean"
},
- "UseSystem": {
- "type": "boolean"
+ "VideoEncodeQuality": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
+ },
+ "AudioEncodeCodec": {
+ "type": "string"
}
- }
+ },
+ "required": [
+ "FfMpegOptions",
+ "HandBrakeOptions"
+ ]
},
"VerifyOptions": {
"type": "object",
@@ -172,23 +286,60 @@
"DeleteInvalidFiles": {
"type": "boolean"
},
- "MaximumBitrate": {
- "type": "integer"
- },
"RegisterInvalidFiles": {
"type": "boolean"
+ },
+ "MinimumDuration": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
+ },
+ "VerifyDuration": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
+ },
+ "IdetDuration": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
+ },
+ "MaximumBitrate": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
+ },
+ "MinimumFileAge": {
+ "type": [
+ "string",
+ "integer"
+ ],
+ "pattern": "^-?(?:0|[1-9]\\d*)$"
}
- }
- }
- },
- "$defs": {
- "hashSetOfString": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ },
+ "required": [
+ "AutoRepair",
+ "DeleteInvalidFiles",
+ "RegisterInvalidFiles",
+ "MaximumBitrate"
+ ]
}
},
+ "required": [
+ "SchemaVersion",
+ "ToolsOptions",
+ "ProcessOptions",
+ "ConvertOptions",
+ "VerifyOptions"
+ ],
"title": "PlexCleaner Configuration Schema",
"$id": "https://raw.githubusercontent.com/ptr727/PlexCleaner/main/PlexCleaner.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema"
diff --git a/PlexCleaner.sln b/PlexCleaner.sln
deleted file mode 100644
index 18520c4a..00000000
--- a/PlexCleaner.sln
+++ /dev/null
@@ -1,86 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.1.31903.286
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlexCleaner", "PlexCleaner\PlexCleaner.csproj", "{1A686E6C-DD3F-4D71-A5F1-DCEE8F872A2F}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D8F3D73D-A71B-4B79-982F-8491CCFB893A}"
- ProjectSection(SolutionItems) = preProject
- .dockerignore = .dockerignore
- .editorconfig = .editorconfig
- .gitattributes = .gitattributes
- .gitignore = .gitignore
- HISTORY.md = HISTORY.md
- LICENSE = LICENSE
- PlexCleaner.defaults.json = PlexCleaner.defaults.json
- PlexCleaner.schema.json = PlexCleaner.schema.json
- README.md = README.md
- version.json = version.json
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{C560F57F-962E-4C71-8F27-939D06813302}"
- ProjectSection(SolutionItems) = preProject
- Docker\Alpine.Edge.Dockerfile = Docker\Alpine.Edge.Dockerfile
- Docker\Alpine.Latest.Dockerfile = Docker\Alpine.Latest.Dockerfile
- Docker\Build.sh = Docker\Build.sh
- Docker\Debian.Stable.Dockerfile = Docker\Debian.Stable.Dockerfile
- Docker\Debian.Testing.Dockerfile = Docker\Debian.Testing.Dockerfile
- Docker\DebugTools.sh = Docker\DebugTools.sh
- Docker\README.m4 = Docker\README.m4
- Docker\Test.sh = Docker\Test.sh
- Docker\Ubuntu.Devel.Dockerfile = Docker\Ubuntu.Devel.Dockerfile
- Docker\Ubuntu.Rolling.Dockerfile = Docker\Ubuntu.Rolling.Dockerfile
- Docker\UnitTest.sh = Docker\UnitTest.sh
- Docker\Version.sh = Docker\Version.sh
- EndProjectSection
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Actions", "Actions", "{CA5B5CC1-86DF-46DB-8AEB-12CDE10FB785}"
- ProjectSection(SolutionItems) = preProject
- .github\workflows\BuildDockerPush.yml = .github\workflows\BuildDockerPush.yml
- .github\workflows\BuildGitHubRelease.yml = .github\workflows\BuildGitHubRelease.yml
- .github\dependabot.yml = .github\dependabot.yml
- .github\workflows\DependabotAutoMerge.yml = .github\workflows\DependabotAutoMerge.yml
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlexCleanerTests", "PlexCleanerTests\PlexCleanerTests.csproj", "{D6124D3D-CC4F-448F-BF57-C1D7E2FAC226}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "Sandbox\Sandbox.csproj", "{134B6EC9-E20F-4CDA-B079-755BEC15E8C5}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Issues", "Issues", "{03A7B3AB-49DA-481B-AF3D-CF3D16BBFD0E}"
- ProjectSection(SolutionItems) = preProject
- .github\ISSUE_TEMPLATE\bug_report.yml = .github\ISSUE_TEMPLATE\bug_report.yml
- .github\ISSUE_TEMPLATE\config.yml = .github\ISSUE_TEMPLATE\config.yml
- EndProjectSection
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {1A686E6C-DD3F-4D71-A5F1-DCEE8F872A2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1A686E6C-DD3F-4D71-A5F1-DCEE8F872A2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1A686E6C-DD3F-4D71-A5F1-DCEE8F872A2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1A686E6C-DD3F-4D71-A5F1-DCEE8F872A2F}.Release|Any CPU.Build.0 = Release|Any CPU
- {D6124D3D-CC4F-448F-BF57-C1D7E2FAC226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D6124D3D-CC4F-448F-BF57-C1D7E2FAC226}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D6124D3D-CC4F-448F-BF57-C1D7E2FAC226}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D6124D3D-CC4F-448F-BF57-C1D7E2FAC226}.Release|Any CPU.Build.0 = Release|Any CPU
- {134B6EC9-E20F-4CDA-B079-755BEC15E8C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {134B6EC9-E20F-4CDA-B079-755BEC15E8C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {134B6EC9-E20F-4CDA-B079-755BEC15E8C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {134B6EC9-E20F-4CDA-B079-755BEC15E8C5}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {C560F57F-962E-4C71-8F27-939D06813302} = {D8F3D73D-A71B-4B79-982F-8491CCFB893A}
- {CA5B5CC1-86DF-46DB-8AEB-12CDE10FB785} = {D8F3D73D-A71B-4B79-982F-8491CCFB893A}
- {03A7B3AB-49DA-481B-AF3D-CF3D16BBFD0E} = {D8F3D73D-A71B-4B79-982F-8491CCFB893A}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {A127C929-B096-4BFD-8BAD-56A3860AEB94}
- EndGlobalSection
-EndGlobal
diff --git a/PlexCleaner.slnx b/PlexCleaner.slnx
new file mode 100644
index 00000000..8ab77210
--- /dev/null
+++ b/PlexCleaner.slnx
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/PlexCleaner/AssemblyVersion.cs b/PlexCleaner/AssemblyVersion.cs
index 3d3404dd..29e4e56e 100644
--- a/PlexCleaner/AssemblyVersion.cs
+++ b/PlexCleaner/AssemblyVersion.cs
@@ -1,5 +1,3 @@
-using System;
-using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
@@ -22,31 +20,28 @@ public static string GetBuildType()
return build;
}
- public static string GetName() => GetAssembly().GetName().Name;
+ public static string GetName() => GetAssembly().GetName().Name ?? string.Empty;
public static string GetInformationalVersion() =>
// E.g. 1.2.3+abc123.abc123
GetAssembly()
.GetCustomAttribute()
- ?.InformationalVersion;
+ ?.InformationalVersion
+ ?? string.Empty;
public static string GetFileVersion() =>
// E.g. 1.2.3.4
- GetAssembly().GetCustomAttribute()?.Version;
+ GetAssembly().GetCustomAttribute()?.Version
+ ?? string.Empty;
public static string GetReleaseVersion() =>
// E.g. 1.2.3 part of 1.2.3+abc123.abc123
// Use major.minor.build from informational version
GetInformationalVersion().Split('+', '-')[0];
- public static DateTime GetBuildDate() =>
- // Use assembly modified time as build date
- // https://stackoverflow.com/questions/1600962/displaying-the-build-date
- File.GetLastWriteTime(GetAssembly().Location).ToLocalTime();
-
private static Assembly GetAssembly()
{
- Assembly assembly = Assembly.GetEntryAssembly();
+ Assembly? assembly = Assembly.GetEntryAssembly();
assembly ??= Assembly.GetExecutingAssembly();
return assembly;
}
diff --git a/PlexCleaner/AudioProps.cs b/PlexCleaner/AudioProps.cs
index 050fb7b9..2e8b0aea 100644
--- a/PlexCleaner/AudioProps.cs
+++ b/PlexCleaner/AudioProps.cs
@@ -15,5 +15,5 @@ public class AudioProps(MediaProps mediaProps) : TrackProps(TrackType.Audio, med
// Required
// Format = track.Format;
// Codec = track.CodecId;
- public override bool Create(MediaInfoToolXmlSchema.Track track) => base.Create(track);
+ public override bool Create(MediaInfoToolJsonSchema.Track track) => base.Create(track);
}
diff --git a/PlexCleaner/CommandLineOptions.cs b/PlexCleaner/CommandLineOptions.cs
index 6c75d7a7..9c8ddc32 100644
--- a/PlexCleaner/CommandLineOptions.cs
+++ b/PlexCleaner/CommandLineOptions.cs
@@ -28,8 +28,8 @@ public class CommandLineParser
{
public CommandLineParser(string[] args)
{
- _root = CreateRootCommand();
- Result = _root.Parse(args);
+ Root = CreateRootCommand();
+ Result = Root.Parse(args);
}
public ParseResult Result { get; init; }
@@ -41,7 +41,7 @@ symbolResult is OptionResult optionResult
);
private static readonly List s_cliBypassList = ["--help", "--version"];
- private RootCommand _root { get; init; }
+ private RootCommand Root { get; init; }
private class CommandHandler(Func action) : SynchronousCommandLineAction
{
@@ -135,12 +135,9 @@ private class CommandHandler(Func action) : SynchronousCommand
private RootCommand CreateRootCommand()
{
- // TODO: https://github.com/dotnet/command-line-api/issues/2597
-#pragma warning disable IDE0028 // Simplify collection initialization
RootCommand rootCommand = new(
"Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc."
);
-#pragma warning restore IDE0028 // Simplify collection initialization
// Global options
rootCommand.Options.Add(_logFileOption);
diff --git a/PlexCleaner/ConfigFileJsonSchema.cs b/PlexCleaner/ConfigFileJsonSchema.cs
index ef0145f3..b32406bd 100644
--- a/PlexCleaner/ConfigFileJsonSchema.cs
+++ b/PlexCleaner/ConfigFileJsonSchema.cs
@@ -10,10 +10,9 @@
using System;
using System.IO;
using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Schema;
using System.Text.Json.Serialization;
-using System.Text.Json.Serialization.Metadata;
-using Json.Schema;
-using Json.Schema.Generation;
using Serilog;
namespace PlexCleaner;
@@ -27,7 +26,12 @@ public record ConfigFileJsonSchemaBase
[JsonPropertyName("$schema")]
[JsonPropertyOrder(-3)]
- public string Schema { get; } = SchemaUri;
+ [System.Diagnostics.CodeAnalysis.SuppressMessage(
+ "Performance",
+ "CA1822:Mark members as static",
+ Justification = "Schema must be written as instance property for JSON serialization."
+ )]
+ public string Schema => SchemaUri;
[JsonRequired]
[JsonPropertyOrder(-2)]
@@ -37,7 +41,7 @@ public record ConfigFileJsonSchemaBase
// v1
public record ConfigFileJsonSchema1 : ConfigFileJsonSchemaBase
{
- protected const int Version = 1;
+ public const int Version = 1;
[JsonRequired]
[JsonPropertyOrder(1)]
@@ -45,29 +49,27 @@ public record ConfigFileJsonSchema1 : ConfigFileJsonSchemaBase
// v2 : Replaced with ProcessOptions2
[Obsolete("Replaced with ProcessOptions2 in v2.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public ProcessOptions1 ProcessOptions { get; set; } = new();
// v3 : Replaced with ConvertOptions2
[Obsolete("Replaced with ConvertOptions2 in v3.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public ConvertOptions1 ConvertOptions { get; set; } = new();
// v3 : Replaced with VerifyOptions2
[Obsolete("Replaced with VerifyOptions2 in v3.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public VerifyOptions1 VerifyOptions { get; set; } = new();
- // TODO: Remove, never customized
- [JsonRequired]
- [JsonPropertyOrder(5)]
- public MonitorOptions1 MonitorOptions { get; set; } = new();
+ // v4 : Removed
+ // public MonitorOptions1 MonitorOptions { get; set; } = new();
}
// v2
public record ConfigFileJsonSchema2 : ConfigFileJsonSchema1
{
- protected new const int Version = 2;
+ public new const int Version = 2;
public ConfigFileJsonSchema2() { }
@@ -77,14 +79,14 @@ public ConfigFileJsonSchema2(ConfigFileJsonSchema1 configFileJsonSchema1)
// v2 : Added
// v3 : Replaced with ProcessOptions3
[Obsolete("Replaced with ProcessOptions3 in v3.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public new ProcessOptions2 ProcessOptions { get; set; } = new();
}
// v3
public record ConfigFileJsonSchema3 : ConfigFileJsonSchema2
{
- protected new const int Version = 3;
+ public new const int Version = 3;
public ConfigFileJsonSchema3() { }
@@ -97,13 +99,13 @@ public ConfigFileJsonSchema3(ConfigFileJsonSchema2 configFileJsonSchema2)
// v3 : Added
// v4 : Replaced with ProcessOptions4
[Obsolete("Replaced with ProcessOptions4 in v4.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public new ProcessOptions3 ProcessOptions { get; set; } = new();
// v3 : Added
// v4 : Replaced with ConvertOptions3
[Obsolete("Replaced with ConvertOptions3 in v4.")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public new ConvertOptions2 ConvertOptions { get; set; } = new();
// v3 : Added
@@ -117,25 +119,8 @@ public record ConfigFileJsonSchema4 : ConfigFileJsonSchema3
{
public new const int Version = 4;
- public static readonly JsonSerializerOptions JsonReadOptions = new()
- {
- AllowTrailingCommas = true,
- IncludeFields = true,
- NumberHandling = JsonNumberHandling.AllowReadingFromString,
- PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
- ReadCommentHandling = JsonCommentHandling.Skip,
- };
-
- public static readonly JsonSerializerOptions JsonWriteOptions = new()
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- IncludeFields = true,
- TypeInfoResolver = new DefaultJsonTypeInfoResolver().WithAddedModifier(
- ExcludeObsoletePropertiesModifier
- ),
- WriteIndented = true,
- NewLine = "\r\n",
- };
+ [JsonIgnore]
+ public int DeserializedVersion { get; private set; } = Version;
public ConfigFileJsonSchema4() { }
@@ -160,13 +145,6 @@ public ConfigFileJsonSchema4(ConfigFileJsonSchema3 configFileJsonSchema3)
private void Upgrade(int version)
{
- // v4:
- // ToolsOptions1
- // ProcessOptions4
- // ConvertOptions3
- // VerifyOptions2
- // MonitorOptions1
-
// v1
if (version == ConfigFileJsonSchema1.Version)
{
@@ -220,7 +198,16 @@ private void Upgrade(int version)
#pragma warning restore CS0618 // Type or member is obsolete
}
- // v4
+ // v4:
+ // ToolsOptions1
+ // ProcessOptions4
+ // ConvertOptions3
+ // VerifyOptions2
+ // MonitorOptions1
+
+ // Set schema version to current and save original version
+ SchemaVersion = Version;
+ DeserializedVersion = version;
}
public void SetDefaults()
@@ -228,7 +215,6 @@ public void SetDefaults()
ToolsOptions.SetDefaults();
ConvertOptions.SetDefaults();
ProcessOptions.SetDefaults();
- MonitorOptions.SetDefaults();
VerifyOptions.SetDefaults();
}
@@ -236,7 +222,6 @@ public bool VerifyValues() =>
ToolsOptions.VerifyValues()
&& ConvertOptions.VerifyValues()
&& ProcessOptions.VerifyValues()
- && MonitorOptions.VerifyValues()
&& VerifyOptions.VerifyValues();
public static void WriteDefaultsToFile(string path)
@@ -251,6 +236,22 @@ public static void WriteDefaultsToFile(string path)
public static ConfigFileJsonSchema FromFile(string path) => FromJson(File.ReadAllText(path));
+ public static ConfigFileJsonSchema OpenAndUpgrade(string path)
+ {
+ ConfigFileJsonSchema configJson = FromFile(path);
+ if (configJson.DeserializedVersion != Version)
+ {
+ Log.Warning(
+ "Writing ConfigFileJsonSchema upgraded from version {LoadedVersion} to {CurrentVersion}, {FileName}",
+ configJson.DeserializedVersion,
+ Version,
+ path
+ );
+ ToFile(path, configJson);
+ }
+ return configJson;
+ }
+
public static void ToFile(string path, ConfigFileJsonSchema json)
{
// Set the schema version to the current version
@@ -260,79 +261,97 @@ public static void ToFile(string path, ConfigFileJsonSchema json)
File.WriteAllText(path, ToJson(json));
}
+ // Write using custom serializer to ignore null or empty strings
private static string ToJson(ConfigFileJsonSchema json) =>
- JsonSerializer.Serialize(json, JsonWriteOptions);
+ JsonSerialization.SerializeIgnoreEmptyStrings(json, ConfigFileJsonContext.Default);
+ // Will throw on failure to deserialize
private static ConfigFileJsonSchema FromJson(string json)
{
// Deserialize the base class to get the schema version
ConfigFileJsonSchemaBase configFileJsonSchemaBase =
- JsonSerializer.Deserialize(json, JsonReadOptions);
- if (configFileJsonSchemaBase == null)
- {
- return null;
- }
+ JsonSerializer.Deserialize(json, ConfigFileJsonContext.Default.ConfigFileJsonSchemaBase)
+ ?? throw new JsonException("Failed to deserialize ConfigFileJsonSchemaBase");
if (configFileJsonSchemaBase.SchemaVersion != Version)
{
Log.Warning(
- "Converting ConfigFileJsonSchema from {JsonSchemaVersion} to {CurrentSchemaVersion}",
+ "Converting ConfigFileJsonSchema version from {JsonSchemaVersion} to {CurrentSchemaVersion}",
configFileJsonSchemaBase.SchemaVersion,
Version
);
}
// Deserialize the correct version
- return configFileJsonSchemaBase.SchemaVersion switch
+ switch (configFileJsonSchemaBase.SchemaVersion)
{
- ConfigFileJsonSchema1.Version => new ConfigFileJsonSchema(
- JsonSerializer.Deserialize(json, JsonReadOptions)
- ),
- ConfigFileJsonSchema2.Version => new ConfigFileJsonSchema(
- JsonSerializer.Deserialize(json, JsonReadOptions)
- ),
- ConfigFileJsonSchema3.Version => new ConfigFileJsonSchema(
- JsonSerializer.Deserialize(json, JsonReadOptions)
- ),
- Version => JsonSerializer.Deserialize(json, JsonReadOptions),
- _ => throw new NotImplementedException(),
- };
+ case ConfigFileJsonSchema1.Version:
+ ConfigFileJsonSchema1 configFileJsonSchema1 =
+ JsonSerializer.Deserialize(
+ json,
+ ConfigFileJsonContext.Default.ConfigFileJsonSchema1
+ ) ?? throw new JsonException("Failed to deserialize ConfigFileJsonSchema1");
+ return new ConfigFileJsonSchema(configFileJsonSchema1);
+ case ConfigFileJsonSchema2.Version:
+ ConfigFileJsonSchema2 configFileJsonSchema2 =
+ JsonSerializer.Deserialize(
+ json,
+ ConfigFileJsonContext.Default.ConfigFileJsonSchema2
+ ) ?? throw new JsonException("Failed to deserialize ConfigFileJsonSchema2");
+ return new ConfigFileJsonSchema(configFileJsonSchema2);
+ case ConfigFileJsonSchema3.Version:
+ ConfigFileJsonSchema3 configFileJsonSchema3 =
+ JsonSerializer.Deserialize(
+ json,
+ ConfigFileJsonContext.Default.ConfigFileJsonSchema3
+ ) ?? throw new JsonException("Failed to deserialize ConfigFileJsonSchema3");
+ return new ConfigFileJsonSchema(configFileJsonSchema3);
+ case Version:
+ ConfigFileJsonSchema configFileJsonSchema4 =
+ JsonSerializer.Deserialize(
+ json,
+ ConfigFileJsonContext.Default.ConfigFileJsonSchema4
+ ) ?? throw new JsonException("Failed to deserialize ConfigFileJsonSchema4");
+ return configFileJsonSchema4;
+ default:
+ throw new NotSupportedException(
+ $"Unsupported schema version: {configFileJsonSchemaBase.SchemaVersion}"
+ );
+ }
}
public static void WriteSchemaToFile(string path)
{
// Create JSON schema
- const string schemaVersion = "https://json-schema.org/draft/2020-12/schema";
- JsonSchema schemaBuilder = new JsonSchemaBuilder()
- .FromType(
- new SchemaGeneratorConfiguration { PropertyOrder = PropertyOrder.ByName }
- )
- .Title("PlexCleaner Configuration Schema")
- .Id(new Uri(SchemaUri))
- .Schema(new Uri(schemaVersion))
- .Build();
- string jsonSchema = JsonSerializer.Serialize(schemaBuilder, JsonWriteOptions);
-
- // Write to file
- File.WriteAllText(path, jsonSchema);
- }
+ JsonNode schemaNode = ConfigFileJsonContext.Default.Options.GetJsonSchemaAsNode(
+ typeof(ConfigFileJsonSchema)
+ );
- private static void ExcludeObsoletePropertiesModifier(JsonTypeInfo typeInfo)
- {
- // Only process objects
- if (typeInfo.Kind != JsonTypeInfoKind.Object)
- {
- return;
- }
+ // Add decorators
+ JsonObject schemaObject = schemaNode.AsObject();
+ _ = schemaObject.TryAdd("title", "PlexCleaner Configuration Schema");
+ _ = schemaObject.TryAdd("$id", SchemaUri);
+ _ = schemaObject.TryAdd("$schema", "https://json-schema.org/draft/2020-12/schema");
- // Iterate over all properties
- foreach (JsonPropertyInfo property in typeInfo.Properties)
- {
- // Do not serialize [Obsolete] items
- if (property.AttributeProvider?.IsDefined(typeof(ObsoleteAttribute), true) == true)
- {
- property.ShouldSerialize = (_, _) => false;
- }
- }
+ // Write to file
+ string schemaJson = schemaObject.ToJsonString(ConfigFileJsonContext.Default.Options);
+ File.WriteAllText(path, schemaJson);
}
}
+
+[JsonSourceGenerationOptions(
+ AllowTrailingCommas = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ IncludeFields = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ WriteIndented = true,
+ NewLine = "\r\n"
+)]
+[JsonSerializable(typeof(ConfigFileJsonSchemaBase))]
+[JsonSerializable(typeof(ConfigFileJsonSchema1))]
+[JsonSerializable(typeof(ConfigFileJsonSchema2))]
+[JsonSerializable(typeof(ConfigFileJsonSchema3))]
+[JsonSerializable(typeof(ConfigFileJsonSchema))]
+internal partial class ConfigFileJsonContext : JsonSerializerContext;
diff --git a/PlexCleaner/Convert.cs b/PlexCleaner/Convert.cs
index 78116bb4..70b7a3b2 100644
--- a/PlexCleaner/Convert.cs
+++ b/PlexCleaner/Convert.cs
@@ -119,7 +119,7 @@ public static bool ReMuxToMkv(string inputName, out string outputName)
public static bool ReMuxToMkv(
string inputName,
- SelectMediaProps selectMediaProps,
+ SelectMediaProps? selectMediaProps,
out string outputName
)
{
diff --git a/PlexCleaner/ConvertOptions.cs b/PlexCleaner/ConvertOptions.cs
index bec9dea9..1788d63f 100644
--- a/PlexCleaner/ConvertOptions.cs
+++ b/PlexCleaner/ConvertOptions.cs
@@ -1,6 +1,5 @@
using System;
using System.Text.Json.Serialization;
-using Json.Schema.Generation;
using Serilog;
namespace PlexCleaner;
@@ -9,29 +8,29 @@ namespace PlexCleaner;
public record HandBrakeOptions
{
[JsonRequired]
- public string Video { get; set; } = "";
+ public string Video { get; set; } = string.Empty;
[JsonRequired]
- public string Audio { get; set; } = "";
+ public string Audio { get; set; } = string.Empty;
}
// v2 : Added
public record FfMpegOptions
{
[JsonRequired]
- public string Video { get; set; } = "";
+ public string Video { get; set; } = string.Empty;
[JsonRequired]
- public string Audio { get; set; } = "";
+ public string Audio { get; set; } = string.Empty;
// v3 : Value no longer needs defaults
[JsonRequired]
- public string Global { get; set; } = "";
+ public string Global { get; set; } = string.Empty;
// v3 : Removed
[Obsolete("Removed in v3")]
- [JsonExclude]
- public string Output { get; set; } = "";
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
+ public string Output { get; set; } = string.Empty;
}
// v1
@@ -41,16 +40,16 @@ public record ConvertOptions1
// v2 : Replaced with FfMpegOptions and HandBrakeOptions
[Obsolete("Replaced in v2 with FfMpegOptions and HandBrakeOptions")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public bool EnableH265Encoder { get; set; }
[Obsolete("Replaced in v2 with FfMpegOptions and HandBrakeOptions")]
- [JsonExclude]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
public int VideoEncodeQuality { get; set; }
[Obsolete("Replaced in v2 with FfMpegOptions and HandBrakeOptions")]
- [JsonExclude]
- public string AudioEncodeCodec { get; set; } = "";
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWriting)]
+ public string AudioEncodeCodec { get; set; } = string.Empty;
}
// v2
@@ -122,7 +121,7 @@ private void Upgrade(int version)
// Obsolete
#pragma warning disable CS0618 // Type or member is obsolete
- convertOptions2.FfMpegOptions.Output = "";
+ convertOptions2.FfMpegOptions.Output = string.Empty;
#pragma warning restore CS0618 // Type or member is obsolete
// Remove old default global options
@@ -138,7 +137,7 @@ public void SetDefaults()
{
FfMpegOptions.Video = FfMpeg.DefaultVideoOptions;
FfMpegOptions.Audio = FfMpeg.DefaultAudioOptions;
- FfMpegOptions.Global = "";
+ FfMpegOptions.Global = string.Empty;
HandBrakeOptions.Video = HandBrake.DefaultVideoOptions;
HandBrakeOptions.Audio = HandBrake.DefaultAudioOptions;
diff --git a/PlexCleaner/Extensions.cs b/PlexCleaner/Extensions.cs
index 3facd054..44fa5a87 100644
--- a/PlexCleaner/Extensions.cs
+++ b/PlexCleaner/Extensions.cs
@@ -5,20 +5,28 @@ namespace PlexCleaner;
public static class Extensions
{
- public static bool LogAndPropagate(this ILogger logger, Exception exception, string function)
+ extension(ILogger logger)
{
- logger.Error(exception, "{Function}", function);
- return false;
- }
+ public bool LogAndPropagate(
+ Exception exception,
+ [System.Runtime.CompilerServices.CallerMemberName] string function = "unknown"
+ )
+ {
+ logger.Error(exception, "{Function}", function);
+ return false;
+ }
- public static bool LogAndHandle(this ILogger logger, Exception exception, string function)
- {
- logger.Error(exception, "{Function}", function);
- return true;
- }
+ public bool LogAndHandle(
+ Exception exception,
+ [System.Runtime.CompilerServices.CallerMemberName] string function = "unknown"
+ )
+ {
+ logger.Error(exception, "{Function}", function);
+ return true;
+ }
- public static ILogger LogOverrideContext(this ILogger logger) =>
- logger.ForContext();
+ public ILogger LogOverrideContext() => logger.ForContext();
+ }
public class LogOverride;
}
diff --git a/PlexCleaner/FfMpegBuilder.cs b/PlexCleaner/FfMpegBuilder.cs
index f3b3162b..b95deed0 100644
--- a/PlexCleaner/FfMpegBuilder.cs
+++ b/PlexCleaner/FfMpegBuilder.cs
@@ -17,10 +17,8 @@ public partial class FfMpeg
{
public class GlobalOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
public GlobalOptions Default() =>
- LogLevelError().HideBanner().NoStats().AbortOnEmptyOutput();
+ NoStdIn().LogLevelError().HideBanner().NoStats().AbortOnEmptyOutput();
public GlobalOptions LogLevel() => Add("-loglevel");
@@ -40,6 +38,8 @@ public GlobalOptions Default() =>
public GlobalOptions AbortOnEmptyOutput() => AbortOn("empty_output");
+ public GlobalOptions NoStdIn() => Add("-nostdin");
+
public GlobalOptions Add(string option) => Add(option, false);
public GlobalOptions Add(string option, bool escape)
@@ -48,15 +48,13 @@ public GlobalOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
public class InputOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
// https://trac.ffmpeg.org/ticket/2622
// Error with some PGS subtitles
// [matroska,webm @ 000001d77fb61ca0] Could not find codec parameters for stream 2 (Subtitle: hdmv_pgs_subtitle): unspecified size
@@ -115,15 +113,13 @@ public InputOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
public class OutputOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
// https://trac.ffmpeg.org/ticket/6375
// Too many packets buffered for output stream 0:1
// Set max_muxing_queue_size to large value to work around issue
@@ -214,7 +210,7 @@ public OutputOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
diff --git a/PlexCleaner/FfMpegIdetInfo.cs b/PlexCleaner/FfMpegIdetInfo.cs
index c78431ac..280deac6 100644
--- a/PlexCleaner/FfMpegIdetInfo.cs
+++ b/PlexCleaner/FfMpegIdetInfo.cs
@@ -66,7 +66,7 @@ public partial class FfMpegIdetInfo
public bool IsInterlaced(out double percentage) =>
MultiFrame.IsInterlaced(out percentage) || SingleFrame.IsInterlaced(out percentage);
- public static bool GetIdetInfo(string fileName, out FfMpegIdetInfo idetInfo, out string error)
+ public static bool GetIdetInfo(string fileName, out FfMpegIdetInfo? idetInfo, out string error)
{
// Get idet output from ffmpeg
idetInfo = null;
diff --git a/PlexCleaner/FfMpegTool.cs b/PlexCleaner/FfMpegTool.cs
index a250d366..989f8eba 100644
--- a/PlexCleaner/FfMpegTool.cs
+++ b/PlexCleaner/FfMpegTool.cs
@@ -4,7 +4,6 @@
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using CliWrap;
@@ -113,8 +112,7 @@ protected override bool GetLatestVersionWindows(out MediaToolInfo mediaToolInfo)
mediaToolInfo.FileName
);
}
- catch (Exception e)
- when (Log.Logger.LogAndHandle(e, MethodBase.GetCurrentMethod()?.Name))
+ catch (Exception e) when (Log.Logger.LogAndHandle(e))
{
return false;
}
@@ -138,7 +136,10 @@ public override bool Update(string updateFile)
// Delete the tool destination directory
string toolPath = GetToolFolder();
- Directory.Delete(toolPath, true);
+ if (Directory.Exists(toolPath))
+ {
+ Directory.Delete(toolPath, true);
+ }
// Build the versioned out folder from the downloaded filename
// E.g. ffmpeg-3.4-win64-static.zip to .\Tools\FFmpeg\ffmpeg-3.4-win64-static
@@ -146,7 +147,6 @@ public override bool Update(string updateFile)
// Rename the extract folder to the tool folder
// E.g. ffmpeg-3.4-win64-static to .\Tools\FFMpeg
- Directory.Delete(toolPath, true);
Directory.Move(extractPath, toolPath);
return true;
@@ -265,7 +265,7 @@ out string outputMap
public bool ConvertToMkv(
string inputName,
- SelectMediaProps selectMediaProps,
+ SelectMediaProps? selectMediaProps,
string outputName,
out string error
)
diff --git a/PlexCleaner/FfMpegToolJsonSchema.cs b/PlexCleaner/FfMpegToolJsonSchema.cs
index 85574322..95e494f9 100644
--- a/PlexCleaner/FfMpegToolJsonSchema.cs
+++ b/PlexCleaner/FfMpegToolJsonSchema.cs
@@ -2,15 +2,9 @@
using System.Text.Json;
using System.Text.Json.Serialization;
-// Convert JSON file to C# using app.quicktype.io
-// Set language, framework, namespace, list
-
// No JSON schema, use XML schema
// https://github.com/FFmpeg/FFmpeg/blob/master/doc/ffprobe.xsd
-// Convert array[] to List<>
-// Remove per item NullValueHandling = NullValueHandling.Ignore and add to Converter settings
-
// Use ffprobe example output:
// ffprobe -loglevel quiet -show_streams -print_format json file.mkv
@@ -26,14 +20,16 @@ public class FfProbe
[JsonPropertyName("format")]
public FormatInfo Format { get; } = new();
+ // Will throw on failure to deserialize
public static FfProbe FromJson(string json) =>
- JsonSerializer.Deserialize(json, ConfigFileJsonSchema.JsonReadOptions);
+ JsonSerializer.Deserialize(json, FfMpegToolJsonContext.Default.FfProbe)
+ ?? throw new JsonException("Failed to deserialize FfProbe");
}
public class FormatInfo
{
[JsonPropertyName("format_name")]
- public string FormatName { get; set; } = "";
+ public string FormatName { get; set; } = string.Empty;
[JsonPropertyName("duration")]
public double Duration { get; set; }
@@ -48,27 +44,26 @@ public class Track
public long Index { get; set; }
[JsonPropertyName("codec_name")]
- public string CodecName { get; set; } = "";
+ public string CodecName { get; set; } = string.Empty;
[JsonPropertyName("codec_long_name")]
- public string CodecLongName { get; set; } = "";
+ public string CodecLongName { get; set; } = string.Empty;
[JsonPropertyName("profile")]
- public string Profile { get; set; } = "";
+ public string Profile { get; set; } = string.Empty;
[JsonPropertyName("codec_type")]
- public string CodecType { get; set; } = "";
+ public string CodecType { get; set; } = string.Empty;
[JsonPropertyName("codec_tag_string")]
- public string CodecTagString { get; set; } = "";
+ public string CodecTagString { get; set; } = string.Empty;
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("field_order")]
- public string FieldOrder { get; set; } = "";
+ public string FieldOrder { get; set; } = string.Empty;
- // XSD says it is a Boolean, examples use an int
[JsonPropertyName("closed_captions")]
public int ClosedCaptions { get; set; }
@@ -84,23 +79,44 @@ public class Disposition
[JsonPropertyName("default")]
public int Default { get; set; }
+ [JsonIgnore]
+ public bool IsDefault => Default != 0;
+
[JsonPropertyName("forced")]
public int Forced { get; set; }
+ [JsonIgnore]
+ public bool IsForced => Forced != 0;
+
[JsonPropertyName("original")]
public int Original { get; set; }
+ [JsonIgnore]
+ public bool IsOriginal => Original != 0;
+
[JsonPropertyName("comment")]
public int Comment { get; set; }
+ [JsonIgnore]
+ public bool IsCommentary => Comment != 0;
+
[JsonPropertyName("hearing_impaired")]
public int HearingImpaired { get; set; }
+ [JsonIgnore]
+ public bool IsHearingImpaired => HearingImpaired != 0;
+
[JsonPropertyName("visual_impaired")]
public int VisualImpaired { get; set; }
+ [JsonIgnore]
+ public bool IsVisualImpaired => VisualImpaired != 0;
+
[JsonPropertyName("descriptions")]
public int Descriptions { get; set; }
+
+ [JsonIgnore]
+ public bool IsDescriptions => Descriptions != 0;
}
public class PacketInfo
@@ -112,7 +128,7 @@ public class PacketInfo
public class Packet
{
[JsonPropertyName("codec_type")]
- public string CodecType { get; set; } = "";
+ public string CodecType { get; set; } = string.Empty;
[JsonPropertyName("stream_index")]
public long StreamIndex { get; set; } = -1;
@@ -130,3 +146,14 @@ public class Packet
public long Size { get; set; } = -1;
}
}
+
+[JsonSourceGenerationOptions(
+ AllowTrailingCommas = true,
+ IncludeFields = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
+ ReadCommentHandling = JsonCommentHandling.Skip
+)]
+[JsonSerializable(typeof(FfMpegToolJsonSchema.FfProbe))]
+[JsonSerializable(typeof(FfMpegToolJsonSchema.Packet))]
+internal partial class FfMpegToolJsonContext : JsonSerializerContext;
diff --git a/PlexCleaner/FfProbeBuilder.cs b/PlexCleaner/FfProbeBuilder.cs
index 7af2cd8c..ae8077e5 100644
--- a/PlexCleaner/FfProbeBuilder.cs
+++ b/PlexCleaner/FfProbeBuilder.cs
@@ -24,8 +24,6 @@ public static string EscapeMovieFileName(string fileName) =>
public class GlobalOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
public GlobalOptions Default() => AnalyzeDuration("2G").ProbeSize("2G");
public GlobalOptions LogLevel() => Add("-loglevel");
@@ -54,7 +52,7 @@ public GlobalOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
@@ -62,8 +60,6 @@ public GlobalOptions Add(string option, bool escape)
// TODO: Rename to input or output options
public class FfProbeOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
public FfProbeOptions OutputFormat() => Add("-output_format");
public FfProbeOptions OutputFormat(string option) => OutputFormat().Add(option);
@@ -125,7 +121,7 @@ public FfProbeOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
diff --git a/PlexCleaner/FfProbeTool.cs b/PlexCleaner/FfProbeTool.cs
index d586345f..b8d7e76b 100644
--- a/PlexCleaner/FfProbeTool.cs
+++ b/PlexCleaner/FfProbeTool.cs
@@ -3,9 +3,9 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
-using System.Reflection;
using System.Text;
using System.Text.Json;
+using System.Text.Json.Serialization;
using System.Text.Json.Stream;
using System.Threading;
using System.Threading.Tasks;
@@ -94,7 +94,7 @@ out string error
if (
jsonStreamReader.TokenType == JsonTokenType.PropertyName
&& jsonStreamReader
- .GetString()
+ .GetString()!
.Equals("packets", StringComparison.OrdinalIgnoreCase)
)
{
@@ -131,14 +131,13 @@ out string error
// Send packet to delegate
// A false returns means delegate does not want any more packets
- if (
- !await packetFunc(
- await jsonStreamReader.DeserializeAsync(
- ConfigFileJsonSchema.JsonReadOptions,
- cancellationToken
- )
- )
- )
+ FfMpegToolJsonSchema.Packet? packet =
+ await jsonStreamReader.DeserializeAsync(
+ JsonReadOptions,
+ cancellationToken
+ );
+
+ if (packet == null || !await packetFunc(packet))
{
// Done
break;
@@ -184,8 +183,7 @@ out string error
);
return (false, string.Empty);
}
- catch (Exception e)
- when (Log.Logger.LogAndHandle(e, MethodBase.GetCurrentMethod()?.Name))
+ catch (Exception e) when (Log.Logger.LogAndHandle(e))
{
return (false, string.Empty);
}
@@ -196,7 +194,10 @@ public bool GetSubCcPackets(
Func packetFunc
)
{
- // Quickscan
+ // TODO: Switch to ffprobe and analyze_frames (when available in the shipping version).
+ // `ffprobe -i FILE -show_entries stream=closed_captions -select_streams v:0 -analyze_frames -read_intervals %X`
+
+ // Quickscan is not supported with subcc filter
// -t and read_intervals do not work with the subcc filter
// https://superuser.com/questions/1893673/how-to-time-limit-the-input-stream-duration-when-using-movie-filenameout0subcc
// ReMux using FFmpeg to a snippet file then scan the snippet file
@@ -297,7 +298,7 @@ public bool GetBitratePackets(
public bool GetMediaProps(string fileName, out MediaProps mediaProps)
{
- mediaProps = null;
+ mediaProps = null!;
return GetMediaPropsJson(fileName, out string json)
&& GetMediaPropsFromJson(json, fileName, out mediaProps);
}
@@ -363,7 +364,6 @@ out MediaProps mediaProps
{
// Deserialize
FfMpegToolJsonSchema.FfProbe ffProbe = FfMpegToolJsonSchema.FfProbe.FromJson(json);
- ArgumentNullException.ThrowIfNull(ffProbe);
if (ffProbe.Tracks.Count == 0)
{
Log.Error(
@@ -429,8 +429,7 @@ out MediaProps mediaProps
// TODO: Chapters
// TODO: Attachments
}
- catch (Exception e)
- when (Log.Logger.LogAndHandle(e, MethodBase.GetCurrentMethod()?.Name))
+ catch (Exception e) when (Log.Logger.LogAndHandle(e))
{
return false;
}
@@ -460,4 +459,14 @@ private static bool HasUnwantedTags(Dictionary tags) =>
s_undesirableTags.Any(tag => tag.Equals(key, StringComparison.OrdinalIgnoreCase))
);
}
+
+ public static readonly JsonSerializerOptions JsonReadOptions = new()
+ {
+ AllowTrailingCommas = true,
+ IncludeFields = true,
+ NumberHandling = JsonNumberHandling.AllowReadingFromString,
+ PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ TypeInfoResolver = FfMpegToolJsonContext.Default,
+ };
}
diff --git a/PlexCleaner/GitHubRelease.cs b/PlexCleaner/GitHubRelease.cs
index f9e7f86b..ba80efda 100644
--- a/PlexCleaner/GitHubRelease.cs
+++ b/PlexCleaner/GitHubRelease.cs
@@ -4,6 +4,7 @@
namespace PlexCleaner;
+// TODO: Convert to JSON Schema class
public class GitHubRelease
{
// Can throw HTTP exceptions
@@ -17,9 +18,9 @@ public static string GetLatestRelease(string repo)
Debug.Assert(json != null);
// Parse latest version from "tag_name"
- JsonNode releases = JsonNode.Parse(json);
+ JsonNode? releases = JsonNode.Parse(json);
Debug.Assert(releases != null);
- JsonNode versionTag = releases["tag_name"];
+ JsonNode? versionTag = releases["tag_name"];
Debug.Assert(versionTag != null);
return versionTag.ToString();
}
diff --git a/PlexCleaner/GlobalUsing.cs b/PlexCleaner/GlobalUsing.cs
index 1186f8af..d9c83552 100644
--- a/PlexCleaner/GlobalUsing.cs
+++ b/PlexCleaner/GlobalUsing.cs
@@ -1,9 +1,2 @@
-// TODO: info IDE0005: Using directive is unnecessary.
-// https://github.com/dotnet/roslyn/discussions/78254
-
-#pragma warning disable IDE0005 // Using directive is unnecessary.
-// Current schema version is v4
global using ConfigFileJsonSchema = PlexCleaner.ConfigFileJsonSchema4;
-// Current schema version is v4
-global using SidecarFileJsonSchema = PlexCleaner.SidecarFileJsonSchema4;
-#pragma warning restore IDE0005 // Using directive is unnecessary.
+global using SidecarFileJsonSchema = PlexCleaner.SidecarFileJsonSchema5;
diff --git a/PlexCleaner/HandBrakeBuilder.cs b/PlexCleaner/HandBrakeBuilder.cs
index 6b814a4f..7230fda4 100644
--- a/PlexCleaner/HandBrakeBuilder.cs
+++ b/PlexCleaner/HandBrakeBuilder.cs
@@ -8,8 +8,6 @@ public partial class HandBrake
{
public class GlobalOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
// TODO: Consolidate
public GlobalOptions Default() => this;
@@ -21,15 +19,13 @@ public GlobalOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
public class InputOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
public InputOptions Input() => Add("--input");
public InputOptions InputFile(string option) => Input().Add($"\"{option}\"");
@@ -60,15 +56,13 @@ public InputOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
public class OutputOptions(ArgumentsBuilder argumentsBuilder)
{
- private readonly ArgumentsBuilder _argumentsBuilder = argumentsBuilder;
-
public OutputOptions Output() => Add("--output");
public OutputOptions OutputFile(string option) => Output().Add($"\"{option}\"");
@@ -116,7 +110,7 @@ public OutputOptions Add(string option, bool escape)
{
return this;
}
- _ = _argumentsBuilder.Add(option, escape);
+ _ = argumentsBuilder.Add(option, escape);
return this;
}
}
diff --git a/PlexCleaner/HandBrakeTool.cs b/PlexCleaner/HandBrakeTool.cs
index b6b5c282..ae62e4f8 100644
--- a/PlexCleaner/HandBrakeTool.cs
+++ b/PlexCleaner/HandBrakeTool.cs
@@ -1,7 +1,6 @@
using System;
using System.Diagnostics;
using System.IO;
-using System.Reflection;
using System.Text.RegularExpressions;
using CliWrap;
using CliWrap.Buffered;
@@ -84,8 +83,7 @@ protected override bool GetLatestVersionWindows(out MediaToolInfo mediaToolInfo)
mediaToolInfo.FileName
);
}
- catch (Exception e)
- when (Log.Logger.LogAndHandle(e, MethodBase.GetCurrentMethod()?.Name))
+ catch (Exception e) when (Log.Logger.LogAndHandle(e))
{
return false;
}
diff --git a/PlexCleaner/JsonSerialization.cs b/PlexCleaner/JsonSerialization.cs
new file mode 100644
index 00000000..8b6ae494
--- /dev/null
+++ b/PlexCleaner/JsonSerialization.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+
+namespace PlexCleaner;
+
+internal static class JsonSerialization
+{
+ private static readonly ConditionalWeakTable<
+ JsonSerializerContext,
+ JsonSerializerOptions
+ > s_writeOptions = [];
+
+ public static string SerializeIgnoreEmptyStrings(T value, JsonSerializerContext context)
+ {
+ JsonSerializerOptions options = s_writeOptions.GetValue(context, CreateWriteOptions);
+ JsonTypeInfo typeInfo =
+ options.GetTypeInfo(typeof(T)) as JsonTypeInfo
+ ?? throw new InvalidOperationException(
+ $"Json type info not found for {typeof(T).FullName}"
+ );
+ return JsonSerializer.Serialize(value, typeInfo);
+ }
+
+ private static JsonSerializerOptions CreateWriteOptions(JsonSerializerContext context)
+ {
+ JsonSerializerOptions options = new(context.Options)
+ {
+ TypeInfoResolver = new IgnoreEmptyStringTypeInfoResolver(context),
+ };
+ return options;
+ }
+
+ private sealed class IgnoreEmptyStringTypeInfoResolver(IJsonTypeInfoResolver inner)
+ : IJsonTypeInfoResolver
+ {
+ public JsonTypeInfo? GetTypeInfo(Type type, JsonSerializerOptions options)
+ {
+ JsonTypeInfo? typeInfo = inner.GetTypeInfo(type, options);
+ if (typeInfo?.Kind != JsonTypeInfoKind.Object)
+ {
+ return typeInfo;
+ }
+
+ foreach (JsonPropertyInfo property in typeInfo.Properties)
+ {
+ if (property.PropertyType != typeof(string))
+ {
+ continue;
+ }
+
+ if (property.IsRequired)
+ {
+ continue;
+ }
+
+ Func