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
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.

- Version 3.20:
- Added per-file stateful log level filtering to enable information level output after a warning or error event is detected, overriding the `--logwarning` warning only filter. Prior to this change the log output would only contain the trigger warning or error event, now it will also log all subsequent information level events for that file during its processing cycle.
- Always log the end-of-run summary at information level, even with `--logwarning`, so the modified, error, and verify-failed counts are recorded for every processing run.
- Handle the `SIGINT`, `SIGTERM`, and `SIGQUIT` termination signals (`docker stop`, `Ctrl+C`) so processing is interrupted gracefully and the summary and exit code are logged before exit. The custom `Ctrl+Q`/`Ctrl+Z` exit keys are removed in favor of the standard signals.
- Version 3.19:
- Reworked the CI/CD pipeline to a branch-scoped self-publishing model: a weekly scheduled run (and manual dispatch) publishes both `main` (stable, Docker `latest`) and `develop` (prerelease, Docker `develop`) - native executables, the multi-arch Docker image, and the GitHub release - while merges accumulate until the next run. No application changes.
- Added `WORKFLOW.md` (the canonical CI/CD specification) and `repo-config/` (rulesets and repository settings as code).
Expand Down
6 changes: 1 addition & 5 deletions PlexCleaner/Monitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ public class Monitor

private readonly Lock _watchLock = new();

private static void LogMonitorMessage()
{
Log.Information("Monitoring folders ...");
Program.LogInterruptMessage();
}
private static void LogMonitorMessage() => Log.Information("Monitoring folders ...");

public bool MonitorFolders(List<string> folders)
{
Expand Down
2 changes: 1 addition & 1 deletion PlexCleaner/Process.cs
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ public static bool DeleteEmptyFolders(IEnumerable<string> folderList)
fatalError = true;
}

Log.Information("Deleted folders : {Deleted}", totalDeleted);
Log.Logger.LogOverrideContext().Information("Deleted folders : {Deleted}", totalDeleted);

return !fatalError;
}
Expand Down
10 changes: 5 additions & 5 deletions PlexCleaner/ProcessDriver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,11 +220,11 @@ Func<string, bool> taskFunc
// Stop the timer
timer.Stop();

// Done
Log.Information("Completed {TaskName}", taskName);
Log.Information("Processing time : {Elapsed}", timer.Elapsed);
Log.Information("Total files : {Count}", totalCount);
Log.Information("Error files : {Count}", errorCount);
// Done, force logging so the summary survives the warning floor and an interrupted run
Log.Logger.LogOverrideContext().Information("Completed {TaskName}", taskName);
Log.Logger.LogOverrideContext().Information("Processing time : {Elapsed}", timer.Elapsed);
Log.Logger.LogOverrideContext().Information("Total files : {Count}", totalCount);
Log.Logger.LogOverrideContext().Information("Error files : {Count}", errorCount);

return !error;
}
Expand Down
86 changes: 24 additions & 62 deletions PlexCleaner/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace PlexCleaner;

public static class Program
{
// Never disposed, so signal handlers can safely call Cancel() for the process lifetime
private static readonly CancellationTokenSource s_cancelSource = new();
private static readonly Lazy<HttpClient> s_httpClient = new(CreateHttpClient);

Expand Down Expand Up @@ -41,15 +42,6 @@ private static HttpClient CreateHttpClient()
private static int MakeExitCode(bool success) =>
success ? (int)ExitCode.Success : (int)ExitCode.Error;

public static void LogInterruptMessage()
{
// Keyboard handler is only active if input is not redirected
if (!Console.IsInputRedirected)
{
Console.WriteLine("Press Ctrl+C or Ctrl+Z or Ctrl+Q to exit.");
}
}

private static int Main(string[] args)
{
// Wait for debugger to attach
Expand All @@ -71,12 +63,20 @@ private static int Main(string[] args)

// Setup
CreateLogger();
Console.CancelKeyPress += CancelEventHandler;
Task? consoleKeyTask = null;
if (!Console.IsInputRedirected)
{
consoleKeyTask = Task.Run(KeyPressHandler);
}

// Handle termination signals to cancel gracefully and still log the summary before exit
PosixSignalRegistration sigIntRegistration = PosixSignalRegistration.Create(
PosixSignal.SIGINT,
PosixSignalHandler
);
PosixSignalRegistration sigTermRegistration = PosixSignalRegistration.Create(
PosixSignal.SIGTERM,
PosixSignalHandler
);
PosixSignalRegistration sigQuitRegistration = PosixSignalRegistration.Create(
PosixSignal.SIGQUIT,
PosixSignalHandler
);
Comment thread
ptr727 marked this conversation as resolved.

// Keep the system from going to sleep
KeepAwake.PreventSleep();
Expand All @@ -90,8 +90,10 @@ private static int Main(string[] args)

// Cleanup
Cancel();
consoleKeyTask?.Wait();
Console.CancelKeyPress -= CancelEventHandler;
// Unhook signals before flushing so a second signal reverts to default OS termination
sigIntRegistration.Dispose();
sigTermRegistration.Dispose();
sigQuitRegistration.Dispose();
keepAwakeTimer.Stop();
KeepAwake.AllowSleep();

Expand Down Expand Up @@ -135,47 +137,13 @@ public static void VerifyLatestVersion()
}
}

