Merge branch 'master' into fix-CA1819-LibraryOptions

Fix Conflicts:
	Emby.Server.Implementations/Library/LibraryManager.cs
	Jellyfin.Api/Controllers/LibraryStructureController.cs
pull/11473/head
del 2 weeks ago
commit 8c4419aba5

@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@8f596b4ae3cb3c588a5c46780b86dd53fef16c52 # v3.25.2
uses: github/codeql-action/init@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@8f596b4ae3cb3c588a5c46780b86dd53fef16c52 # v3.25.2
uses: github/codeql-action/autobuild@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@8f596b4ae3cb3c588a5c46780b86dd53fef16c52 # v3.25.2
uses: github/codeql-action/analyze@d39d31e687223d841ef683f52467bd88e9b21c14 # v3.25.3

@ -127,15 +127,11 @@ namespace Emby.Server.Implementations.AppBase
if (_configurationFactories is null)
{
_configurationFactories = new[] { factory };
_configurationFactories = [factory];
}
else
{
var oldLen = _configurationFactories.Length;
var arr = new IConfigurationFactory[oldLen + 1];
_configurationFactories.CopyTo(arr, 0);
arr[oldLen] = factory;
_configurationFactories = arr;
_configurationFactories = [.._configurationFactories, factory];
}
_configurationStores = _configurationFactories

@ -49,8 +49,8 @@ namespace Emby.Server.Implementations.Data
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,LUFS,NormalizationGain,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@LUFS,@NormalizationGain,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
private readonly IServerConfigurationManager _config;
private readonly IServerApplicationHost _appHost;
@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Data
"DateLastMediaAdded",
"Album",
"LUFS",
"NormalizationGain",
"CriticRating",
"IsVirtualItem",
"SeriesName",
@ -478,6 +479,7 @@ namespace Emby.Server.Implementations.Data
AddColumn(connection, "TypedBaseItems", "DateLastMediaAdded", "DATETIME", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "Album", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "LUFS", "Float", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "NormalizationGain", "Float", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "IsVirtualItem", "BIT", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "SeriesName", "Text", existingColumnNames);
AddColumn(connection, "TypedBaseItems", "UserDataKey", "Text", existingColumnNames);
@ -886,6 +888,7 @@ namespace Emby.Server.Implementations.Data
saveItemStatement.TryBind("@Album", item.Album);
saveItemStatement.TryBind("@LUFS", item.LUFS);
saveItemStatement.TryBind("@NormalizationGain", item.NormalizationGain);
saveItemStatement.TryBind("@IsVirtualItem", item.IsVirtualItem);
if (item is IHasSeries hasSeriesName)
@ -1672,6 +1675,11 @@ namespace Emby.Server.Implementations.Data
item.LUFS = lUFS;
}
if (reader.TryGetSingle(index++, out var normalizationGain))
{
item.NormalizationGain = normalizationGain;
}
if (reader.TryGetSingle(index++, out var criticRating))
{
item.CriticRating = criticRating;
@ -2315,14 +2323,7 @@ namespace Emby.Server.Implementations.Data
columns.Add(builder.ToString());
var oldLen = query.ExcludeItemIds.Length;
var newLen = oldLen + item.ExtraIds.Length + 1;
var excludeIds = new Guid[newLen];
query.ExcludeItemIds.CopyTo(excludeIds, 0);
excludeIds[oldLen] = item.Id;
item.ExtraIds.CopyTo(excludeIds, oldLen + 1);
query.ExcludeItemIds = excludeIds;
query.ExcludeItemIds = [..query.ExcludeItemIds, item.Id, ..item.ExtraIds];
query.ExcludeProviderIds = item.ProviderIds;
}
@ -2830,10 +2831,7 @@ namespace Emby.Server.Implementations.Data
prepend.Add((ItemSortBy.Random, SortOrder.Ascending));
}
var arr = new (ItemSortBy, SortOrder)[prepend.Count + orderBy.Count];
prepend.CopyTo(arr, 0);
orderBy.CopyTo(arr, prepend.Count);
orderBy = query.OrderBy = arr;
orderBy = query.OrderBy = [..prepend, ..orderBy];
}
else if (orderBy.Count == 0)
{
@ -4194,7 +4192,19 @@ namespace Emby.Server.Implementations.Data
{
int index = 0;
string includedTags = string.Join(',', query.IncludeInheritedTags.Select(_ => paramName + index++));
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
// Episodes do not store inherit tags from their parents in the database, and the tag may be still required by the client.
// In addtion to the tags for the episodes themselves, we need to manually query its parent (the season)'s tags as well.
if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Episode)
{
whereClauses.Add($"""
((select CleanValue from ItemValues where ItemId=Guid and Type=6 and CleanValue in ({includedTags})) is not null
OR (select CleanValue from ItemValues where ItemId=ParentId and Type=6 and CleanValue in ({includedTags})) is not null)
""");
}
else
{
whereClauses.Add("((select CleanValue from ItemValues where ItemId=Guid and Type=6 and cleanvalue in (" + includedTags + ")) is not null)");
}
}
else
{

@ -898,7 +898,15 @@ namespace Emby.Server.Implementations.Dto
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
}
dto.LUFS = item.LUFS;
if (item.LUFS.HasValue)
{
// -18 LUFS reference, same as ReplayGain 2.0, compatible with ReplayGain 1.0
dto.NormalizationGain = -18f - item.LUFS;
}
else if (item.NormalizationGain.HasValue)
{
dto.NormalizationGain = item.NormalizationGain;
}
// Add audio info
if (item is Audio audio)

@ -103,7 +103,7 @@ namespace Emby.Server.Implementations.Library
}
};
private static readonly Glob[] _globs = _patterns.Select(p => Glob.Parse(p, _globOptions)).ToArray();
private static readonly Glob[] _globs = Array.ConvertAll(_patterns, p => Glob.Parse(p, _globOptions));
/// <summary>
/// Returns true if the supplied path should be ignored.

@ -3059,8 +3059,7 @@ namespace Emby.Server.Implementations.Library
SyncLibraryOptionsToLocations(virtualFolderPath, libraryOptions);
var list = libraryOptions.PathInfos.ToList();
foreach (var originalPathInfo in list)
foreach (var originalPathInfo in libraryOptions.PathInfos)
{
if (string.Equals(mediaPath.Path, originalPathInfo.Path, StringComparison.Ordinal))
{

@ -13,7 +13,6 @@ using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Resolvers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;

@ -303,8 +303,8 @@ namespace Emby.Server.Implementations.Library
{
// Handle situations with the grouping setting, e.g. movies showing up in tv, etc.
// Thanks to mixed content libraries included in the UserView
var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0)
var hasCollectionType = parents.OfType<UserView>().ToList();
if (hasCollectionType.Count > 0)
{
if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{

@ -126,5 +126,7 @@
"External": "خارجي",
"HearingImpaired": "ضعاف السمع",
"TaskRefreshTrickplayImages": "توليد صور Trickplay",
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة."
"TaskRefreshTrickplayImagesDescription": "يُنشئ معاينات Trickplay لمقاطع الفيديو في المكتبات المُمكّنة.",
"TaskCleanCollectionsAndPlaylists": "حذف المجموعات وقوائم التشغيل",
"TaskCleanCollectionsAndPlaylistsDescription": "حذف عناصر من المجموعات وقوائم التشغيل التي لم تعد موجودة."
}

@ -106,6 +106,8 @@
"TaskCleanCacheDescription": "Deletes cache files no longer needed by the system.",
"TaskRefreshChapterImages": "Extract Chapter Images",
"TaskRefreshChapterImagesDescription": "Creates thumbnails for videos that have chapters.",
"TaskAudioNormalization": "Audio Normalization",
"TaskAudioNormalizationDescription": "Scans files for audio normalization data.",
"TaskRefreshLibrary": "Scan Media Library",
"TaskRefreshLibraryDescription": "Scans your media library for new files and refreshes metadata.",
"TaskCleanLogs": "Clean Log Directory",

@ -126,5 +126,7 @@
"External": "Externe",
"HearingImpaired": "Malentendants",
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées."
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprimer les liens inexistants des collections et des listes de lecture"
}

@ -128,5 +128,7 @@
"TaskRefreshTrickplayImages": "Générer des images Trickplay",
"TaskRefreshTrickplayImagesDescription": "Crée des aperçus Trickplay pour les vidéos dans les médiathèques activées.",
"TaskCleanCollectionsAndPlaylists": "Nettoyer les collections et les listes de lecture",
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus."
"TaskCleanCollectionsAndPlaylistsDescription": "Supprime les éléments des collections et des listes de lecture qui n'existent plus.",
"TaskAudioNormalization": "Normalisation audio",
"TaskAudioNormalizationDescription": "Analyse les fichiers à la recherche de données de normalisation audio."
}

@ -226,13 +226,8 @@ namespace Emby.Server.Implementations.Playlists
return;
}
// Create a new array with the updated playlist items
var newLinkedChildren = new LinkedChild[playlist.LinkedChildren.Length + childrenToAdd.Count];
playlist.LinkedChildren.CopyTo(newLinkedChildren, 0);
childrenToAdd.CopyTo(newLinkedChildren, playlist.LinkedChildren.Length);
// Update the playlist in the repository
playlist.LinkedChildren = newLinkedChildren;
playlist.LinkedChildren = [..playlist.LinkedChildren, ..childrenToAdd];
await UpdatePlaylistInternal(playlist).ConfigureAwait(false);
@ -526,8 +521,8 @@ namespace Emby.Server.Implementations.Playlists
foreach (var playlist in playlists)
{
// Update owner if shared
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToArray();
if (rankedShares.Length > 0)
var rankedShares = playlist.Shares.OrderByDescending(x => x.CanEdit).ToList();
if (rankedShares.Count > 0)
{
playlist.OwnerUserId = rankedShares[0].UserId;
playlist.Shares = rankedShares.Skip(1).ToArray();

@ -256,8 +256,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
{
get
{
var triggers = InternalTriggers;
return triggers.Select(i => i.Item1).ToArray();
return Array.ConvertAll(InternalTriggers, i => i.Item1);
}
set
@ -269,7 +268,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
SaveTriggers(triggerList);
InternalTriggers = triggerList.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i)));
}
}
@ -503,7 +502,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
{
// This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
var settings = LoadTriggerSettings().Where(i => i is not null).ToArray();
var settings = LoadTriggerSettings().Where(i => i is not null);
return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
}

@ -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;
}
}
}

@ -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;

@ -400,7 +400,7 @@ namespace Emby.Server.Implementations.Session
{
session.NowPlayingQueue = nowPlayingQueue;
var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray();
var itemIds = Array.ConvertAll(nowPlayingQueue, queue => queue.Id);
session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos(
_libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }),
new DtoOptions(true));
@ -1386,16 +1386,13 @@ namespace Emby.Server.Implementations.Session
if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId)))
{
var user = _userManager.GetUserById(userId);
var list = session.AdditionalUsers.ToList();
list.Add(new SessionUserInfo
var newUser = new SessionUserInfo
{
UserId = userId,
UserName = user.Username
});
};
session.AdditionalUsers = list.ToArray();
session.AdditionalUsers = [..session.AdditionalUsers, newUser];
}
}

@ -5,6 +5,7 @@ using System.Linq;
using System.Net.Mime;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Models;
using MediaBrowser.Common.Api;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Plugins;
@ -45,9 +46,9 @@ public class DashboardController : BaseJellyfinApiController
/// <response code="404">Server still loading.</response>
/// <returns>An <see cref="IEnumerable{ConfigurationPageInfo}"/> with infos about the plugins.</returns>
[HttpGet("web/ConfigurationPages")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize]
public ActionResult<IEnumerable<ConfigurationPageInfo>> GetConfigurationPages(
[FromQuery] bool? enableInMainMenu)
{

@ -1481,7 +1481,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (currentTranscodingIndex.HasValue)
{
DeleteLastFile(playlistPath, segmentExtension, 0);
await DeleteLastFile(playlistPath, segmentExtension, 0).ConfigureAwait(false);
}
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
@ -2009,17 +2009,19 @@ public class DynamicHlsController : BaseJellyfinApiController
}
}
private void DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
private Task DeleteLastFile(string playlistPath, string segmentExtension, int retryCount)
{
var file = GetLastTranscodingFile(playlistPath, segmentExtension, _fileSystem);
if (file is not null)
if (file is null)
{
DeleteFile(file.FullName, retryCount);
return Task.CompletedTask;
}
return DeleteFile(file.FullName, retryCount);
}
private void DeleteFile(string path, int retryCount)
private async Task DeleteFile(string path, int retryCount)
{
if (retryCount >= 5)
{
@ -2036,9 +2038,8 @@ public class DynamicHlsController : BaseJellyfinApiController
{
_logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path);
var task = Task.Delay(100);
task.Wait();
DeleteFile(path, retryCount + 1);
await Task.Delay(100).ConfigureAwait(false);
await DeleteFile(path, retryCount + 1).ConfigureAwait(false);
}
catch (Exception ex)
{

@ -264,7 +264,7 @@ public class ItemUpdateController : BaseJellyfinApiController
if (request.Studios is not null)
{
item.Studios = request.Studios.Select(x => x.Name).ToArray();
item.Studios = Array.ConvertAll(request.Studios, x => x.Name);
}
if (request.DateCreated.HasValue)
@ -379,10 +379,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
hasAlbumArtists.AlbumArtists = request
.AlbumArtists
.Select(i => i.Name)
.ToArray();
hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
}
}
@ -390,10 +387,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
hasArtists.Artists = request
.ArtistItems
.Select(i => i.Name)
.ToArray();
hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
}
}

