commit
93467b4402
@ -0,0 +1,133 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"AppDeviceValues": "App: {0}, Apparat: {1}",
|
||||
"Application": "Applikazzjoni",
|
||||
"Artists": "Artisti",
|
||||
"AuthenticationSucceededWithUserName": "{1} awtentikat b'suċċess",
|
||||
"Books": "Kotba",
|
||||
"CameraImageUploadedFrom": "Ttellgħet immaġni ġdida tal-kamera minn {1}",
|
||||
"Channels": "Kanali",
|
||||
"ChapterNameValue": "Kapitlu {0}",
|
||||
"Collections": "Kollezzjonijiet",
|
||||
"DeviceOfflineWithName": "{0} inqatgħa",
|
||||
"DeviceOnlineWithName": "{0} qabad",
|
||||
"External": "Estern",
|
||||
"FailedLoginAttemptWithUserName": "Tentattiv t'aċċess fallut minn {0}",
|
||||
"Favorites": "Favoriti",
|
||||
"Forced": "Sfurzat",
|
||||
"Genres": "Ġeneri",
|
||||
"HeaderAlbumArtists": "Artisti tal-album",
|
||||
"HeaderContinueWatching": "Kompli Segwi",
|
||||
"HeaderFavoriteAlbums": "Albums Favoriti",
|
||||
"HeaderFavoriteArtists": "Artisti Favoriti",
|
||||
"HeaderFavoriteEpisodes": "Episodji Favoriti",
|
||||
"HeaderFavoriteShows": "Programmi Favoriti",
|
||||
"HeaderFavoriteSongs": "Kanzunetti Favoriti",
|
||||
"HeaderNextUp": "Li Jmiss",
|
||||
"SubtitleDownloadFailureFromForItem": "Is-sottotitli naqsu milli jitniżżlu minn {0} għal {1}",
|
||||
"UserPasswordChangedWithName": "Il-password inbidel għall-utent {0}",
|
||||
"TaskUpdatePluginsDescription": "Iniżżel u jinstalla aġġornamenti għal plugins li huma kkonfigurati biex jaġġornaw awtomatikament.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Ifittex fuq l-internet għal sottotitli neqsin abbażi tal-konfigurazzjoni tal-metadata.",
|
||||
"TaskOptimizeDatabaseDescription": "Jikkompatti d-database u jaqta' l-ispazju ħieles. It-tħaddim ta' dan il-kompitu wara li tiskennja l-librerija jew tagħmel bidliet oħra li jimplikaw modifiki fid-database jistgħu jtejbu l-prestazzjoni.",
|
||||
"Default": "Standard",
|
||||
"Folders": "Folders",
|
||||
"HeaderLiveTV": "TV Dirett",
|
||||
"HeaderRecordingGroups": "Gruppi ta' Reġistrazzjoni",
|
||||
"HearingImpaired": "Nuqqas ta' Smigħ",
|
||||
"HomeVideos": "Vidjows Personali",
|
||||
"Inherit": "Jiret",
|
||||
"ItemAddedWithName": "{0} ġie miżjud mal-librerija",
|
||||
"ItemRemovedWithName": "{0} tneħħa mil-librerija",
|
||||
"LabelIpAddressValue": "Indirizz IP: {0}",
|
||||
"Latest": "Tal-Aħħar",
|
||||
"MessageApplicationUpdated": "Jellyfin Server ġie aġġornat",
|
||||
"MessageApplicationUpdatedTo": "JellyFin Server ġie aġġornat għal {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Is-sezzjoni {0} tal-konfigurazzjoni tas-server ġiet aġġornata",
|
||||
"MessageServerConfigurationUpdated": "Il-konfigurazzjoni tas-server ġiet aġġornata",
|
||||
"MixedContent": "Kontenut imħallat",
|
||||
"Movies": "Films",
|
||||
"Music": "Mużika",
|
||||
"MusicVideos": "Vidjows tal-Mużika",
|
||||
"NameInstallFailed": "L-installazzjoni ta' {0} falliet",
|
||||
"NameSeasonNumber": "Staġun {0}",
|
||||
"NameSeasonUnknown": "Staġun Mhux Magħruf",
|
||||
"NewVersionIsAvailable": "Verżjoni ġdida ta' Jellyfin Server hija disponibbli biex titniżżel.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Aġġornament tal-applikazzjoni disponibbli",
|
||||
"NotificationOptionCameraImageUploaded": "Immaġini tal-kamera mtella'",
|
||||
"LabelRunningTimeValue": "Tul: {0}",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Aġġornament tal-applikazzjoni ġie installat",
|
||||
"NotificationOptionAudioPlayback": "Il-playback tal-awdjo beda",
|
||||
"NotificationOptionAudioPlaybackStopped": "Il-playback tal-awdjo twaqqaf",
|
||||
"NotificationOptionInstallationFailed": "Installazzjoni falliet",
|
||||
"NotificationOptionNewLibraryContent": "Kontenut ġdid miżjud",
|
||||
"NotificationOptionPluginError": "Ħsara fil-plugin",
|
||||
"NotificationOptionPluginInstalled": "Plugin installat",
|
||||
"NotificationOptionPluginUninstalled": "Plugin tneħħa",
|
||||
"NotificationOptionServerRestartRequired": "Meħtieġ l-istartjar mill-ġdid tas-server",
|
||||
"NotificationOptionTaskFailed": "Falliment tal-kompitu skedat",
|
||||
"NotificationOptionUserLockedOut": "Utent imsakkar",
|
||||
"Photos": "Ritratti",
|
||||
"Playlists": "Playlists",
|
||||
"Plugin": "Plugin",
|
||||
"PluginInstalledWithName": "{0} ġie installat",
|
||||
"PluginUninstalledWithName": "{0} ġie mneħħi",
|
||||
"PluginUpdatedWithName": "{0} ġie aġġornat",
|
||||
"ProviderValue": "Fornitur: {0}",
|
||||
"ScheduledTaskFailedWithName": "{0} falla",
|
||||
"ScheduledTaskStartedWithName": "{0} beda",
|
||||
"ServerNameNeedsToBeRestarted": "{0} jeħtieġ li jerġa' jinbeda",
|
||||
"Songs": "Kanzunetti",
|
||||
"StartupEmbyServerIsLoading": "Jellyfin Server qed jixgħel. Jekk jogħġbok erġa' pprova dalwaqt.",
|
||||
"Sync": "Sinkronizza",
|
||||
"System": "Sistema",
|
||||
"Undefined": "Mhux Definit",
|
||||
"User": "Utent",
|
||||
"UserCreatedWithName": "L-utent {0} inħoloq",
|
||||
"UserDeletedWithName": "L-utent {0} tħassar",
|
||||
"UserDownloadingItemWithValues": "{0} qed iniżżel {1}",
|
||||
"UserLockedOutWithName": "L-utent {0} ġie msakkar",
|
||||
"UserOfflineFromDevice": "{0} skonnettja minn {1}",
|
||||
"UserOnlineFromDevice": "{0} huwa online minn {1}",
|
||||
"NotificationOptionPluginUpdateInstalled": "Aġġornament ta' plugin ġie installat",
|
||||
"NotificationOptionVideoPlayback": "Il-playback tal-vidjow beda",
|
||||
"NotificationOptionVideoPlaybackStopped": "Il-playback tal-vidjow waqaf",
|
||||
"Shows": "Programmi",
|
||||
"TvShows": "Programmi tat-TV",
|
||||
"UserPolicyUpdatedWithName": "Il-policy tal-utent ġiet aġġornata għal {0}",
|
||||
"UserStartedPlayingItemWithValues": "{0} qed iħaddem {1} fuq {2}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} waqaf iħaddem {1} fuq {2}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ġie miżjud mal-librerija tal-midja tiegħek",
|
||||
"ValueSpecialEpisodeName": "Speċjali - {0}",
|
||||
"VersionNumber": "Verżjoni {0}",
|
||||
"TasksMaintenanceCategory": "Manutenzjoni",
|
||||
"TasksLibraryCategory": "Librerija",
|
||||
"TasksApplicationCategory": "Applikazzjoni",
|
||||
"TasksChannelsCategory": "Kanali tal-Internet",
|
||||
"TaskCleanActivityLog": "Naddaf il-Logg tal-Attività",
|
||||
"TaskCleanActivityLogDescription": "Iħassar l-entrati tar-reġistru tal-attività eqdem mill-età kkonfigurata.",
|
||||
"TaskCleanCache": "Naddaf id-Direttorju tal-Cache",
|
||||
"TaskCleanCacheDescription": "Iħassar il-fajls tal-cache li m'għadhomx meħtieġa mis-sistema.",
|
||||
"TaskRefreshChapterImages": "Oħroġ l-Immaġini tal-Kapitolu",
|
||||
"TaskRefreshChapterImagesDescription": "Joħloq thumbnails għal vidjows li għandhom kapitli.",
|
||||
"TaskAudioNormalization": "Normalizzazzjoni Awdjo",
|
||||
"TaskAudioNormalizationDescription": "Skennja fajls għal data ta' normalizzazzjoni awdjo.",
|
||||
"TaskRefreshLibrary": "Skennja l-Librerija tal-Midja",
|
||||
"TaskRefreshLibraryDescription": "Jiskennja l-librerija tal-midja tiegħek għal fajls ġodda u jġedded il-metadejta.",
|
||||
"TaskCleanLogs": "Naddaf id-Direttorju tal-Logg",
|
||||
"TaskCleanLogsDescription": "Iħassar fajls tal-logg eqdem minn {0} ijiem.",
|
||||
"TaskRefreshPeople": "Aġġorna Persuni",
|
||||
"TaskRefreshPeopleDescription": "Jaġġorna l-metadejta għall-atturi u d-diretturi fil-librerija tal-midja tiegħek.",
|
||||
"TaskRefreshTrickplayImages": "Iġġenera Stampi Trickplay",
|
||||
"TaskRefreshTrickplayImagesDescription": "Joħloq previews trickplay għal vidjows fil-libreriji attivati.",
|
||||
"TaskUpdatePlugins": "Aġġorna il-Plugins",
|
||||
"TaskCleanTranscode": "Naddaf id-Direttorju tat-Transcode",
|
||||
"TaskCleanTranscodeDescription": "Iħassar fajls transcode eqdem minn ġurnata.",
|
||||
"TaskRefreshChannels": "Aġġorna l-Kanali",
|
||||
"TaskRefreshChannelsDescription": "Aġġorna l-informazzjoni tal-kanali tal-internet.",
|
||||
"TaskDownloadMissingSubtitles": "Niżżel is-sottotitli nieqsa",
|
||||
"TaskOptimizeDatabase": "Ottimizza d-database",
|
||||
"TaskKeyframeExtractor": "Estrattur ta' Keyframes",
|
||||
"TaskKeyframeExtractorDescription": "Jiġbed il-keyframes mill-fajls tal-vidjow biex joħloq playlists HLS aktar preċiżi. Dan il-kompitu jista' jdum għal żmien twil.",
|
||||
"TaskCleanCollectionsAndPlaylists": "Naddaf il-kollezzjonijiet u l-playlists",
|
||||
"TaskCleanCollectionsAndPlaylistsDescription": "Ineħħi oġġetti minn kollezzjonijiet u playlists li m'għadhomx jeżistu."
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
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 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(@"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;
|
||||
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
MatchCollection split = LUFSRegex().Matches(output);
|
||||
|
||||
if (split.Count != 0)
|
||||
{
|
||||
return float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
|
||||
}
|
||||
|
||||
_logger.LogError("Failed to find LUFS value in output:\n{Output}", output);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue