diff --git a/AudioQualityEnhancer.Tests/AudioInfoTests.cs b/AudioQualityEnhancer.Tests/AudioInfoTests.cs
new file mode 100644
index 0000000..54b4f51
--- /dev/null
+++ b/AudioQualityEnhancer.Tests/AudioInfoTests.cs
@@ -0,0 +1,54 @@
+using System.Globalization;
+using AudioQualityEnhancer.Models;
+using AudioQualityEnhancer.Services;
+
+namespace AudioQualityEnhancer.Tests;
+
+public sealed class AudioInfoTests
+{
+ ///
+ /// AudioInfo subscribes to the localization service in its constructor so its display
+ /// strings follow a language change. That service lives for the whole session, so an
+ /// instance that is never disposed stays reachable through it for just as long. This
+ /// pins the contract that makes disposal matter.
+ ///
+ [Fact]
+ public void Dispose_StopsListeningForLanguageChanges()
+ {
+ var info = new AudioInfo { Codec = "mp3" };
+ var notifications = 0;
+ var original = LocalizationService.Instance.Culture;
+
+ try
+ {
+ // Start from a known culture. Another test class may have left the process on
+ // English, and assigning the same culture again raises no change at all.
+ LocalizationService.Instance.Culture = CultureInfo.GetCultureInfo("de");
+ info.PropertyChanged += (_, _) => notifications++;
+
+ LocalizationService.Instance.Culture = CultureInfo.GetCultureInfo("en");
+ var whileSubscribed = notifications;
+ Assert.True(whileSubscribed > 0, "A language change has to reach a live instance.");
+
+ info.Dispose();
+ LocalizationService.Instance.Culture = CultureInfo.GetCultureInfo("de");
+
+ Assert.Equal(whileSubscribed, notifications);
+ }
+ finally
+ {
+ LocalizationService.Instance.Culture = original;
+ info.Dispose();
+ }
+ }
+
+ [Fact]
+ public void WithSelectedAudioStream_ReturnsTheSameInstanceWithoutStreams()
+ {
+ using var info = new AudioInfo { Codec = "mp3" };
+
+ // Returning this is what keeps BatchProcessingItem.SelectAudioStream from handing
+ // an instance to SetAudioInfo that would then be disposed and stored at once.
+ Assert.Same(info, info.WithSelectedAudioStream(null));
+ }
+}
diff --git a/Services/AudioProcessingService.cs b/Services/AudioProcessingService.cs
index a3baf55..21f7475 100644
--- a/Services/AudioProcessingService.cs
+++ b/Services/AudioProcessingService.cs
@@ -85,18 +85,32 @@ public async Task> ProcessAsync(
_fileNameService.CleanupTemporaryOutputFiles(options.OutputDirectory, TimeSpan.FromDays(2));
var sourceInfo = options.SourceInfo;
- if (sourceInfo is null)
+ if (sourceInfo is not null)
{
- Report(progress, 8, LocalizationService.Instance["Phase_AnalyzingSource"]);
- var analysis = await _ffprobeService.AnalyzeAsync(options.InputPath, _logService.Info, cancellationToken);
- if (analysis.IsFailure || analysis.Value is null)
- {
- return Result.Failure(analysis.ErrorMessage ?? LocalizationService.Instance["Error_SourceAnalysisFailed"], analysis.Exception);
- }
+ // Supplied by the caller, so it belongs to the queue item and is disposed with it.
+ return await RenderAsync(options, sourceInfo, progress, cancellationToken);
+ }
- sourceInfo = analysis.Value;
+ Report(progress, 8, LocalizationService.Instance["Phase_AnalyzingSource"]);
+ var analysis = await _ffprobeService.AnalyzeAsync(options.InputPath, _logService.Info, cancellationToken);
+ if (analysis.IsFailure || analysis.Value is null)
+ {
+ return Result.Failure(analysis.ErrorMessage ?? LocalizationService.Instance["Error_SourceAnalysisFailed"], analysis.Exception);
}
+ // Created here, so disposed here. AudioInfo subscribes to the localization
+ // service in its constructor, and that service outlives the run, so an
+ // undisposed instance would stay reachable for the rest of the session.
+ using var ownedSourceInfo = analysis.Value;
+ return await RenderAsync(options, ownedSourceInfo, progress, cancellationToken);
+ }
+
+ private async Task> RenderAsync(
+ ProcessingOptions options,
+ AudioInfo sourceInfo,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
var diskSpaceCheck = EnsureSufficientDiskSpace(options.OutputDirectory, EstimateOutputSizeBytes(options, sourceInfo));
if (diskSpaceCheck.IsFailure)
{