@ -256,6 +256,13 @@ public class ItemsController : BaseJellyfinApiController
return BadRequest("userId is required");
}
if (user is not null
&& user.GetPreference(PreferenceKind.AllowedTags).Length != 0
&& !fields.Contains(ItemFields.Tags))
{
fields = [..fields, ItemFields.Tags];
}
var dtoOptions = new DtoOptions { Fields = fields }
.AddClientFields(User)
.AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes);

@ -158,13 +158,13 @@ public class LibraryController : BaseJellyfinApiController
return NotFound();
}
IEnumerable<BaseItem> themeItems;
IReadOnlyList<BaseItem> themeItems;
while (true)
{
themeItems = item.GetThemeSongs();
if (themeItems.Any() || !inheritFromParent)
if (themeItems.Count > 0 || !inheritFromParent)
{
break;
}

@ -18,7 +18,7 @@ namespace Jellyfin.Api.Controllers;
/// Package Controller.
/// </summary>
[Route("")]
[Authorize]
[Authorize(Policy = Policies.RequiresElevation)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@ -90,7 +90,6 @@ public class PackageController : BaseJellyfinApiController
[HttpPost("Packages/Installed/{name}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
[FromRoute, Required] string name,
[FromQuery] Guid? assemblyGuid,
@ -128,7 +127,6 @@ public class PackageController : BaseJellyfinApiController
/// <response code="204">Installation cancelled.</response>
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
[HttpDelete("Packages/Installing/{packageId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CancelPackageInstallation(
[FromRoute, Required] Guid packageId)
@ -156,7 +154,6 @@ public class PackageController : BaseJellyfinApiController
/// <response code="204">Package repositories saved.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("Repositories")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRepositories([FromBody, Required] RepositoryInfo[] repositoryInfos)
{

@ -22,7 +22,7 @@ namespace Jellyfin.Api.Controllers;
/// <summary>
/// Plugins controller.
/// </summary>
[Authorize]
[Authorize(Policy = Policies.RequiresElevation)]
public class PluginsController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@ -66,7 +66,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Enable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult EnablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@ -90,7 +89,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpPost("{pluginId}/{version}/Disable")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult DisablePlugin([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@ -114,7 +112,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}/{version}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UninstallPluginByVersion([FromRoute, Required] Guid pluginId, [FromRoute, Required] Version version)
@ -137,7 +134,6 @@ public class PluginsController : BaseJellyfinApiController
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the plugin could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Obsolete("Please use the UninstallPluginByVersion API.")]

@ -85,16 +85,11 @@ public class UserViewsController : BaseJellyfinApiController
var folders = _userViewManager.GetUserViews(query);
var dtoOptions = new DtoOptions().AddClientFields(User);
var fields = dtoOptions.Fields.ToList();
fields.Add(ItemFields.PrimaryImageAspectRatio);
fields.Add(ItemFields.DisplayPreferencesId);
dtoOptions.Fields = fields.ToArray();
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId];
var user = _userManager.GetUserById(userId.Value);
var dtos = folders.Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user))
.ToArray();
var dtos = Array.ConvertAll(folders, i => _dtoService.GetBaseItemDto(i, dtoOptions, user));
return new QueryResult<BaseItemDto>(dtos);
}

@ -43,11 +43,7 @@ public static class DtoExtensions
client.Contains("media center", StringComparison.OrdinalIgnoreCase) ||
client.Contains("classic", StringComparison.OrdinalIgnoreCase))
{
int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1];
dtoOptions.Fields.CopyTo(arr, 0);
arr[oldLen] = ItemFields.RecursiveItemCount;
dtoOptions.Fields = arr;
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.RecursiveItemCount];
}
}
@ -61,11 +57,7 @@ public static class DtoExtensions
client.Contains("samsung", StringComparison.OrdinalIgnoreCase) ||
client.Contains("androidtv", StringComparison.OrdinalIgnoreCase))
{
int oldLen = dtoOptions.Fields.Count;
var arr = new ItemFields[oldLen + 1];
dtoOptions.Fields.CopyTo(arr, 0);
arr[oldLen] = ItemFields.ChildCount;
dtoOptions.Fields = arr;
dtoOptions.Fields = [..dtoOptions.Fields, ItemFields.ChildCount];
}
}

@ -106,18 +106,11 @@ public class TrickplayManager : ITrickplayManager
}
var imgTempDir = string.Empty;
var outputDir = GetTrickplayDirectory(video, width);
using (await _resourcePool.LockAsync(cancellationToken).ConfigureAwait(false))
{
try
{
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
{
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return;
}
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
@ -128,17 +121,35 @@ public class TrickplayManager : ITrickplayManager
return;
}
// The width has to be even, otherwise a lot of filters will not be able to sample it
var actualWidth = 2 * (width / 2);
// Force using the video width when the trickplay setting has a too large width
if (mediaSource.VideoStream.Width is not null && mediaSource.VideoStream.Width < width)
{
_logger.LogWarning("Video width {VideoWidth} is smaller than trickplay setting {TrickPlayWidth}, using video width for thumbnails", mediaSource.VideoStream.Width, width);
actualWidth = 2 * ((int)mediaSource.VideoStream.Width / 2);
}
var outputDir = GetTrickplayDirectory(video, actualWidth);
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(actualWidth))
{
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting", video.Id);
return;
}
var mediaPath = mediaSource.Path;
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", actualWidth, mediaPath, video.Id);
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
mediaPath,
container,
mediaSource,
mediaStream,
width,
actualWidth,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
options.EnableHwEncoding,
@ -159,7 +170,7 @@ public class TrickplayManager : ITrickplayManager
.ToList();
// Create tiles
var trickplayInfo = CreateTiles(images, width, options, outputDir);
var trickplayInfo = CreateTiles(images, actualWidth, options, outputDir);
// Save tiles info
try

