mirror of
				https://github.com/jellyfin/jellyfin.git
				synced 2025-10-30 18:22:48 -04:00 
			
		
		
		
	Wait for ffmpeg to exit on Windows before we try deleting the concat file Original-merge: 346f36bc09eb6989d7cd6439175e46b699162cbb Merged-by: Bond-009 <bond.009@outlook.com> Backported-by: Bond_009 <bond.009@outlook.com>
		
			
				
	
	
		
			221 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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 IApplicationPaths _applicationPaths;
 | |
|     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="applicationPaths">Instance of the <see cref="IApplicationPaths"/> 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,
 | |
|         IApplicationPaths applicationPaths,
 | |
|         ILocalizationManager localizationManager,
 | |
|         ILogger<AudioNormalizationTask> logger)
 | |
|     {
 | |
|         _itemRepository = itemRepository;
 | |
|         _libraryManager = libraryManager;
 | |
|         _mediaEncoder = mediaEncoder;
 | |
|         _applicationPaths = applicationPaths;
 | |
|         _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;
 | |
|                 }
 | |
| 
 | |
|                 _logger.LogInformation("Calculating LUFS for album: {Album} with id: {Id}", a.Name, a.Id);
 | |
|                 var tempDir = _applicationPaths.TempDirectory;
 | |
|                 Directory.CreateDirectory(tempDir);
 | |
|                 var tempFile = Path.Join(tempDir, a.Id + ".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);
 | |
|                 try
 | |
|                 {
 | |
|                     a.LUFS = await CalculateLUFSAsync(
 | |
|                         string.Format(CultureInfo.InvariantCulture, "-f concat -safe 0 -i \"{0}\"", tempFile),
 | |
|                         OperatingSystem.IsWindows(), // Wait for process to exit on Windows before we try deleting the concat file
 | |
|                         cancellationToken).ConfigureAwait(false);
 | |
|                 }
 | |
|                 finally
 | |
|                 {
 | |
|                     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)),
 | |
|                     false,
 | |
|                     cancellationToken).ConfigureAwait(false);
 | |
|             }
 | |
| 
 | |
|             _itemRepository.SaveItems(tracks, cancellationToken);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /// <inheritdoc />
 | |
|     public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
 | |
|     {
 | |
|         return
 | |
|         [
 | |
|             new TaskTriggerInfo
 | |
|             {
 | |
|                 Type = TaskTriggerInfoType.IntervalTrigger,
 | |
|                 IntervalTicks = TimeSpan.FromHours(24).Ticks
 | |
|             }
 | |
|         ];
 | |
|     }
 | |
| 
 | |
|     private async Task<float?> CalculateLUFSAsync(string inputArgs, bool waitForExit, 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;
 | |
|             float? lufs = null;
 | |
|             await foreach (var line in reader.ReadAllLinesAsync(cancellationToken))
 | |
|             {
 | |
|                 Match match = LUFSRegex().Match(line);
 | |
|                 if (match.Success)
 | |
|                 {
 | |
|                     lufs = float.Parse(match.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
 | |
|                     break;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (lufs is null)
 | |
|             {
 | |
|                 _logger.LogError("Failed to find LUFS value in output");
 | |
|             }
 | |
| 
 | |
|             if (waitForExit)
 | |
|             {
 | |
|                 await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
 | |
|             }
 | |
| 
 | |
|             return lufs;
 | |
|         }
 | |
|     }
 | |
| }
 |