From 21f6b259f131151d3ae330f7c483c2a535c8500f Mon Sep 17 00:00:00 2001 From: Hai Vo Date: Wed, 5 Aug 2026 15:47:16 +0700 Subject: [PATCH] feat: add opt-out logging flag and log operation completion, not just start NativeEncoder previously only logged "Start native probe/convert/cut" -- there was no matching completion or success log, so a consumer's log stream couldn't tell a finished operation from a hung one without also correlating exceptions. Add a matching completion log to Probe (now includes format/codec/duration), ConvertFile, and CutFile (which also now surfaces AudioCutter.Cut's previously-discarded bool return value -- whether the requested range actually produced a file). Also add an enableLogging flag (default true, so this is additive/ non-breaking): NativeEncoder(logger, enableLogging: false) and AddEggEncoder(enableLogging: false) fully suppress Start/Completed/Failed logging. Standard ILogger category filtering (Logging:LogLevel:EggEncoder.NativeEncoder in appsettings.json) already covers this with zero code changes, but an explicit flag is a fair convenience on top of that for callers who'd rather not touch logging config just to silence one library. Docs (CLAUDE.md, llms-full.txt, Configuration.html, Dependency-Injection.html, index.html) updated to drop the now-inaccurate "no options" framing. --- CLAUDE.md | 2 +- docs/Configuration.html | 10 +-- docs/Dependency-Injection.html | 2 +- docs/index.html | 2 +- llms-full.txt | 3 +- src/EggEncoder.UnitTests/NativeEncoderTest.cs | 69 +++++++++++++++++++ src/EggEncoder/NativeEncoder.cs | 42 ++++++++--- src/EggEncoder/ServiceCollectionExtensions.cs | 6 +- 8 files changed, 117 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9e087e1..c9ceef0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -43,7 +43,7 @@ dotnet test src/EggEncoder.UnitTests/EggEncoder.UnitTests.csproj --configuration - **`Native/`** — `FlacNative.cs`/`Mp3Native.cs` (`[LibraryImport]` P/Invoke declarations), `NativeLibraryLoader.cs` (a `[ModuleInitializer]` that registers a custom `DllImportResolver` so `libFLAC`/`libmp3lame` load from `Native/win-x64/` relative to `AppContext.BaseDirectory` regardless of the consuming app's working directory) - **`Waveform/WaveformCalculator.cs`** — streaming peak-window calculator fed blocks during decode, used by every codec's probe path to produce `ProbeResult.Waveform` - **`Results/ProbeResult.cs`** — the public `ProbeResult` DTO returned by every `Probe` call -- **`ServiceCollectionExtensions.cs`** — `AddEggEncoder()` DI registration; no options, registers `IMediaEncoder` → `NativeEncoder` (scoped) +- **`ServiceCollectionExtensions.cs`** — `AddEggEncoder(enableLogging: true)` DI registration; registers `IMediaEncoder` → `NativeEncoder` (scoped). `enableLogging: false` fully silences `NativeEncoder`'s start/completion/failure logs ### Native binary packaging diff --git a/docs/Configuration.html b/docs/Configuration.html index 8588298..71b50ff 100644 --- a/docs/Configuration.html +++ b/docs/Configuration.html @@ -4,7 +4,7 @@ Configuration — EggEncoder - + @@ -42,10 +42,12 @@

Configuration

-

There Isn't Any

-

EggEncoder is fully native and has no options to set. AddEggEncoder() takes no parameters:

-
builder.Services.AddEggEncoder();
+

Almost None

+

EggEncoder is fully native and has one optional setting: whether it logs. AddEggEncoder() takes an optional enableLogging parameter (default true):

+
builder.Services.AddEggEncoder();                      // logs via ILogger<NativeEncoder> (default)
+builder.Services.AddEggEncoder(enableLogging: false);   // fully silent

This registers IMediaEncoderNativeEncoder (scoped). There's no engine to pick, no binary path to provide, and nothing that can fail to resolve at startup due to missing external tooling.

+

Every Probe/ConvertFile/CutFile call logs a start and a completion message at LogInformation, and logs (then rethrows) any failure at LogError — set enableLogging: false to suppress all of it, or leave it on and control verbosity the standard way via your app's logging configuration (e.g. "Logging:LogLevel:EggEncoder.NativeEncoder": "None" in appsettings.json).

Native Binary Resolution

The bundled libmp3lame.dll and libFLAC.dll are located automatically at runtime by NativeLibraryLoader, relative to AppContext.BaseDirectory — this is not configurable and requires no setup. See Native Binary Packaging for how the files get there.

diff --git a/docs/Dependency-Injection.html b/docs/Dependency-Injection.html index 4bc12c0..5919c3e 100644 --- a/docs/Dependency-Injection.html +++ b/docs/Dependency-Injection.html @@ -43,7 +43,7 @@

Dependency Injection

What AddEggEncoder Registers

-

services.AddEggEncoder() registers exactly one service: IMediaEncoderNativeEncoder (scoped). Inject it into a controller, a scoped service, or resolve it from an IServiceScope in a background worker.

+

services.AddEggEncoder() registers exactly one service: IMediaEncoderNativeEncoder (scoped). Inject it into a controller, a scoped service, or resolve it from an IServiceScope in a background worker. Pass AddEggEncoder(enableLogging: false) to silence NativeEncoder's logging entirely — see Configuration.

Typical Registration

// Program.cs
diff --git a/docs/index.html b/docs/index.html
index 9d7e129..beeb1dc 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -122,7 +122,7 @@ 

Key Features

🔌
One-Line DI
-

services.AddEggEncoder() registers IMediaEncoder — no options, no config required.

+

services.AddEggEncoder() registers IMediaEncoder — no config required, one optional flag to silence logging.

diff --git a/llms-full.txt b/llms-full.txt index f1efbd4..a7e0c9f 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -58,7 +58,8 @@ Only fields this library can genuinely compute are populated -- nothing is fille ```csharp using EggEncoder; -builder.Services.AddEggEncoder(); // no options — registers IMediaEncoder -> NativeEncoder (scoped) +builder.Services.AddEggEncoder(); // registers IMediaEncoder -> NativeEncoder (scoped) +// optional: AddEggEncoder(enableLogging: false) to silence Start/Completed/Failed logging entirely ``` ### Consuming IMediaEncoder diff --git a/src/EggEncoder.UnitTests/NativeEncoderTest.cs b/src/EggEncoder.UnitTests/NativeEncoderTest.cs index c3aa126..29a09c6 100644 --- a/src/EggEncoder.UnitTests/NativeEncoderTest.cs +++ b/src/EggEncoder.UnitTests/NativeEncoderTest.cs @@ -192,6 +192,75 @@ public async Task CutFile_ToNestedMissingDirectory_Should_CreateDirectory_And_Cu } } + [Fact] + public async Task Probe_Should_Log_Start_And_Completion() + { + await _nativeEncoder.Probe(_wavFixturePath); + + _logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged"); + } + + [Fact] + public async Task ConvertFile_Should_Log_Start_And_Completion() + { + var tempDirectory = CreateTempDirectory(); + + try + { + var destPath = Path.Combine(tempDirectory, "dest.flac"); + + await _nativeEncoder.ConvertFile(_wavFixturePath, destPath); + + _logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged"); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [Fact] + public async Task CutFile_Should_Log_Start_And_Completion() + { + var tempDirectory = CreateTempDirectory(); + + try + { + var destPath = Path.Combine(tempDirectory, "cut.wav"); + + await _nativeEncoder.CutFile(_wavFixturePath, destPath, 0, 1); + + _logger.Invocations.Should().HaveCountGreaterThanOrEqualTo(2, "both a start and a completion message should be logged"); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [Fact] + public async Task Probe_WithLoggingDisabled_Should_Not_Log() + { + var logger = new Mock>(); + var encoder = new NativeEncoder(logger.Object, enableLogging: false); + + await encoder.Probe(_wavFixturePath); + + logger.Invocations.Should().BeEmpty(); + } + + [Fact] + public async Task Probe_WithLoggingDisabled_OnFailure_Should_Not_Log() + { + var logger = new Mock>(); + var encoder = new NativeEncoder(logger.Object, enableLogging: false); + + var act = () => encoder.Probe("file.ogg"); + + await act.Should().ThrowExactlyAsync(); + logger.Invocations.Should().BeEmpty(); + } + private static void AssertNonEmptyWaveform(IReadOnlyList? waveform) { waveform.Should().NotBeNull(); diff --git a/src/EggEncoder/NativeEncoder.cs b/src/EggEncoder/NativeEncoder.cs index 6d23c28..5ac2340 100644 --- a/src/EggEncoder/NativeEncoder.cs +++ b/src/EggEncoder/NativeEncoder.cs @@ -15,17 +15,19 @@ public class NativeEncoder : IMediaEncoder private const int FramesPerBlock = 4096; private readonly ILogger _logger; + private readonly bool _loggingEnabled; - public NativeEncoder(ILogger logger) + public NativeEncoder(ILogger logger, bool enableLogging = true) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _loggingEnabled = enableLogging; } public Task Probe(string filePath) { try { - _logger.LogInformation($"Start native probe '{filePath}'"); + LogInformation($"Start native probe '{filePath}'"); var extension = Path.GetExtension(filePath).ToLowerInvariant(); var result = extension switch @@ -39,13 +41,13 @@ public Task Probe(string filePath) _ => throw new NotSupportedException($"Probing '{extension}' files is not supported by the native audio encoder") }; - _logger.LogInformation($"Probe '{filePath}', format: '{result.FormatName}', codec: '{result.CodecName}', duration: {result.DurationSeconds}s"); + LogInformation($"Completed native probe '{filePath}': format '{result.FormatName}', codec '{result.CodecName}', duration {result.DurationSeconds}s"); return Task.FromResult(result); } catch (Exception e) { - _logger.LogError(e, $"Failed to probe '{filePath}' {e.Message}"); + LogError(e, $"Failed to probe '{filePath}' {e.Message}"); throw; } } @@ -54,16 +56,18 @@ public Task ConvertFile(string sourceFilePath, string destFilePath) { try { - _logger.LogInformation($"Start native convert '{sourceFilePath}' to '{destFilePath}'"); + LogInformation($"Start native convert '{sourceFilePath}' to '{destFilePath}'"); EnsureDestinationDirectory(destFilePath); AudioCutter.Convert(sourceFilePath, destFilePath); + LogInformation($"Completed native convert '{sourceFilePath}' to '{destFilePath}'"); + return Task.CompletedTask; } catch (Exception e) { - _logger.LogError(e, $"Failed to convert from '{sourceFilePath}' to '{destFilePath}' {e.Message}"); + LogError(e, $"Failed to convert from '{sourceFilePath}' to '{destFilePath}' {e.Message}"); throw; } } @@ -72,20 +76,40 @@ public Task CutFile(string sourceFilePath, string destFilePath, int startInSecon { try { - _logger.LogInformation($"Start native cut '{sourceFilePath}' to '{destFilePath}' start {startInSeconds} end {endInSeconds}"); + LogInformation($"Start native cut '{sourceFilePath}' to '{destFilePath}' start {startInSeconds} end {endInSeconds}"); EnsureDestinationDirectory(destFilePath); - AudioCutter.Cut(sourceFilePath, destFilePath, startInSeconds, endInSeconds); + var produced = AudioCutter.Cut(sourceFilePath, destFilePath, startInSeconds, endInSeconds); + + LogInformation(produced + ? $"Completed native cut '{sourceFilePath}' to '{destFilePath}'" + : $"Completed native cut '{sourceFilePath}' to '{destFilePath}': requested range was outside the source duration, no file written"); return Task.CompletedTask; } catch (Exception e) { - _logger.LogError(e, $"Failed to cut from '{sourceFilePath}' to '{destFilePath}' {e.Message}"); + LogError(e, $"Failed to cut from '{sourceFilePath}' to '{destFilePath}' {e.Message}"); throw; } } + private void LogInformation(string message) + { + if (_loggingEnabled) + { + _logger.LogInformation(message); + } + } + + private void LogError(Exception exception, string message) + { + if (_loggingEnabled) + { + _logger.LogError(exception, message); + } + } + private static void EnsureDestinationDirectory(string destFilePath) { var directory = Path.GetDirectoryName(destFilePath); diff --git a/src/EggEncoder/ServiceCollectionExtensions.cs b/src/EggEncoder/ServiceCollectionExtensions.cs index 5218e6a..28879a2 100644 --- a/src/EggEncoder/ServiceCollectionExtensions.cs +++ b/src/EggEncoder/ServiceCollectionExtensions.cs @@ -1,12 +1,14 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace EggEncoder { public static class ServiceCollectionExtensions { - public static IServiceCollection AddEggEncoder(this IServiceCollection services) + public static IServiceCollection AddEggEncoder(this IServiceCollection services, bool enableLogging = true) { - services.AddScoped(); + services.AddScoped(serviceProvider => + new NativeEncoder(serviceProvider.GetRequiredService>(), enableLogging)); return services; }