@ -183,14 +183,13 @@ namespace MediaBrowser.Controller.Entities.Audio
progress.Report(percent * 95);
}
// get album LUFS
LUFS = items.OfType<Audio>().Max(item => item.LUFS);
var parentRefreshOptions = refreshOptions;
if (childUpdateType > ItemUpdateType.None)
{
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions);
parentRefreshOptions.MetadataRefreshMode = MetadataRefreshMode.FullRefresh;
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions)
{
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
};
}
// Refresh current item

@ -137,6 +137,13 @@ namespace MediaBrowser.Controller.Entities
[JsonIgnore]
public float? LUFS { get; set; }
/// <summary>
/// Gets or sets the gain required for audio normalization.
/// </summary>
/// <value>The gain required for audio normalization.</value>
[JsonIgnore]
public float? NormalizationGain { get; set; }
/// <summary>
/// Gets or sets the channel identifier.
/// </summary>
@ -1603,7 +1610,7 @@ namespace MediaBrowser.Controller.Entities
}
var parent = GetParents().FirstOrDefault() ?? this;
if (parent is UserRootFolder)
if (parent is UserRootFolder or AggregateFolder)
{
return true;
}
@ -1772,14 +1779,11 @@ namespace MediaBrowser.Controller.Entities
int curLen = current.Length;
if (curLen == 0)
{
Studios = new[] { name };
Studios = [name];
}
else
{
var newArr = new string[curLen + 1];
current.CopyTo(newArr, 0);
newArr[curLen] = name;
Studios = newArr;
Studios = [..current, name];
}
}
}
@ -1801,9 +1805,7 @@ namespace MediaBrowser.Controller.Entities
var genres = Genres;
if (!genres.Contains(name, StringComparison.OrdinalIgnoreCase))
{
var list = genres.ToList();
list.Add(name);
Genres = list.ToArray();
Genres = [..genres, name];
}
}
@ -1973,12 +1975,7 @@ namespace MediaBrowser.Controller.Entities
public void AddImage(ItemImageInfo image)
{
var current = ImageInfos;
var currentCount = current.Length;
var newArr = new ItemImageInfo[currentCount + 1];
current.CopyTo(newArr, 0);
newArr[currentCount] = image;
ImageInfos = newArr;
ImageInfos = [..ImageInfos, image];
}
public virtual Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)

@ -30,15 +30,11 @@ namespace MediaBrowser.Controller.Entities
if (item.RemoteTrailers.Count == 0)
{
item.RemoteTrailers = new[] { mediaUrl };
item.RemoteTrailers = [mediaUrl];
}
else
{
var oldIds = item.RemoteTrailers;
var newIds = new MediaUrl[oldIds.Count + 1];
oldIds.CopyTo(newIds);
newIds[oldIds.Count] = mediaUrl;
item.RemoteTrailers = newIds;
item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
}
}
}

@ -21,11 +21,11 @@ namespace MediaBrowser.Controller.Entities
{
if (current.Length == 0)
{
item.Tags = new[] { name };
item.Tags = [name];
}
else
{
item.Tags = current.Concat(new[] { name }).ToArray();
item.Tags = [..current, name];
}
}
}

@ -116,8 +116,8 @@ namespace MediaBrowser.Controller.Library
{
get
{
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : new[] { Path };
return AdditionalLocations is null ? paths : paths.Concat(AdditionalLocations).ToArray();
var paths = string.IsNullOrEmpty(Path) ? Array.Empty<string>() : [Path];
return AdditionalLocations is null ? paths : [..paths, ..AdditionalLocations];
}
}

