| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 | using System;using System.Collections.Generic;using System.Diagnostics;using System.Globalization;using System.IO;using System.Linq;using System.Text.RegularExpressions;using System.Threading;using System.Threading.Tasks;using Jellyfin.Data.Enums;using Jellyfin.Extensions;using MediaBrowser.Common.Configuration;using MediaBrowser.Controller.Entities;using MediaBrowser.Controller.Entities.Audio;using MediaBrowser.Controller.Library;using MediaBrowser.Controller.MediaEncoding;using MediaBrowser.Controller.Persistence;using MediaBrowser.Model.Globalization;using MediaBrowser.Model.Tasks;using Microsoft.Extensions.Logging;namespace Emby.Server.Implementations.ScheduledTasks.Tasks;/// <summary>/// The audio normalization task./// </summary>public partial class AudioNormalizationTask : IScheduledTask{    private readonly IItemRepository _itemRepository;    private readonly ILibraryManager _libraryManager;    private readonly IMediaEncoder _mediaEncoder;    private readonly IConfigurationManager _configurationManager;    private readonly ILocalizationManager _localization;    private readonly ILogger<AudioNormalizationTask> _logger;    /// <summary>    /// Initializes a new instance of the <see cref="AudioNormalizationTask"/> class.    /// </summary>    /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param>    /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>    /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>    /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>    /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param>    /// <param name="logger">Instance of the <see cref="ILogger{AudioNormalizationTask}"/> interface.</param>    public AudioNormalizationTask(        IItemRepository itemRepository,        ILibraryManager libraryManager,        IMediaEncoder mediaEncoder,        IConfigurationManager configurationManager,        ILocalizationManager localizationManager,        ILogger<AudioNormalizationTask> logger)    {        _itemRepository = itemRepository;        _libraryManager = libraryManager;        _mediaEncoder = mediaEncoder;        _configurationManager = configurationManager;        _localization = localizationManager;        _logger = logger;    }    /// <inheritdoc />    public string Name => _localization.GetLocalizedString("TaskAudioNormalization");    /// <inheritdoc />    public string Description => _localization.GetLocalizedString("TaskAudioNormalizationDescription");    /// <inheritdoc />    public string Category => _localization.GetLocalizedString("TasksLibraryCategory");    /// <inheritdoc />    public string Key => "AudioNormalization";    [GeneratedRegex(@"^\s+I:\s+(.*?)\s+LUFS")]    private static partial Regex LUFSRegex();    /// <inheritdoc />    public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)    {        foreach (var library in _libraryManager.RootFolder.Children)        {            var libraryOptions = _libraryManager.GetLibraryOptions(library);            if (!libraryOptions.EnableLUFSScan)            {                continue;            }            // Album gain            var albums = _libraryManager.GetItemList(new InternalItemsQuery            {                IncludeItemTypes = [BaseItemKind.MusicAlbum],                Parent = library,                Recursive = true            });            foreach (var a in albums)            {                if (a.NormalizationGain.HasValue || a.LUFS.HasValue)                {                    continue;                }                // Skip albums that don't have multiple tracks, album gain is useless here                var albumTracks = ((MusicAlbum)a).Tracks.Where(x => x.IsFileProtocol).ToList();                if (albumTracks.Count <= 1)                {                    continue;                }                var tempFile = Path.Join(_configurationManager.GetTranscodePath(), Guid.NewGuid() + ".concat");                var inputLines = albumTracks.Select(x => string.Format(CultureInfo.InvariantCulture, "file '{0}'", x.Path.Replace("'", @"'\''", StringComparison.Ordinal)));                await File.WriteAllLinesAsync(tempFile, inputLines, cancellationToken).ConfigureAwait(false);                a.LUFS = await CalculateLUFSAsync(                    string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),                    cancellationToken).ConfigureAwait(false);                File.Delete(tempFile);            }            _itemRepository.SaveItems(albums, cancellationToken);            // Track gain            var tracks = _libraryManager.GetItemList(new InternalItemsQuery            {                MediaTypes = [MediaType.Audio],                IncludeItemTypes = [BaseItemKind.Audio],                Parent = library,                Recursive = true            });            foreach (var t in tracks)            {                if (t.NormalizationGain.HasValue || t.LUFS.HasValue || !t.IsFileProtocol)                {                    continue;                }                t.LUFS = await CalculateLUFSAsync(string.Format(CultureInfo.InvariantCulture, "-i \"{0}\"", t.Path.Replace("\"", "\\\"", StringComparison.Ordinal)), cancellationToken);            }            _itemRepository.SaveItems(tracks, cancellationToken);        }    }    /// <inheritdoc />    public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()    {        return        [            new TaskTriggerInfo            {                Type = TaskTriggerInfo.TriggerInterval,                IntervalTicks = TimeSpan.FromHours(24).Ticks            }        ];    }    private async Task<float?> CalculateLUFSAsync(string inputArgs, CancellationToken cancellationToken)    {        var args = $"-hide_banner {inputArgs} -af ebur128=framelog=verbose -f null -";        using (var process = new Process()        {            StartInfo = new ProcessStartInfo            {                FileName = _mediaEncoder.EncoderPath,                Arguments = args,                RedirectStandardOutput = false,                RedirectStandardError = true            },        })        {            try            {                _logger.LogDebug("Starting ffmpeg with arguments: {Arguments}", args);                process.Start();            }            catch (Exception ex)            {                _logger.LogError(ex, "Error starting ffmpeg with arguments: {Arguments}", args);                return null;            }            using var reader = process.StandardError;            await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))            {                Match match = LUFSRegex().Match(line);                if (match.Success)                {                    return float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);                }            }            _logger.LogError("Failed to find LUFS value in output");            return null;        }    }}
 |