private static void KeyPressHandler()
{
for (; ; )
{
// Wait on key available or cancelled
while (!Console.KeyAvailable)
{
if (WaitForCancel(100))
{
// Done
return;
}
}

// Read key and hide from console display
ConsoleKeyInfo keyInfo = Console.ReadKey(true);

// Break on Ctrl+Q or Ctrl+Z, Ctrl+C and Ctrl+Break is handled in cancel handler
if (
keyInfo.Key is ConsoleKey.Q or ConsoleKey.Z
&& keyInfo.Modifiers == ConsoleModifiers.Control
)
{
// Signal the cancel event
Cancel(ConsoleModifiers.Control, keyInfo.Key);

// Done
return;
}
}
}

private static void CancelEventHandler(object? sender, ConsoleCancelEventArgs eventArgs)
private static void PosixSignalHandler(PosixSignalContext context)
{
Log.Warning("Cancel event triggered : {EventType}", eventArgs.SpecialKey);

// Keep running and do graceful exit
eventArgs.Cancel = true;
Log.Warning("Operation interrupted : {Signal}", context.Signal);

// Signal the cancel event, use Ctrl+Break as signal
Cancel(ConsoleModifiers.Control, ConsoleKey.Pause);
// Keep running and do a graceful exit so the summary and exit code are logged
context.Cancel = true;
Cancel();
}

private static void WaitForDebugger()
Expand Down Expand Up @@ -563,12 +531,6 @@ public static void Cancel() =>
// Signal cancel
s_cancelSource.Cancel();

public static void Cancel(ConsoleModifiers modifiers, ConsoleKey key)
{
Log.Warning("Operation interrupted : {Modifiers}+{Key}", modifiers, key);
Cancel();
}

public static CancellationToken CancelToken() => s_cancelSource.Token;

private enum ExitCode
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Utility to optimize media files for Direct Play in Plex, Emby, Jellyfin, etc.
**Summary:**

- Added per-file stateful log level filtering to enable information level output after a warning or error event is detected, overriding the `--logwarning` warning only filter.
- Always log the end-of-run summary, and handle stop signals (`docker stop`, `Ctrl+C`) so processing stops gracefully and the summary and exit code are logged before exit.

See [Release History](./HISTORY.md) for complete release notes and older versions.

Expand Down Expand Up @@ -222,6 +223,7 @@ services:
image: docker.io/ptr727/plexcleaner:latest # Use :develop for pre-release builds
container_name: PlexCleaner
restart: unless-stopped
stop_grace_period: 30s # Allow time to finish the current file and log the summary on stop
user: 1000:100 # Change to match your nonroot:users
command:
- /PlexCleaner/PlexCleaner
Expand All @@ -237,6 +239,8 @@ services:
- /data/media:/media # Map host path /data/media to container /media (read/write)
```

On stop (`docker stop`, or Compose shutdown), PlexCleaner handles the termination signal, stops processing gracefully, and logs the run summary and exit code before exiting. Because it aborts the in-flight file rather than killing it mid-write, allow enough grace for a long re-encode to unwind: set `stop_grace_period` (Compose) or `--stop-timeout` / `docker stop --time 30` (Docker run). Without enough grace the container is force-killed (`SIGKILL`) and the summary is lost.

#### Docker Run Examples

For a simple one-time process operation, see the [Getting Started](#getting-started) example.
Expand Down