@ -2984,8 +2984,8 @@ namespace MediaBrowser.Controller.MediaEncoding
var scaleW = (double)maximumWidth / outputWidth;
var scaleH = (double)maximumHeight / outputHeight;
var scale = Math.Min(scaleW, scaleH);
outputWidth = Math.Min(maximumWidth, (int)(outputWidth * scale));
outputHeight = Math.Min(maximumHeight, (int)(outputHeight * scale));
outputWidth = Math.Min(maximumWidth, Convert.ToInt32(outputWidth * scale));
outputHeight = Math.Min(maximumHeight, Convert.ToInt32(outputHeight * scale));
}
outputWidth = 2 * (outputWidth / 2);

@ -288,7 +288,7 @@ namespace MediaBrowser.Controller.Net
lock (_activeConnectionsLock)
{
foreach (var connection in _activeConnections.ToArray())
foreach (var connection in _activeConnections.ToList())
{
DisposeConnection(connection);
}

@ -270,9 +270,7 @@ namespace MediaBrowser.Controller.Session
public void AddController(ISessionController controller)
{
var controllers = SessionControllers.ToList();
controllers.Add(controller);
SessionControllers = controllers.ToArray();
SessionControllers = [..SessionControllers, controller];
}
public bool ContainsUser(Guid userId)

@ -782,10 +782,10 @@ namespace MediaBrowser.Model.Dto
public string TimerId { get; set; }
/// <summary>
/// Gets or sets the LUFS value.
/// Gets or sets the gain required for audio normalization.
/// </summary>
/// <value>The LUFS Value.</value>
public float? LUFS { get; set; }
/// <value>The gain required for audio normalization.</value>
public float? NormalizationGain { get; set; }
/// <summary>
/// Gets or sets the current program.

@ -1,9 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
@ -18,7 +15,6 @@ using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
using TagLib;
namespace MediaBrowser.Providers.MediaInfo
@ -26,12 +22,8 @@ namespace MediaBrowser.Providers.MediaInfo
/// <summary>
/// Probes audio files for metadata.
/// </summary>
public partial class AudioFileProber
public class AudioFileProber
{
// Default LUFS value for use with the web interface, at -18db gain will be 1(no db gain).
private const float DefaultLUFSValue = -18;
private readonly ILogger<AudioFileProber> _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly ILibraryManager _libraryManager;
@ -42,7 +34,6 @@ namespace MediaBrowser.Providers.MediaInfo
/// <summary>
/// Initializes a new instance of the <see cref="AudioFileProber"/> class.
/// </summary>
/// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
@ -50,7 +41,6 @@ namespace MediaBrowser.Providers.MediaInfo
/// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
/// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
public AudioFileProber(
ILogger<AudioFileProber> logger,
IMediaSourceManager mediaSourceManager,
IMediaEncoder mediaEncoder,
IItemRepository itemRepo,
@ -58,7 +48,6 @@ namespace MediaBrowser.Providers.MediaInfo
LyricResolver lyricResolver,
ILyricManager lyricManager)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_itemRepo = itemRepo;
_libraryManager = libraryManager;
@ -67,9 +56,6 @@ namespace MediaBrowser.Providers.MediaInfo
_lyricManager = lyricManager;
}
[GeneratedRegex(@"I:\s+(.*?)\s+LUFS")]
private static partial Regex LUFSRegex();
/// <summary>
/// Probes the specified item for metadata.
/// </summary>
@ -112,45 +98,6 @@ namespace MediaBrowser.Providers.MediaInfo
await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
}
var libraryOptions = _libraryManager.GetLibraryOptions(item);
if (libraryOptions.EnableLUFSScan && item.LUFS is null)
{
using (var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = _mediaEncoder.EncoderPath,
Arguments = $"-hide_banner -i \"{path}\" -af ebur128=framelog=verbose -f null -",
RedirectStandardOutput = false,
RedirectStandardError = true
},
})
{
try
{
process.Start();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ffmpeg");
throw;
}
using var reader = process.StandardError;
var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
MatchCollection split = LUFSRegex().Matches(output);
if (split.Count != 0)
{
item.LUFS = float.Parse(split[0].Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat);
}
}
}
_logger.LogDebug("LUFS for {ItemName} is {LUFS}.", item.Name, item.LUFS);
return ItemUpdateType.MetadataImport;
}
@ -339,7 +286,7 @@ namespace MediaBrowser.Providers.MediaInfo
if (!double.IsNaN(tags.ReplayGainTrackGain))
{
audio.LUFS = DefaultLUFSValue - (float)tags.ReplayGainTrackGain;
audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))

