Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,31 +343,42 @@ flowchart TD
P21 --> P22["ReEncode(): ReEncode"]
P22 --> P23["VerifyAndRepair(): Verify, AutoRepair"]
end
P23 --> CLEAN
subgraph CLEAN["Cleanup re-run, S8"]
P24["RepairMetadataErrors(): AutoRepair"] --> P25["SetUnknownLanguageTracks(): SetUnknownLanguage"]
P25 --> P26["RemoveTags(): RemoveTags"]
P26 --> P27["RepairDefaultFlags(): SetTrackFlags"]
P27 --> P28["SetLastWriteTimeUtc(): RestoreFileTimestamp"]
P28 --> P29["VerifyMediaInfo()"]
P23 --> G6{"A step from RemoveCoverArt() on modified the file?"}
G6 -->|yes| CLEAN
G6 -->|no| P33
subgraph CLEAN["Cleanup re-run"]
P24["RemoveCoverArt(): ReMux"] --> P25["RemoveTags(): RemoveTags"]
P25 --> P26["RepairMetadataErrors(): AutoRepair"]
P26 --> P27["RepairMatroskaStructure(): Verify"]
P27 --> P28["RemoveClosedCaptions(): RemoveClosedCaptions"]
P28 --> P29["SetUnknownLanguageTracks(): SetUnknownLanguage"]
P29 --> P30["RemoveUnwantedLanguageTracks(): RemoveUnwantedLanguageTracks"]
P30 --> P31["RemoveDuplicateTracks(): RemoveDuplicateTracks"]
P31 --> P32["RepairDefaultFlags(): SetTrackFlags"]
end
P29 --> OK(["Result true"])
P32 --> P33["SetLastWriteTimeUtc(): RestoreFileTimestamp"]
P33 --> P34["VerifyMediaInfo()"]
P34 --> OK(["Result true"])
P03 -.->|"false or cancel"| AB
P06 -.->|false| AB
P07 -.->|"false or cancel"| AB
MAIN -.->|"any step false or cancel"| AB
CLEAN -.->|"any step false or cancel"| AB
P33 -.->|"false or cancel"| AB
P34 -.->|false| AB
THR["Any step throws while no cancel is requested"] -.-> AB
AB["Abort the file, failedOperation names the step"] --> G5{"Skipped as read-only, or a step threw?"}
G5 -->|yes| RET(["Result false, return the state and final path"])
G5 -->|no| DF["DeleteFailedFile(): DeleteInvalidFiles deletes the media file, and the sidecar of an MKV file, unless cancelled, a tool run for the file failed to launch, threw, or was killed by a signal outside Windows, or the state has PostDecodeTimestamps"]
DF --> RET
classDef suspect stroke:#c00,stroke-width:2px,stroke-dasharray:5 3
class P02,CLEAN suspect
class P02 suspect
```

`ProcessDriver.ProcessFiles()` counts the tool runs of each file that fail to launch, are cancelled, or throw, since such a run leaves the media unjudged. Outside Windows, it counts a tool run a signal terminates the same way, since that run ended before a result. It counts an FfMpeg verify run a signal terminates apart, since an external kill and a crash on corrupt media exit alike. `DeleteFailedFile()` does not delete a file that had any such tool run. It also keeps a file whose state carries `PostDecodeTimestamps`. A delete that throws is logged, and the file counts as not deleted.

After `VerifyAndRepair()`, when a step from `RemoveCoverArt()` on modified the file, the steps from `RemoveCoverArt()` through `RepairDefaultFlags()` run again, except `VerifyMediaInfo()` and `DeInterlace()`. A rewrite by FfMpeg or HandBrake can undo their work. Otherwise the first pass already ran on the final media, so running the steps again would change nothing.

`VerifyTrackCounts()` fails a file with no video track or more than one, and only warns when there is no audio track. `VerifyMediaInfo()` fails a file when FfProbe, MkvMerge and MediaInfo disagree on the track count of any type. A rename from a remux or the lowercase step reaches the result as the new path.

### Sidecar Refresh
Expand Down Expand Up @@ -740,7 +751,6 @@ Each entry describes a branch the diagrams mark, as the code behaves today, and
- **S3** ([#1070](https://github.com/ptr727/PlexCleaner/issues/1070)): Deleting an orphan sidecar returns false, so the file counts as an error and runs `DeleteFailedFile()`. Deleting a non-MKV file in `DeleteNonMkvFile()` counts as a success.
- **S6** ([#1073](https://github.com/ptr727/PlexCleaner/issues/1073)): `Convert.ReMuxToMkv()` moves its output to the `.mkv` name with overwrite, so `name.avi` replaces an existing `name.mkv`. `MakeExtensionLowercase()` replaces a lowercase sibling on a case-sensitive filesystem the same way.
- **S7** ([#1074](https://github.com/ptr727/PlexCleaner/issues/1074)): `RepairMetadataErrors()` can pass with errors left. `RepairMetadataRemux()` does nothing with `ReMux` off, and `RepairMetadataFlags()` neither re-checks its result nor marks `VerifyFailed`.
- **S8** ([#1075](https://github.com/ptr727/PlexCleaner/issues/1075)): The cleanup re-run after `VerifyAndRepair()` repeats only metadata, language, tag and default flag cleanup. It skips language and duplicate track removal, captions, cover art and the structure check after a re-encode repair.
- **S9** ([#1076](https://github.com/ptr727/PlexCleaner/issues/1076)): `ReEncode()` and `DeInterlace()` only warn when their flag is already set. `ReEncode()` processes output that still matches its selection again on every run. `DeInterlace()` does the same while a tool still flags the video as interlaced, since its idet scan skips a file already `DeInterlaced`.
- **S10** ([#1077](https://github.com/ptr727/PlexCleaner/issues/1077)): `RemoveCoverArt()` removes nothing when only MediaInfo reports cover art, and it does not re-check after the FfProbe path falls through to removing tags. Only a debug assertion catches the leftover.
- **S11** ([#1078](https://github.com/ptr727/PlexCleaner/issues/1078), [#1079](https://github.com/ptr727/PlexCleaner/issues/1079)): Failed bitrate and packet analysis results are ignored unless a signal ended the run, and `BitrateExceeded` is measured before any repair. No processing step clears it, so it lasts until the media changes outside PlexCleaner. `IsMediaAndToolsCurrent()` treats tools as current when `SidecarUpdateOnToolChange` is on, the opposite sense of `Read()`.
Expand Down
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
- So each scan runs once per file without a verify, and a `--quickscan` sample sets neither flag.
- A file an earlier version marked `Verified` with the `Verify` option off is not re-verified automatically, and the `createsidecar` command resets its state.
- With `UseSidecarFiles` off, a step that only changes a file's state does not run the media info tools again.
- When a cleanup step or a later step modifies a file, processing repeats the cleanup steps after the verify and repair step.
- The repeated steps are cover art, tags, metadata, Matroska structure, closed captions, track language, duplicate tracks and default flags.
- A rewrite by FfMpeg or HandBrake, such as a deinterlace, re-encode or repair, can undo their work.
- Before, it repeated only the metadata, unknown language, tag and default flag steps.
- Version 3.22:
- Added always-on runtime metrics published via `System.Diagnostics.Metrics` under the `PlexCleaner.Process` meter, readable with `dotnet-counters` with no extra infrastructure.
- Overall progress is operation-weighted: each heavy full-file operation (the closed-caption and interlace scans, bitrate analysis, re-encode, deinterlace, and verify) counts the file's size as work to do when it starts and as work done when it finishes, so a run mixing tiny and huge files reports the actual work completed, and the total grows as the non-deterministic per-file path is discovered.
Expand Down
94 changes: 68 additions & 26 deletions PlexCleaner/Process.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ out string? failedOperation
ProcessFile? processFile = null;
bool result;
bool skipped = false;
bool modifiedBeforeCleanup = false;

// Current step, reported as the failure reason on error
string? operation = null;
Expand Down Expand Up @@ -188,6 +189,9 @@ out string? failedOperation
break;
}

modifiedBeforeCleanup = modified;
modified = false;

// Remove all cover art attachments or video tracks that interfere with processing logic
// Conditional on ReMux option, fails if cover art is present and ReMux not enabled
operation = nameof(processFile.RemoveCoverArt);
Expand Down Expand Up @@ -324,33 +328,70 @@ out string? failedOperation

// FfMpeg or HandBrake could undo the previous cleanup, repeat cleanup
// Split so the reported operation names the actual failing step
operation = nameof(processFile.RepairMetadataErrors);
if (!processFile.RepairMetadataErrors(ref modified) || Program.IsCancelled())
{
// Error
result = false;
break;
}
operation = nameof(processFile.SetUnknownLanguageTracks);
if (!processFile.SetUnknownLanguageTracks(ref modified) || Program.IsCancelled())
{
// Error
result = false;
break;
}
operation = nameof(processFile.RemoveTags);
if (!processFile.RemoveTags(ref modified) || Program.IsCancelled())
{
// Error
result = false;
break;
}
operation = nameof(processFile.RepairDefaultFlags);
if (!processFile.RepairDefaultFlags(ref modified) || Program.IsCancelled())
if (modified)
Comment thread
ptr727 marked this conversation as resolved.
Resolved
{
// Error
result = false;
break;
operation = nameof(processFile.RemoveCoverArt);
if (!processFile.RemoveCoverArt(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
operation = nameof(processFile.RemoveTags);
if (!processFile.RemoveTags(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
operation = nameof(processFile.RepairMetadataErrors);
if (!processFile.RepairMetadataErrors(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
operation = nameof(processFile.RepairMatroskaStructure);
if (!processFile.RepairMatroskaStructure(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
operation = nameof(processFile.RemoveClosedCaptions);
if (
!processFile.RemoveClosedCaptions(true, ref modified)
Comment thread
ptr727 marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
|| Program.IsCancelled()
)
{
result = false;
break;
}
operation = nameof(processFile.SetUnknownLanguageTracks);
if (
!processFile.SetUnknownLanguageTracks(ref modified) || Program.IsCancelled()
)
{
result = false;
break;
}
operation = nameof(processFile.RemoveUnwantedLanguageTracks);
if (
!processFile.RemoveUnwantedLanguageTracks(ref modified)
Comment thread
ptr727 marked this conversation as resolved.
Resolved
|| Program.IsCancelled()
)
{
result = false;
break;
}
operation = nameof(processFile.RemoveDuplicateTracks);
if (!processFile.RemoveDuplicateTracks(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
operation = nameof(processFile.RepairDefaultFlags);
if (!processFile.RepairDefaultFlags(ref modified) || Program.IsCancelled())
{
result = false;
break;
}
}

// Restore the file timestamp
Expand Down Expand Up @@ -383,6 +424,7 @@ out string? failedOperation
skipped = true;
}

modified |= modifiedBeforeCleanup;
if (!result)
{
failedOperation = operation;
Expand Down
Loading