< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.ScheduledTasks.Tasks.AudioNormalizationTask
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs
Line coverage
72%
Covered lines: 8
Uncovered lines: 3
Coverable lines: 11
Total lines: 235
Line coverage: 72.7%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Name()100%11100%
get_Description()100%210%
get_Category()100%210%
get_Key()100%210%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Diagnostics;
 4using System.Globalization;
 5using System.IO;
 6using System.Linq;
 7using System.Text.RegularExpressions;
 8using System.Threading;
 9using System.Threading.Tasks;
 10using Jellyfin.Data.Enums;
 11using Jellyfin.Extensions;
 12using MediaBrowser.Common.Configuration;
 13using MediaBrowser.Controller.Entities;
 14using MediaBrowser.Controller.Entities.Audio;
 15using MediaBrowser.Controller.Library;
 16using MediaBrowser.Controller.MediaEncoding;
 17using MediaBrowser.Controller.Persistence;
 18using MediaBrowser.Model.Globalization;
 19using MediaBrowser.Model.Tasks;
 20using Microsoft.Extensions.Logging;
 21
 22namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
 23
 24/// <summary>
 25/// The audio normalization task.
 26/// </summary>
 27public partial class AudioNormalizationTask : IScheduledTask
 28{
 29    private readonly IItemRepository _itemRepository;
 30    private readonly ILibraryManager _libraryManager;
 31    private readonly IMediaEncoder _mediaEncoder;
 32    private readonly IApplicationPaths _applicationPaths;
 33    private readonly ILocalizationManager _localization;
 34    private readonly ILogger<AudioNormalizationTask> _logger;
 35
 36    /// <summary>
 37    /// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.
 38    /// </summary>
 39    /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>
 40    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 41    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 42    /// <param name="applicationPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param>
 43    /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>
 44    /// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>
 45    public AudioNormalizationTask(
 46        IItemRepository itemRepository,
 47        ILibraryManager libraryManager,
 48        IMediaEncoder mediaEncoder,
 49        IApplicationPaths applicationPaths,
 50        ILocalizationManager localizationManager,
 51        ILogger<AudioNormalizationTask> logger)
 52    {
 2153        _itemRepository = itemRepository;
 2154        _libraryManager = libraryManager;
 2155        _mediaEncoder = mediaEncoder;
 2156        _applicationPaths = applicationPaths;
 2157        _localization = localizationManager;
 2158        _logger = logger;
 2159    }
 60
 61    /// <inheritdoc />
 2162    public string Name => _localization.GetLocalizedString("TaskAudioNormalization");
 63
 64    /// <inheritdoc />
 065    public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");
 66
 67    /// <inheritdoc />
 068    public string Category => _localization.GetLocalizedString("TasksLibraryCategory");
 69
 70    /// <inheritdoc />
 071    public string Key => "AudioNormalization";
 72
 73    [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]
 74    private static partial Regex LUFSRegex();
 75
 76    /// <inheritdoc />
 77    public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
 78    {
 79        var numComplete = 0;
 80        var libraries = _libraryManager.RootFolder.Children.Where(library => _libraryManager.GetLibraryOptions(library).
 81        double percent = 0;
 82
 83        foreach (var library in libraries)
 84        {
 85            var albums = _libraryManager.GetItemList(new InternalItemsQuery { IncludeItemTypes = [BaseItemKind.MusicAlbu
 86
 87            double nextPercent = numComplete + 1;
 88            nextPercent /= libraries.Length;
 89            nextPercent -= percent;
 90            // Split the progress for this single library into two halves: album gain and track gain.
 91            // The first half will be for album gain, the second half for track gain.
 92            nextPercent /= 2;
 93            var albumComplete = 0;
 94
 95            foreach (var a in albums)
 96            {
 97                if (!a.NormalizationGain.HasValue && !a.LUFS.HasValue)
 98                {
 99                    // Album gain
 100                    var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();
 101
 102                    // Skip albums that don't have multiple tracks, album gain is useless here
 103                    if (albumTracks.Count > 1)
 104                    {
 105                        _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
 106                        var tempDir = _applicationPaths.TempDirectory;
 107                        Directory.CreateDirectory(tempDir);
 108                        var tempFile = Path.Join(tempDir, a.Id + ".concat");
 109                        var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'
 110                        await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);
 111                        try
 112                        {
 113                            a.LUFS = await CalculateLUFSAsync(
 114                                string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
 115                                OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleti
 116                                cancellationToken).ConfigureAwait(false);
 117                        }
 118                        finally
 119                        {
 120                            File.Delete(tempFile);
 121                        }
 122                    }
 123                }
 124
 125                // Update sub-progress for album gain
 126                albumComplete++;
 127                double albumPercent = albumComplete;
 128                albumPercent /= albums.Count;
 129
 130                progress.Report(100 * (percent + (albumPercent * nextPercent)));
 131            }
 132
 133            // Update progress to start at the track gain percent calculation
 134            percent += nextPercent;
 135
 136            _itemRepository.SaveItems(albums, cancellationToken);
 137
 138            // Track gain
 139            var tracks = _libraryManager.GetItemList(new InternalItemsQuery { MediaTypes = [MediaType.Audio], IncludeIte
 140
 141            var tracksComplete = 0;
 142            foreach (var t in tracks)
 143            {
 144                if (!t.NormalizationGain.HasValue && !t.LUFS.HasValue && t.IsFileProtocol)
 145                {
 146                    t.LUFS = await CalculateLUFSAsync(
 147                        string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringCom
 148                        false,
 149                        cancellationToken).ConfigureAwait(false);
 150                }
 151
 152                // Update sub-progress for track gain
 153                tracksComplete++;
 154                double trackPercent = tracksComplete;
 155                trackPercent /= tracks.Count;
 156
 157                progress.Report(100 * (percent + (trackPercent * nextPercent)));
 158            }
 159
 160            _itemRepository.SaveItems(tracks, cancellationToken);
 161
 162            // Update progress
 163            numComplete++;
 164            percent = numComplete;
 165            percent /= libraries.Length;
 166
 167            progress.Report(100 * percent);
 168        }
 169
 170        progress.Report(100.0);
 171    }
 172
 173    /// <inheritdoc />
 174    public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
 175    {
 176        yield return new TaskTriggerInfo
 177        {
 178            Type = TaskTriggerInfoType.IntervalTrigger,
 179            IntervalTicks = TimeSpan.FromHours(24).Ticks
 180        };
 181    }
 182
 183    private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, CancellationToken cancellationToke
 184    {
 185        var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";
 186
 187        using (var process = new Process()
 188        {
 189            StartInfo = new ProcessStartInfo
 190            {
 191                FileName = _mediaEncoder.EncoderPath,
 192                Arguments = args,
 193                RedirectStandardOutput = false,
 194                RedirectStandardError = true
 195            },
 196        })
 197        {
 198            try
 199            {
 200                _logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);
 201                process.Start();
 202                process.PriorityClass = ProcessPriorityClass.BelowNormal;
 203            }
 204            catch (Exception ex)
 205            {
 206                _logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);
 207                return null;
 208            }
 209
 210            using var reader = process.StandardError;
 211            float? lufs = null;
 212            await foreach (var line in reader.ReadAllLinesAsync(cancellationToken).ConfigureAwait(false))
 213            {
 214                Match match = LUFSRegex().Match(line);
 215                if (match.Success)
 216                {
 217                    lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
 218                    break;
 219                }
 220            }
 221
 222            if (lufs is null)
 223            {
 224                _logger.LogError("Failed to find LUFS value in output");
 225            }
 226
 227            if (waitForExit)
 228            {
 229                await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
 230            }
 231
 232            return lufs;
 233        }
 234    }
 235}