@ -103,7 +103,6 @@ namespace MediaBrowser.Providers.MediaInfo
_subtitleResolver);
_audioProber = new AudioFileProber(
loggerFactory.CreateLogger<AudioFileProber>(),
mediaSourceManager,
mediaEncoder,
itemRepo,

@ -117,9 +117,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var artist = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
{
var list = artistVideo.Artists.ToList();
list.Add(artist);
artistVideo.Artists = list.ToArray();
artistVideo.Artists = [..artistVideo.Artists, artist];
}
break;

@ -1130,7 +1130,7 @@ namespace Jellyfin.LiveTv.Channels
{
if (!item.Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase))
{
item.Tags = item.Tags.Concat(new[] { "livestream" }).ToArray();
item.Tags = [..item.Tags, "livestream"];
_logger.LogDebug("Forcing update due to Tags {0}", item.Name);
forceUpdate = true;
}

@ -60,14 +60,13 @@ public class ListingsManager : IListingsManager
var config = _config.GetLiveTvConfiguration();
var list = config.ListingProviders.ToList();
int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
var list = config.ListingProviders;
int index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
list.Add(info);
config.ListingProviders = list.ToArray();
config.ListingProviders = [..list, info];
}
else
{
@ -236,13 +235,12 @@ public class ListingsManager : IListingsManager
if (!string.Equals(tunerChannelNumber, providerChannelNumber, StringComparison.OrdinalIgnoreCase))
{
var list = listingsProviderInfo.ChannelMappings.ToList();
list.Add(new NameValuePair
var newItem = new NameValuePair
{
Name = tunerChannelNumber,
Value = providerChannelNumber
});
listingsProviderInfo.ChannelMappings = list.ToArray();
};
listingsProviderInfo.ChannelMappings = [..listingsProviderInfo.ChannelMappings, newItem];
}
_config.SaveConfiguration("livetv", config);

@ -939,7 +939,7 @@ namespace Jellyfin.LiveTv
{
var internalChannelId = _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId);
var channel = _libraryManager.GetItemById(internalChannelId);
channelName = channel is null ? null : channel.Name;
channelName = channel?.Name;
}
return _tvDtoService.GetSeriesTimerInfoDto(i.Item1, i.Item2, channelName);

@ -115,11 +115,7 @@ namespace Jellyfin.LiveTv.Timers
throw new ArgumentException("item already exists", nameof(item));
}
int oldLen = _items.Length;
var newList = new T[oldLen + 1];
_items.CopyTo(newList, 0);
newList[oldLen] = item;
_items = newList;
_items = [.._items, item];
SaveList();
}
@ -134,11 +130,7 @@ namespace Jellyfin.LiveTv.Timers
int index = Array.FindIndex(_items, i => EqualityComparer(i, item));
if (index == -1)
{
int oldLen = _items.Length;
var newList = new T[oldLen + 1];
_items.CopyTo(newList, 0);
newList[oldLen] = item;
_items = newList;
_items = [.._items, item];
}
else
{

@ -76,14 +76,13 @@ public class TunerHostManager : ITunerHostManager
var config = _config.GetLiveTvConfiguration();
var list = config.TunerHosts.ToList();
var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
var list = config.TunerHosts;
var index = Array.FindIndex(list, i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase));
if (index == -1 || string.IsNullOrWhiteSpace(info.Id))
{
info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture);
list.Add(info);
config.TunerHosts = list.ToArray();
config.TunerHosts = [..list, info];
}
else
{

Loading…
Cancel
Save