diff options
Diffstat (limited to 'Emby.Server.Implementations/ScheduledTasks/Tasks')
5 files changed, 219 insertions, 97 deletions
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs new file mode 100644 index 000000000..301c04915 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/AudioNormalizationTask.cs @@ -0,0 +1,206 @@ +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), + 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)), 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; + } + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index d03d40863..36456504b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index 812df8192..804097219 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -116,7 +116,7 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask foreach (var linkedChild in folder.LinkedChildren) { var path = linkedChild.Path; - if (!File.Exists(path)) + if (!File.Exists(path) && !Directory.Exists(path)) { _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path); (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild); @@ -127,15 +127,8 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask { _logger.LogDebug("Updating {FolderName}", folder.Name); folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray(); + _providerManager.SaveMetadataAsync(folder, ItemUpdateType.MetadataEdit); folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken); - - _providerManager.QueueRefresh( - folder.Id, - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) - { - ForceSave = true - }, - RefreshPriority.High); } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 03935b384..fc3ad90f6 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; @@ -133,53 +134,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks cancellationToken.ThrowIfCancellationRequested(); - DeleteFile(file.FullName); + FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); index++; } - DeleteEmptyFolders(directory); + FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); progress.Report(100); } - - private void DeleteEmptyFolders(string parent) - { - foreach (var directory in _fileSystem.GetDirectoryPaths(parent)) - { - DeleteEmptyFolders(directory); - if (!_fileSystem.GetFileSystemEntryPaths(directory).Any()) - { - try - { - Directory.Delete(directory, false); - } - catch (UnauthorizedAccessException ex) - { - _logger.LogError(ex, "Error deleting directory {Path}", directory); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting directory {Path}", directory); - } - } - } - } - - private void DeleteFile(string path) - { - try - { - _fileSystem.DeleteFile(path); - } - catch (UnauthorizedAccessException ex) - { - _logger.LogError(ex, "Error deleting file {Path}", path); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {Path}", path); - } - } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index e4e565c64..254500ccd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -1,10 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; +using MediaBrowser.Controller.IO; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; @@ -62,16 +62,17 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <inheritdoc /> public bool IsLogged => true; - /// <summary> - /// Creates the triggers that define when the task will run. - /// </summary> - /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + /// <inheritdoc /> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { return new[] { new TaskTriggerInfo { + Type = TaskTriggerInfo.TriggerStartup + }, + new TaskTriggerInfo + { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } @@ -113,53 +114,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks cancellationToken.ThrowIfCancellationRequested(); - DeleteFile(file.FullName); + FileSystemHelper.DeleteFile(_fileSystem, file.FullName, _logger); index++; } - DeleteEmptyFolders(directory); + FileSystemHelper.DeleteEmptyFolders(_fileSystem, directory, _logger); progress.Report(100); } - - private void DeleteEmptyFolders(string parent) - { - foreach (var directory in _fileSystem.GetDirectoryPaths(parent)) - { - DeleteEmptyFolders(directory); - if (!_fileSystem.GetFileSystemEntryPaths(directory).Any()) - { - try - { - Directory.Delete(directory, false); - } - catch (UnauthorizedAccessException ex) - { - _logger.LogError(ex, "Error deleting directory {Path}", directory); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting directory {Path}", directory); - } - } - } - } - - private void DeleteFile(string path) - { - try - { - _fileSystem.DeleteFile(path); - } - catch (UnauthorizedAccessException ex) - { - _logger.LogError(ex, "Error deleting file {Path}", path); - } - catch (IOException ex) - { - _logger.LogError(ex, "Error deleting file {Path}", path); - } - } } } |
